Create unpacker protocol + ext4 unpacker (#151)

Creates a new Unpacker protocol that defines a single method

```
func unpack(_ image: Image, for platform: Platform, at path: URL, progress: ProgressHandler?) async throws -> Mount
```

This change also removes the `unpack(...)` method from the Image type.

Before
```
let mount = try await image.unpack(for: platform, at: path)
```
After
```
let unpacker = EXT4Unpacker(blockSizeInBytes: 2.gib())
let mount = try await unpacker.unpack(image, for: platform, at: path)
```

---------

Signed-off-by: Aditya Ramani <a_ramani@apple.com>
This commit is contained in:
Aditya Ramani
2025-06-30 11:06:38 -07:00
committed by GitHub
parent 631676b855
commit bdba5b5740
7 changed files with 146 additions and 98 deletions
@@ -19,13 +19,6 @@ import ContainerizationOCI
import ContainerizationOS
import Foundation
#if os(macOS)
import ContainerizationArchive
import ContainerizationEXT4
import SystemPackage
import ContainerizationExtras
#endif
/// Type representing an OCI container image.
public struct Image: Sendable {
private let contentStore: ContentStore
@@ -135,83 +128,3 @@ public struct Image: Sendable {
return content
}
}
#if os(macOS)
extension Image {
/// Unpack the image into a filesystem.
public func unpack(for platform: Platform, at path: URL, blockSizeInBytes: UInt64 = 512.gib(), progress: ProgressHandler? = nil) async throws -> Mount {
let blockPath = try prepareUnpackPath(path: path)
let manifest = try await loadManifest(platform: platform)
return try await unpackContents(
path: blockPath,
manifest: manifest,
blockSizeInBytes: blockSizeInBytes,
progress: progress
)
}
private func loadManifest(platform: Platform) async throws -> Manifest {
let manifest = try await descriptor(for: platform)
guard let m: Manifest = try await self.contentStore.get(digest: manifest.digest) else {
throw ContainerizationError(.notFound, message: "content not found \(manifest.digest)")
}
return m
}
private func prepareUnpackPath(path: URL) throws -> String {
let blockPath = path.absolutePath()
guard !FileManager.default.fileExists(atPath: blockPath) else {
throw ContainerizationError(.exists, message: "block device already exists at \(blockPath)")
}
return blockPath
}
private func unpackContents(path: String, manifest: Manifest, blockSizeInBytes: UInt64, progress: ProgressHandler?) async throws -> Mount {
let filesystem = try EXT4.Formatter(FilePath(path), minDiskSize: blockSizeInBytes)
defer { try? filesystem.close() }
for layer in manifest.layers {
try Task.checkCancellation()
guard let content = try await self.contentStore.get(digest: layer.digest) else {
throw ContainerizationError(.notFound, message: "Content with digest \(layer.digest)")
}
switch layer.mediaType {
case MediaTypes.imageLayer, MediaTypes.dockerImageLayer:
try filesystem.unpack(
source: content.path,
format: .paxRestricted,
compression: .none,
progress: progress
)
case MediaTypes.imageLayerGzip, MediaTypes.dockerImageLayerGzip:
try filesystem.unpack(
source: content.path,
format: .paxRestricted,
compression: .gzip,
progress: progress
)
default:
throw ContainerizationError(.unsupported, message: "Media type \(layer.mediaType) not supported.")
}
}
return .block(
format: "ext4",
source: path,
destination: "/",
options: []
)
}
}
#else
extension Image {
public func unpack(for platform: Platform, at path: URL, blockSizeInBytes: UInt64 = 512.gib()) async throws -> Mount {
throw ContainerizationError(.unsupported, message: "Image unpack unsupported on current platform")
}
}
#endif
@@ -34,7 +34,8 @@ public struct InitImage: Sendable {
extension InitImage {
/// Unpack the initial filesystem for the desired platform at a given path.
public func initBlock(at: URL, for platform: SystemPlatform) async throws -> Mount {
var fs = try await image.unpack(for: platform.ociPlatform(), at: at, blockSizeInBytes: 512.mib())
let unpacker = EXT4Unpacker(blockSizeInBytes: 512.mib())
var fs = try await unpacker.unpack(self.image, for: platform.ociPlatform(), at: at)
fs.options = ["ro"]
return fs
}
@@ -0,0 +1,81 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
//
// 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 ContainerizationOCI
import Foundation
#if os(macOS)
import ContainerizationArchive
import ContainerizationEXT4
import SystemPackage
#endif
public struct EXT4Unpacker: Unpacker {
let blockSizeInBytes: UInt64
public init(blockSizeInBytes: UInt64) {
self.blockSizeInBytes = blockSizeInBytes
}
public func unpack(_ image: Image, for platform: Platform, at path: URL, progress: ProgressHandler? = nil) async throws -> Mount {
#if !os(macOS)
throw ContainerizationError(.unsupported, message: "Cannot unpack an image on current platform")
#else
let blockPath = try prepareUnpackPath(path: path)
let manifest = try await image.manifest(for: platform)
let filesystem = try EXT4.Formatter(FilePath(path), minDiskSize: blockSizeInBytes)
defer { try? filesystem.close() }
for layer in manifest.layers {
try Task.checkCancellation()
let content = try await image.getContent(digest: layer.digest)
let compression: ContainerizationArchive.Filter
switch layer.mediaType {
case MediaTypes.imageLayer, MediaTypes.dockerImageLayer:
compression = .none
case MediaTypes.imageLayerGzip, MediaTypes.dockerImageLayerGzip:
compression = .gzip
default:
throw ContainerizationError(.unsupported, message: "Media type \(layer.mediaType) not supported.")
}
try filesystem.unpack(
source: content.path,
format: .paxRestricted,
compression: compression,
progress: progress
)
}
return .block(
format: "ext4",
source: blockPath,
destination: "/",
options: []
)
#endif
}
private func prepareUnpackPath(path: URL) throws -> String {
let blockPath = path.absolutePath()
guard !FileManager.default.fileExists(atPath: blockPath) else {
throw ContainerizationError(.exists, message: "block device already exists at \(blockPath)")
}
return blockPath
}
}
@@ -0,0 +1,40 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the Containerization project authors. All rights reserved.
//
// 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 ContainerizationExtras
import ContainerizationOCI
import Foundation
/// The `Unpacker` protocol defines a standardized interface that involves
/// decompressing, extracting image layers and preparing it for use.
///
/// The `Unpacker` is responsible for managing the lifecycle of the
/// unpacking process, including any temporary files or resources, until the
/// `Mount` object is produced.
public protocol Unpacker {
/// Unpacks the provided image to a specified path for a given platform.
///
/// This asynchronous method should handle the entire unpacking process, from reading
/// the `Image` layers for the given `Platform` via its `Manifest`,
/// to making the extracted contents available as a `Mount`.
/// Implementations of this method may apply platform-specific optimizations
/// or transformations during the unpacking.
///
/// Progress updates can be observed via the optional `progress` handler.
func unpack(_ image: Image, for platform: Platform, at path: URL, progress: ProgressHandler?) async throws -> Mount
}