chore: import upstream snapshot with attribution
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
Linux build / Linux compile check (push) Has been cancelled
Linux build / Determine Swift version (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:25:30 +08:00
commit 680845cb1c
445 changed files with 103779 additions and 0 deletions
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerizationOCI
struct AuthChallengeTests {
internal struct TestCase: Sendable {
let input: String
let expected: AuthenticateChallenge
}
private static let testCases: [TestCase] = [
.init(
input: """
Bearer realm="https://domain.io/token",service="domain.io",scope="repository:user/image:pull"
""",
expected: .init(type: "Bearer", realm: "https://domain.io/token", service: "domain.io", scope: "repository:user/image:pull", error: nil)),
.init(
input: """
Bearer realm="https://foo-bar-registry.com/auth",service="Awesome Registry"
""",
expected: .init(type: "Bearer", realm: "https://foo-bar-registry.com/auth", service: "Awesome Registry", scope: nil, error: nil)),
.init(
input: """
Bearer realm="users.example.com", scope="create delete"
""",
expected: .init(type: "Bearer", realm: "users.example.com", service: nil, scope: "create delete", error: nil)),
.init(
input: """
Bearer realm="https://auth.server.io/token",service="registry.server.io"
""",
expected: .init(type: "Bearer", realm: "https://auth.server.io/token", service: "registry.server.io", scope: nil, error: nil)),
]
@Test(arguments: testCases)
func parseAuthHeader(testCase: TestCase) throws {
let challenges = RegistryClient.parseWWWAuthenticateHeaders(headers: [testCase.input])
#expect(challenges.count == 1)
#expect(challenges[0] == testCase.expected)
}
}
@@ -0,0 +1,119 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationError
import Crypto
import Foundation
import Testing
@testable import ContainerizationOCI
@Suite("ContentWriter Tests")
struct ContentWriterTests {
private func withTempDirectory(_ body: (URL) throws -> Void) throws {
let dir = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: dir) }
try body(dir)
}
private func makeTempFile(in dir: URL, data: Data) throws -> URL {
let url = dir.appendingPathComponent(UUID().uuidString)
try data.write(to: url)
return url
}
@Test func testWriteReturnsCorrectSizeAndDigest() throws {
try withTempDirectory { dir in
let writer = try ContentWriter(for: dir)
let data = Data("test content".utf8)
let (size, digest) = try writer.write(data)
let expected = SHA256.hash(data: data)
let destination = dir.appendingPathComponent(digest.encoded)
#expect(size == Int64(data.count))
#expect(digest == expected)
#expect(FileManager.default.fileExists(atPath: destination.path))
}
}
@Test func testWriteDuplicateData() throws {
try withTempDirectory { dir in
let writer = try ContentWriter(for: dir)
let data = Data("duplicate".utf8)
#expect(throws: Never.self) {
try writer.write(data)
try writer.write(data)
}
}
}
@Test func testCreateFromFileSmallFile() throws {
try withTempDirectory { base in
try withTempDirectory { src in
let data = Data("small file contents".utf8)
let sourceURL = try makeTempFile(in: src, data: data)
let writer = try ContentWriter(for: base)
let (size, digest) = try writer.create(from: sourceURL)
let destination = base.appendingPathComponent(digest.encoded)
let written = try Data(contentsOf: destination)
#expect(size == Int64(data.count))
#expect(digest == SHA256.hash(data: data))
#expect(FileManager.default.fileExists(atPath: destination.path))
#expect(written == data)
}
}
}
@Test func testCreateFromFileLargeFileDuplicates() throws {
try withTempDirectory { base in
try withTempDirectory { src in
let count = 3 * 1024 * 1024 + 100
var bytes = [UInt8](repeating: 0, count: count)
arc4random_buf(&bytes, count)
let data = Data(bytes)
let sourceURL = try makeTempFile(in: src, data: data)
let writer = try ContentWriter(for: base)
let (size, digest) = try writer.create(from: sourceURL)
let destination = base.appendingPathComponent(digest.encoded)
#expect(size == Int64(data.count))
#expect(digest == SHA256.hash(data: data))
#expect(throws: Never.self) {
try writer.create(from: sourceURL)
}
#expect(FileManager.default.fileExists(atPath: destination.path))
}
}
}
private struct SamplePayload: Codable, Equatable {
let name: String
let value: Int
}
@Test func testCreateFromEncodableReturnsCorrectDigest() throws {
try withTempDirectory { base in
let writer = try ContentWriter(for: base)
let payload = SamplePayload(name: "digest-check", value: 99)
let (size, digest) = try writer.create(from: payload)
let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let expected = try encoder.encode(payload)
#expect(size == Int64(expected.count))
#expect(digest == SHA256.hash(data: expected))
}
}
}
@@ -0,0 +1,66 @@
//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerizationOCI
@Suite
struct LocalContentStoreTests {
private static let digestA = String(repeating: "a", count: 64)
private static let digestB = String(repeating: "b", count: 64)
@Test func totalAllocatedSizeReportsZeroForEmptyStore() async throws {
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
defer { try? FileManager.default.removeItem(at: dir) }
let store = try LocalContentStore(path: dir)
let size = try await store.totalAllocatedSize()
#expect(size == 0)
}
@Test func totalAllocatedSizeReflectsCommittedBlobs() async throws {
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
defer { try? FileManager.default.removeItem(at: dir) }
let store = try LocalContentStore(path: dir)
let payload = Data(repeating: 0xAB, count: 64 * 1024)
try await store.ingest { tempDir in
try payload.write(to: tempDir.appendingPathComponent(Self.digestA))
}
let size = try await store.totalAllocatedSize()
#expect(size >= UInt64(payload.count))
}
@Test func totalAllocatedSizeIncludesInFlightIngest() async throws {
let dir = FileManager.default.uniqueTemporaryDirectory(create: true)
defer { try? FileManager.default.removeItem(at: dir) }
let store = try LocalContentStore(path: dir)
let payload = Data(repeating: 0xCD, count: 32 * 1024)
let session = try await store.newIngestSession()
try payload.write(to: session.ingestDir.appendingPathComponent(Self.digestB))
let size = try await store.totalAllocatedSize()
#expect(size >= UInt64(payload.count))
try await store.cancelIngestSession(session.id)
}
}
@@ -0,0 +1,164 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import Foundation
import Testing
@testable import ContainerizationOCI
struct OCITests {
@Test func config() {
let config = ContainerizationOCI.ImageConfig()
let rootfs = ContainerizationOCI.Rootfs(type: "foo", diffIDs: ["diff1", "diff2"])
let history = ContainerizationOCI.History()
let image = ContainerizationOCI.Image(architecture: "arm64", os: "linux", config: config, rootfs: rootfs, history: [history])
#expect(image.rootfs.type == "foo")
}
@Test func descriptor() {
let platform = ContainerizationOCI.Platform(arch: "arm64", os: "linux")
let descriptor = ContainerizationOCI.Descriptor(mediaType: MediaTypes.descriptor, digest: "123", size: 0, platform: platform)
#expect(descriptor.platform?.architecture == "arm64")
#expect(descriptor.platform?.os == "linux")
#expect(descriptor.artifactType == nil)
}
@Test func descriptorWithArtifactType() throws {
let testArtifactType = "application/vnd.example.test.v1+json"
let descriptor = ContainerizationOCI.Descriptor(
mediaType: MediaTypes.imageManifest,
digest: "sha256:abc123",
size: 1234,
artifactType: testArtifactType
)
#expect(descriptor.artifactType == testArtifactType)
let data = try JSONEncoder().encode(descriptor)
let decoded = try JSONDecoder().decode(ContainerizationOCI.Descriptor.self, from: data)
#expect(decoded.artifactType == testArtifactType)
}
@Test func descriptorWithoutArtifactTypeDecodesAsNil() throws {
let json = """
{"mediaType":"application/vnd.oci.descriptor.v1+json","digest":"sha256:abc","size":0}
"""
let decoded = try JSONDecoder().decode(ContainerizationOCI.Descriptor.self, from: json.data(using: .utf8)!)
#expect(decoded.artifactType == nil)
}
@Test func index() {
var descriptors: [ContainerizationOCI.Descriptor] = []
for i in 0..<5 {
let descriptor = ContainerizationOCI.Descriptor(mediaType: MediaTypes.descriptor, digest: "\(i)", size: Int64(i))
descriptors.append(descriptor)
}
let index = ContainerizationOCI.Index(schemaVersion: 1, manifests: descriptors)
#expect(index.manifests.count == 5)
#expect(index.subject == nil)
#expect(index.artifactType == nil)
}
@Test func indexWithSubjectAndArtifactType() throws {
let testArtifactType = "application/vnd.example.test.v1+json"
let subject = ContainerizationOCI.Descriptor(mediaType: MediaTypes.imageManifest, digest: "sha256:subject", size: 512)
let index = ContainerizationOCI.Index(
schemaVersion: 2,
manifests: [],
subject: subject,
artifactType: testArtifactType
)
#expect(index.subject?.digest == "sha256:subject")
#expect(index.artifactType == testArtifactType)
let data = try JSONEncoder().encode(index)
let decoded = try JSONDecoder().decode(ContainerizationOCI.Index.self, from: data)
#expect(decoded.subject?.digest == "sha256:subject")
#expect(decoded.artifactType == testArtifactType)
}
@Test func indexDecodesWithoutNewFields() throws {
let json = """
{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.descriptor.v1+json","digest":"sha256:abc","size":10}]}
"""
let decoded = try JSONDecoder().decode(ContainerizationOCI.Index.self, from: json.data(using: .utf8)!)
#expect(decoded.schemaVersion == 2)
#expect(decoded.manifests.count == 1)
#expect(decoded.subject == nil)
#expect(decoded.artifactType == nil)
}
@Test func manifests() {
var descriptors: [ContainerizationOCI.Descriptor] = []
for i in 0..<5 {
let descriptor = ContainerizationOCI.Descriptor(mediaType: MediaTypes.descriptor, digest: "\(i)", size: Int64(i))
descriptors.append(descriptor)
}
let config = ContainerizationOCI.Descriptor(mediaType: MediaTypes.descriptor, digest: "123", size: 0)
let manifest = ContainerizationOCI.Manifest(schemaVersion: 1, config: config, layers: descriptors)
#expect(manifest.config.digest == "123")
#expect(manifest.layers.count == 5)
#expect(manifest.subject == nil)
#expect(manifest.artifactType == nil)
}
@Test func manifestWithSubjectAndArtifactType() throws {
let testArtifactType = "application/vnd.example.test.v1+json"
let config = ContainerizationOCI.Descriptor(mediaType: MediaTypes.emptyJSON, digest: "sha256:empty", size: 2)
let subject = ContainerizationOCI.Descriptor(mediaType: MediaTypes.imageManifest, digest: "sha256:target", size: 1234)
let layer = ContainerizationOCI.Descriptor(
mediaType: testArtifactType,
digest: "sha256:meta",
size: 89,
annotations: ["org.opencontainers.image.title": "metadata.json"]
)
let manifest = ContainerizationOCI.Manifest(
config: config,
layers: [layer],
subject: subject,
artifactType: testArtifactType
)
#expect(manifest.subject?.digest == "sha256:target")
#expect(manifest.artifactType == testArtifactType)
#expect(manifest.layers[0].annotations?["org.opencontainers.image.title"] == "metadata.json")
let data = try JSONEncoder().encode(manifest)
let decoded = try JSONDecoder().decode(ContainerizationOCI.Manifest.self, from: data)
#expect(decoded.subject?.digest == "sha256:target")
#expect(decoded.artifactType == testArtifactType)
}
@Test func manifestDecodesWithoutNewFields() throws {
let json = """
{
"schemaVersion": 2,
"config": {"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:abc","size":2},
"layers": []
}
"""
let decoded = try JSONDecoder().decode(ContainerizationOCI.Manifest.self, from: json.data(using: .utf8)!)
#expect(decoded.schemaVersion == 2)
#expect(decoded.subject == nil)
#expect(decoded.artifactType == nil)
}
}
@@ -0,0 +1,84 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import Testing
@testable import ContainerizationOCI
struct OCIPlatformTests {
@Test func identicalPlatforms() {
let amd64lhs = Platform(arch: "amd64", os: "linux")
let amd64rhs = Platform(arch: "amd64", os: "linux")
#expect(amd64lhs == amd64rhs, "amd64 platforms should be equal")
let arm64lhs = Platform(arch: "arm64", os: "linux")
let arm64rhs = Platform(arch: "arm64", os: "linux")
#expect(arm64lhs == arm64rhs, "arm64 platforms should be equal")
}
@Test func differentOS() {
let lhs = Platform(arch: "arm64", os: "linux")
let rhs = Platform(arch: "arm64", os: "darwin")
#expect(lhs != rhs, "Different OS should not be equal")
}
@Test func differentArch() {
let lhs = Platform(arch: "amd64", os: "linux")
let rhs = Platform(arch: "arm64", os: "linux")
#expect(lhs != rhs, "Different arch should not be equal")
}
@Test func arm64_sameVariant() {
let lhs = Platform(arch: "arm64", os: "linux", variant: "v8")
let rhs = Platform(arch: "arm64", os: "linux", variant: "v8")
#expect(lhs == rhs, "Both OS arm64, same arch, same variant => equal")
}
@Test func arm64_nilAndV8() {
let lhs = Platform(arch: "arm64", os: "linux", variant: nil)
let rhs = Platform(arch: "arm64", os: "linux", variant: "v8")
#expect(lhs == rhs, "One variant nil and other v8 => equal under special arm64 rule")
}
@Test func arm64_nilAndV7() {
let lhs = Platform(arch: "arm64", os: "linux", variant: nil)
let rhs = Platform(arch: "arm64", os: "linux", variant: "v7")
#expect(lhs != rhs, "nil vs v7 is not covered by the special rule => not equal")
}
@Test func arm64_bothNil() {
let lhs = Platform(arch: "arm64", os: "linux", variant: nil)
let rhs = Platform(arch: "arm64", os: "linux", variant: nil)
#expect(lhs == rhs, "Both nil variants => variantEqual is true => overall equal")
}
@Test func arm64_nilAndV8_sameHashValue() {
let withoutVariant = Platform(arch: "arm64", os: "linux", variant: nil)
let withV8 = Platform(arch: "arm64", os: "linux", variant: "v8")
// Equal platforms must produce the same hash violating this breaks Set/Dictionary lookups
#expect(withoutVariant.hashValue == withV8.hashValue, "arm64 nil variant and v8 must hash identically")
}
@Test func arm64_nilAndV8_setLookup() {
let withoutVariant = Platform(arch: "arm64", os: "linux", variant: nil)
let withV8 = Platform(arch: "arm64", os: "linux", variant: "v8")
var set = Set<Platform>()
set.insert(withoutVariant)
#expect(set.contains(withV8), "arm64/v8 must be found in a Set that contains arm64 with nil variant")
}
}
@@ -0,0 +1,168 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
import Testing
@testable import ContainerizationOCI
struct OCISpecTests {
@Test func minimalSpecDecode() throws {
let version = "1.2.3"
let minSpec =
"""
{
"ociVersion": "\(version)"
}
"""
guard let data = minSpec.data(using: .utf8) else {
Issue.record("test spec is not valid: \(minSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Spec.self, from: data)
#expect(decodedSpec.version == version)
#expect(decodedSpec.hooks == nil)
#expect(decodedSpec.process == nil)
#expect(decodedSpec.hostname == "")
#expect(decodedSpec.domainname == "")
#expect(decodedSpec.mounts.isEmpty)
#expect(decodedSpec.annotations == nil)
#expect(decodedSpec.root == nil)
#expect(decodedSpec.linux == nil)
}
@Test func minimalProcessSpecDecode() throws {
let cwd = "/test"
let minProcessSpec =
"""
{
"cwd": "\(cwd)",
"user": {
"uid": 10,
"gid": 11
}
}
"""
guard let data = minProcessSpec.data(using: .utf8) else {
Issue.record("test process spec is not valid: \(minProcessSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Process.self, from: data)
#expect(decodedSpec.cwd == cwd)
#expect(decodedSpec.env.isEmpty)
#expect(decodedSpec.consoleSize == nil)
#expect(decodedSpec.selinuxLabel == "")
#expect(decodedSpec.noNewPrivileges == false)
#expect(decodedSpec.commandLine == "")
#expect(decodedSpec.oomScoreAdj == nil)
#expect(decodedSpec.capabilities == nil)
#expect(decodedSpec.apparmorProfile == "")
#expect(decodedSpec.user.uid == 10)
#expect(decodedSpec.user.gid == 11)
#expect(decodedSpec.rlimits.isEmpty)
#expect(decodedSpec.terminal == false)
}
@Test func minimalUserSpecDecode() throws {
let minUserSpec =
"""
{
"uid": 10,
"gid": 11
}
"""
guard let data = minUserSpec.data(using: .utf8) else {
Issue.record("test user spec is not valid: \(minUserSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.User.self, from: data)
#expect(decodedSpec.uid == 10)
#expect(decodedSpec.gid == 11)
#expect(decodedSpec.umask == nil)
#expect(decodedSpec.additionalGids.isEmpty)
#expect(decodedSpec.username == "")
}
@Test func minimalRootSpecDecode() throws {
let path = "/testpath"
let minRootSpec =
"""
{
"path": "\(path)"
}
"""
guard let data = minRootSpec.data(using: .utf8) else {
Issue.record("test root spec is not valid: \(minRootSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Root.self, from: data)
#expect(decodedSpec.path == path)
#expect(decodedSpec.readonly == false)
}
@Test func minimalMountSpecDecode() throws {
let destination = "/testdest"
let minMountSpec =
"""
{
"destination": "\(destination)"
}
"""
guard let data = minMountSpec.data(using: .utf8) else {
Issue.record("test mount spec is not valid: \(minMountSpec)")
return
}
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Mount.self, from: data)
#expect(decodedSpec.type == "")
#expect(decodedSpec.source == "")
#expect(decodedSpec.destination == destination)
#expect(decodedSpec.options.isEmpty)
#expect(decodedSpec.uidMappings == nil)
#expect(decodedSpec.gidMappings == nil)
}
@Test func minimalCapabilitiesDecode() throws {
let minCapabilitiesSpec =
"""
{
"ociVersion": "1.1.0",
"capabilities": {
"permitted": [
"CAP_SYS_ADMIN"
]
},
"linux": {}
}
"""
guard let data = minCapabilitiesSpec.data(using: .utf8) else {
Issue.record("test capabilities spec is not valid: \(minCapabilitiesSpec)")
return
}
let _ = try JSONDecoder().decode(ContainerizationOCI.Spec.self, from: data)
}
}
@@ -0,0 +1,118 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
// swiftlint:disable force_cast large_tuple
import ContainerizationError
import Foundation
import Testing
@testable import ContainerizationOCI
@Suite("Reference Parse Tests")
struct ReferenceParseTests {
internal struct ReferenceParseTestCase: Sendable {
let input: String
let domain: String?
let path: String
let tag: String?
let digest: String?
init(input: String, domain: String? = nil, path: String, tag: String? = nil, digest: String? = nil) {
self.input = input
self.domain = domain
self.path = path
self.tag = tag
self.digest = digest
}
}
@Test(arguments: [
ReferenceParseTestCase(input: "tensorflow/tensorflow", path: "tensorflow/tensorflow"),
ReferenceParseTestCase(input: "debian", path: "debian"),
ReferenceParseTestCase(input: "repo_with_underscore", path: "repo_with_underscore"),
ReferenceParseTestCase(input: "swift5.10:alpine", path: "swift5.10", tag: "alpine"),
ReferenceParseTestCase(input: "registry.com.with.port:5000/no_tag", domain: "registry.com.with.port:5000", path: "no_tag"),
ReferenceParseTestCase(input: "registry.com.with.port:5000/name/foo/bar:tag23", domain: "registry.com.with.port:5000", path: "name/foo/bar", tag: "tag23"),
ReferenceParseTestCase(input: "some-repo-with-dashes/name", path: "some-repo-with-dashes/name"),
ReferenceParseTestCase(input: "domain.with-dashes/cool-image:foo", domain: "domain.with-dashes", path: "cool-image", tag: "foo"),
ReferenceParseTestCase(input: "localhost:8080/123:latest", domain: "localhost:8080", path: "123", tag: "latest"),
ReferenceParseTestCase(
input: "localhost/123@sha256:\(String(repeating: "a", count: 64))", domain: "localhost", path: "123", digest: "sha256:\(String(repeating: "a", count: 64))"),
ReferenceParseTestCase(
input: "registry.com.with.port:1254/foo/bar/baz@sha256:\(String(repeating: "abcd", count: 16))", domain: "registry.com.with.port:1254", path: "foo/bar/baz",
digest: "sha256:\(String(repeating: "abcd", count: 16))"),
ReferenceParseTestCase(input: "192.168.1.1:5544/local/swift:6.0", domain: "192.168.1.1:5544", path: "local/swift", tag: "6.0"),
ReferenceParseTestCase(input: "[abc12::4]:5683/swift", domain: "[abc12::4]:5683", path: "swift"),
// Verify names longer than 127 characters are accepted (OCI spec allows up to 255).
ReferenceParseTestCase(
input: "reg.io/\(String(repeating: "a", count: 121))",
domain: "reg.io",
path: String(repeating: "a", count: 121)),
// Verify a name of exactly 255 characters (the OCI spec maximum) is accepted.
ReferenceParseTestCase(
input: "registry.example.com/\(String(repeating: "a", count: 234))",
domain: "registry.example.com",
path: String(repeating: "a", count: 234)),
])
func validReferenceParse(testCase: ReferenceParseTestCase) async throws {
#expect(throws: Never.self) {
let parsed = try Reference.parse(testCase.input)
#expect(parsed.path == testCase.path)
#expect(parsed.domain == testCase.domain)
#expect(parsed.digest == testCase.digest)
#expect(parsed.tag == testCase.tag)
}
}
@Test(arguments: [
"localhost:8080",
"localhost/123@sha256:\(String(repeating: "a", count: 200))",
"https://github.com/apple/containerization",
"",
"-testString",
"-testString/image",
"-testString.com/image/release",
"foo///bar",
"mostly.valid/image/but/Caps",
"[abc12::4]",
"[abc12::4]:abc12::4",
"[2001:db8:3:4::192.0.2.33]:5000/debian",
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
// A name of 256 characters exceeds the OCI spec limit of 255 and must be rejected.
"registry.example.com/\(String(repeating: "a", count: 235))",
])
func invalidReferenceParse(input: String) async throws {
#expect(throws: ContainerizationError.self) {
try Reference.parse(input)
}
}
@Test(arguments: [
ReferenceParseTestCase(input: "only_name", path: "only_name", tag: "latest"),
ReferenceParseTestCase(input: "docker.io/alpine", domain: "docker.io", path: "library/alpine", tag: "latest"),
ReferenceParseTestCase(input: "ghcr.io/myrepo/alpine", domain: "ghcr.io", path: "myrepo/alpine", tag: "latest"),
ReferenceParseTestCase(input: "name@sha256:" + String(repeating: "1", count: 64), path: "name", digest: "sha256:" + String(repeating: "1", count: 64)),
ReferenceParseTestCase(input: "registry-1.docker.io/testrepo/myname:v2", domain: "registry-1.docker.io", path: "testrepo/myname", tag: "v2"),
])
func testNormalize(testCase: ReferenceParseTestCase) throws {
let parsed = try Reference.parse(testCase.input)
parsed.normalize()
#expect(parsed.path == testCase.path)
#expect(parsed.domain == testCase.domain)
#expect(parsed.digest == testCase.digest)
#expect(parsed.tag == testCase.tag)
}
}
@@ -0,0 +1,366 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
//
import ContainerizationError
import ContainerizationIO
import Crypto
import Foundation
import NIO
import Synchronization
import Testing
@testable import ContainerizationOCI
struct OCIClientTests: ~Copyable {
private var contentPath: URL
private let fileManager = FileManager.default
private var encoder = JSONEncoder()
init() async throws {
let testDir = fileManager.uniqueTemporaryDirectory()
let contentPath = testDir.appendingPathComponent("content")
try fileManager.createDirectory(at: contentPath, withIntermediateDirectories: true)
self.contentPath = contentPath
encoder.outputFormatting = .prettyPrinted
}
deinit {
try? fileManager.removeItem(at: contentPath)
}
private static var arch: String? {
var uts = utsname()
let result = uname(&uts)
guard result == EXIT_SUCCESS else {
return nil
}
let machine = Data(bytes: &uts.machine, count: 256)
guard let arch = String(bytes: machine, encoding: .utf8) else {
return nil
}
switch arch.lowercased().trimmingCharacters(in: .controlCharacters) {
case "arm64":
return "arm64"
default:
return "amd64"
}
}
@Test(.enabled(if: hasRegistryCredentials))
func fetchToken() async throws {
let client = RegistryClient(host: "ghcr.io", authentication: Self.authentication)
let request = TokenRequest(realm: "https://ghcr.io/token", service: "ghcr.io", clientId: "tests", scope: nil)
let response = try await client.fetchToken(request: request)
#expect(response.getToken() != nil)
}
@Test(arguments: [
"registry-1.docker.io",
"public.ecr.aws",
"registry.k8s.io",
"mcr.microsoft.com",
])
func ping(host: String) async throws {
let client = RegistryClient(host: host)
try await client.ping()
}
@Test func pingWithInvalidCredentials() async throws {
let authentication = BasicAuthentication(username: "foo", password: "bar")
let client = RegistryClient(host: "ghcr.io", authentication: authentication)
let error = await #expect(throws: RegistryClient.Error.self) { try await client.ping() }
guard case .invalidStatus(_, let status, let reason) = error else {
throw error!
}
#expect(status == .unauthorized)
#expect(reason == "access denied or wrong credentials")
}
@Test(.enabled(if: hasRegistryCredentials))
func pingWithCredentials() async throws {
let client = RegistryClient(host: "ghcr.io", authentication: Self.authentication)
try await client.ping()
}
@Test func resolve() async throws {
let client = RegistryClient(host: "ghcr.io")
let descriptor = try await client.resolve(name: "apple/containerization/dockermanifestimage", tag: "0.0.2")
#expect(descriptor.mediaType == MediaTypes.dockerManifest)
#expect(descriptor.size != 0)
#expect(!descriptor.digest.isEmpty)
}
@Test func resolveSha() async throws {
let client = RegistryClient(host: "ghcr.io")
let descriptor = try await client.resolve(
name: "apple/containerization/dockermanifestimage", tag: "sha256:c8d344d228b7d9a702a95227438ec0d71f953a9a483e28ffabc5704f70d2b61e")
let namedDescriptor = try await client.resolve(name: "apple/containerization/dockermanifestimage", tag: "0.0.2")
#expect(descriptor == namedDescriptor)
#expect(descriptor.mediaType == MediaTypes.dockerManifest)
#expect(descriptor.size != 0)
#expect(!descriptor.digest.isEmpty)
}
@Test func fetchManifest() async throws {
let client = RegistryClient(host: "ghcr.io")
let descriptor = try await client.resolve(name: "apple/containerization/dockermanifestimage", tag: "0.0.2")
let manifest: Manifest = try await client.fetch(name: "apple/containerization/dockermanifestimage", descriptor: descriptor)
#expect(manifest.schemaVersion == 2)
#expect(manifest.layers.count == 1)
}
@Test func fetchManifestAsData() async throws {
let client = RegistryClient(host: "ghcr.io")
let descriptor = try await client.resolve(name: "apple/containerization/dockermanifestimage", tag: "0.0.2")
let manifestData = try await client.fetchData(name: "apple/containerization/dockermanifestimage", descriptor: descriptor)
let checksum = SHA256.hash(data: manifestData)
#expect(descriptor.digest == checksum.digest)
}
@Test func fetchConfig() async throws {
let client = RegistryClient(host: "ghcr.io")
let descriptor = try await client.resolve(name: "apple/containerization/dockermanifestimage", tag: "0.0.2")
let manifest: Manifest = try await client.fetch(name: "apple/containerization/dockermanifestimage", descriptor: descriptor)
let image: Image = try await client.fetch(name: "apple/containerization/dockermanifestimage", descriptor: manifest.config)
// This is an empty image -- check that the image label is present in the image config
#expect(image.config?.labels?["org.opencontainers.image.source"] == "https://github.com/apple/containerization")
#expect(image.rootfs.diffIDs.count == 1)
}
@Test func fetchBlob() async throws {
let client = RegistryClient(host: "ghcr.io")
let descriptor = try await client.resolve(name: "apple/containerization/dockermanifestimage", tag: "0.0.2")
let manifest: Manifest = try await client.fetch(name: "apple/containerization/dockermanifestimage", descriptor: descriptor)
var called = false
var done = false
try await client.fetchBlob(name: "apple/containerization/dockermanifestimage", descriptor: manifest.layers.first!) { (expected, body) in
called = true
#expect(expected != 0)
var received = 0
for try await buffer in body {
received += buffer.readableBytes
if received == expected {
done = true
}
}
}
#expect(called)
#expect(done)
}
@Test(.disabled("External users cannot push images, disable while we find a better solution"))
func pushIndex() async throws {
let client = RegistryClient(host: "ghcr.io", authentication: Self.authentication)
let indexDescriptor = try await client.resolve(name: "apple/containerization/emptyimage", tag: "0.0.1")
let index: Index = try await client.fetch(name: "apple/containerization/emptyimage", descriptor: indexDescriptor)
let platform = Platform(arch: "amd64", os: "linux")
var manifestDescriptor: Descriptor?
for m in index.manifests where m.platform == platform {
manifestDescriptor = m
break
}
#expect(manifestDescriptor != nil)
let manifest: Manifest = try await client.fetch(name: "apple/containerization/emptyimage", descriptor: manifestDescriptor!)
let imgConfig: Image = try await client.fetch(name: "apple/containerization/emptyimage", descriptor: manifest.config)
let layer = try #require(manifest.layers.first)
let blobPath = contentPath.appendingPathComponent(layer.digest)
let outputStream = OutputStream(toFileAtPath: blobPath.path, append: false)
#expect(outputStream != nil)
try await outputStream!.withThrowingOpeningStream {
try await client.fetchBlob(name: "apple/containerization/emptyimage", descriptor: layer) { (expected, body) in
var received: Int64 = 0
for try await buffer in body {
received += Int64(buffer.readableBytes)
buffer.withUnsafeReadableBytes { pointer in
let unsafeBufferPointer = pointer.bindMemory(to: UInt8.self)
if let addr = unsafeBufferPointer.baseAddress {
_ = outputStream!.write(addr, maxLength: buffer.readableBytes)
}
}
}
#expect(received == expected)
}
}
let name = "apple/test-images/image-push"
let ref = "latest"
// Push the layer first.
do {
let content = try LocalContent(path: blobPath)
let generator = {
let stream = try ReadStream(url: content.path)
try stream.reset()
return stream.stream
}
try await client.push(name: name, ref: ref, descriptor: layer, streamGenerator: generator, progress: nil)
} catch let err as ContainerizationError {
guard err.code == .exists else {
throw err
}
}
// Push the image configuration.
var imgConfigDesc: Descriptor?
do {
imgConfigDesc = try await self.pushDescriptor(
client: client,
name: name,
ref: ref,
content: imgConfig,
baseDescriptor: manifest.config
)
} catch let err as ContainerizationError {
guard err.code != .exists else {
return
}
throw err
}
// Push the image manifest.
let newManifest = Manifest(
schemaVersion: manifest.schemaVersion,
mediaType: manifest.mediaType!,
config: imgConfigDesc!,
layers: manifest.layers,
annotations: manifest.annotations
)
let manifestDesc = try await self.pushDescriptor(
client: client,
name: name,
ref: ref,
content: newManifest,
baseDescriptor: manifestDescriptor!
)
// Push the index.
let newIndex = Index(
schemaVersion: index.schemaVersion,
mediaType: index.mediaType,
manifests: [manifestDesc],
annotations: index.annotations
)
try await self.pushDescriptor(
client: client,
name: name,
ref: ref,
content: newIndex,
baseDescriptor: indexDescriptor
)
}
@Test func resolveWithRetry() async throws {
let counter = Mutex(0)
let client = RegistryClient(
host: "ghcr.io",
retryOptions: RetryOptions(
maxRetries: 3,
retryInterval: 500_000_000,
shouldRetry: ({ response in
if response.status == .notFound {
counter.withLock { $0 += 1 }
return true
}
return false
})
)
)
do {
_ = try await client.resolve(name: "containerization/not-exists", tag: "foo")
} catch {
#expect(counter.withLock { $0 } <= 3)
}
}
// MARK: private functions
static var hasRegistryCredentials: Bool {
authentication != nil
}
static var authentication: Authentication? {
let env = ProcessInfo.processInfo.environment
guard let password = env["REGISTRY_TOKEN"],
let username = env["REGISTRY_USERNAME"]
else {
return nil
}
return BasicAuthentication(username: username, password: password)
}
@discardableResult
private func pushDescriptor<T: Encodable>(
client: RegistryClient,
name: String,
ref: String,
content: T,
baseDescriptor: Descriptor
) async throws -> Descriptor {
let encoded = try self.encoder.encode(content)
let digest = SHA256.hash(data: encoded)
let descriptor = Descriptor(
mediaType: baseDescriptor.mediaType,
digest: digest.digest,
size: Int64(encoded.count),
urls: baseDescriptor.urls,
annotations: baseDescriptor.annotations,
platform: baseDescriptor.platform
)
let generator = {
let stream = ReadStream(data: encoded)
try stream.reset()
return stream.stream
}
try await client.push(
name: name,
ref: ref,
descriptor: descriptor,
streamGenerator: generator,
progress: nil
)
return descriptor
}
}
extension OutputStream {
fileprivate func withThrowingOpeningStream(_ closure: () async throws -> Void) async throws {
self.open()
defer { self.close() }
try await closure()
}
}
extension SHA256.Digest {
fileprivate var digest: String {
let parts = self.description.split(separator: ": ")
return "sha256:\(parts[1])"
}
}