Files
wehub-resource-sync 680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

243 lines
9.6 KiB
Swift

//===----------------------------------------------------------------------===//
// 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 ContainerizationExtras
import Crypto
import Foundation
import NIOCore
import NIOFoundationCompat
package final class LocalOCILayoutClient: ContentClient {
let cs: LocalContentStore
package init(root: URL) throws {
self.cs = try LocalContentStore(path: root)
}
private func _fetch(digest: String) async throws -> Content {
guard let c: Content = try await self.cs.get(digest: digest) else {
throw Error.missingContent(digest)
}
return c
}
private func calculateFileDigest(at url: URL) throws -> SHA256Digest {
let fileHandle = try FileHandle(forReadingFrom: url)
defer {
try? fileHandle.close()
}
var hasher = SHA256()
let chunkSize = Int(getpagesize()) * 1024
while true {
let chunk = fileHandle.readData(ofLength: chunkSize)
if chunk.isEmpty {
break
}
hasher.update(data: chunk)
}
return hasher.finalize()
}
package func fetch<T: Codable>(name: String, descriptor: Descriptor) async throws -> T {
let c = try await self._fetch(digest: descriptor.digest)
return try c.decode()
}
package func fetchBlob(name: String, descriptor: Descriptor, into file: URL, progress: ProgressHandler?) async throws -> (Int64, SHA256Digest) {
let c = try await self._fetch(digest: descriptor.digest)
let fileManager = FileManager.default
let filePath = file.absolutePath()
do {
let src = c.path
try fileManager.copyItem(at: src, to: file)
if let progress, let fileSize = fileManager.fileSize(atPath: filePath) {
await progress([
.addSize(fileSize)
])
}
} catch let error as NSError {
guard error.code == NSFileWriteFileExistsError else {
throw error
}
do {
let expectedDigest = try c.digest()
let existingDigest = try calculateFileDigest(at: file)
guard existingDigest.digestString == expectedDigest.digestString else {
throw ContainerizationError(
.internalError,
message:
"file \(filePath) exists but contains different content, expected digest: \(expectedDigest.digestString), existing digest: \(existingDigest.digestString)"
)
}
if let progress, let fileSize = fileManager.fileSize(atPath: filePath) {
await progress([
.addSize(fileSize)
])
}
} catch {
throw error
}
}
let size = try Int64(c.size())
let digest = try c.digest()
return (size, digest)
}
package func fetchData(name: String, descriptor: Descriptor) async throws -> Data {
let c = try await self._fetch(digest: descriptor.digest)
return try c.data()
}
package func push<T: Sendable & AsyncSequence>(
name: String,
ref: String,
descriptor: Descriptor,
streamGenerator: () throws -> T,
progress: ProgressHandler?
) async throws where T.Element == ByteBuffer {
let input = try streamGenerator()
let (id, dir) = try await self.cs.newIngestSession()
do {
let into = dir.appendingPathComponent(descriptor.digest.trimmingDigestPrefix)
guard FileManager.default.createFile(atPath: into.path, contents: nil) else {
throw Error.cannotCreateFile
}
let fd = try FileHandle(forWritingTo: into)
defer {
try? fd.close()
}
var wrote = 0
var hasher = SHA256()
for try await buffer in input {
wrote += buffer.readableBytes
try fd.write(contentsOf: buffer.readableBytesView)
hasher.update(data: buffer.readableBytesView)
}
try await self.cs.completeIngestSession(id)
} catch {
try await self.cs.cancelIngestSession(id)
}
}
}
extension LocalOCILayoutClient {
private static let ociLayoutFileName = "oci-layout"
private static let ociLayoutVersionString = "imageLayoutVersion"
private static let ociLayoutIndexFileName = "index.json"
package func loadIndexFromOCILayout(directory: URL) throws -> ContainerizationOCI.Index {
let fm = FileManager.default
let decoder = JSONDecoder()
let ociLayoutFile = directory.appendingPathComponent(Self.ociLayoutFileName)
guard fm.fileExists(atPath: ociLayoutFile.absolutePath()) else {
throw ContainerizationError(.notFound, message: ociLayoutFile.absolutePath())
}
var data = try Data(contentsOf: ociLayoutFile)
let ociLayout = try decoder.decode([String: String].self, from: data)
guard ociLayout[Self.ociLayoutVersionString] != nil else {
throw ContainerizationError(.empty, message: "missing key \(Self.ociLayoutVersionString) in \(ociLayoutFile.absolutePath())")
}
let indexFile = directory.appendingPathComponent(Self.ociLayoutIndexFileName)
guard fm.fileExists(atPath: indexFile.absolutePath()) else {
throw ContainerizationError(.notFound, message: indexFile.absolutePath())
}
data = try Data(contentsOf: indexFile)
let index = try decoder.decode(ContainerizationOCI.Index.self, from: data)
return index
}
package func createOCILayoutStructure(directory: URL, manifests: [Descriptor]) throws {
let fm = FileManager.default
let encoder = JSONEncoder()
encoder.outputFormatting = [.withoutEscapingSlashes]
let ingestDir = directory.appendingPathComponent("ingest")
try? fm.removeItem(at: ingestDir)
let ociLayoutContent: [String: String] = [
Self.ociLayoutVersionString: "1.0.0"
]
var data = try encoder.encode(ociLayoutContent)
var p = directory.appendingPathComponent(Self.ociLayoutFileName).absolutePath()
guard fm.createFile(atPath: p, contents: data) else {
throw ContainerizationError(.internalError, message: "failed to create file \(p)")
}
let idx = ContainerizationOCI.Index(schemaVersion: 2, manifests: manifests)
data = try encoder.encode(idx)
p = directory.appendingPathComponent(Self.ociLayoutIndexFileName).absolutePath()
guard fm.createFile(atPath: p, contents: data) else {
throw ContainerizationError(.internalError, message: "failed to create file \(p)")
}
}
package func setImageReferenceAnnotation(descriptor: inout Descriptor, reference: String) {
var annotations = descriptor.annotations ?? [:]
annotations[AnnotationKeys.containerizationImageName] = reference
annotations[AnnotationKeys.containerdImageName] = reference
annotations[AnnotationKeys.openContainersImageName] = reference
descriptor.annotations = annotations
}
package func getImageReferencefromDescriptor(descriptor: Descriptor) -> String {
let annotations = descriptor.annotations
// Annotations here do not conform to the OCI image specification.
// The interpretation of the annotations "org.opencontainers.image.ref.name" and
// "io.containerd.image.name" is under debate:
// - OCI spec examples suggest it should be the image tag:
// https://github.com/opencontainers/image-spec/blob/fbb4662eb53b80bd38f7597406cf1211317768f0/image-layout.md?plain=1#L175
// - Buildkitd maintainers argue it should represent the full image name:
// https://github.com/moby/buildkit/issues/4615#issuecomment-2521810830
// Until a consensus is reached, the preference is given to "com.apple.containerization.image.name" and then to
// using "io.containerd.image.name" as it is the next safest choice
if let annotations {
if let name = annotations[AnnotationKeys.containerizationImageName] {
return name
}
if let name = annotations[AnnotationKeys.containerdImageName] {
return name
}
if let name = annotations[AnnotationKeys.openContainersImageName] {
return name
}
}
// Fallback: Generate digest-based reference for images without annotations
// This makes sure OCI spec compliance as annotations are optional
return "untagged@\(descriptor.digest)"
}
package enum Error: Swift.Error {
case missingContent(_ digest: String)
case unsupportedInput
case cannotCreateFile
}
}