Files
container/Tests/ContainerizationEXT4Tests/TestFormatterUnpack.swift
T
2025-06-12 09:57:35 -04:00

183 lines
8.0 KiB
Swift

//===----------------------------------------------------------------------===//
// 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.
//===----------------------------------------------------------------------===//
// swiftlint:disable force_try static_over_final_class
#if os(macOS)
import ContainerizationArchive
import Foundation
import Testing
import SystemPackage
@testable import ContainerizationEXT4
struct Tar2EXT4Test: ~Copyable {
let fsPath = FilePath(
FileManager.default.uniqueTemporaryDirectory()
.appendingPathComponent("ext4.tar.img.delme", isDirectory: false))
let xattrs: [String: Data] = [
"foo.bar": Data([1, 2, 3]),
"bar": Data([0, 0, 0]),
"system.richacl.bar": Data([99, 1, 9, 1]),
"foobar.user": Data([71, 2, 45]),
"test.xattr.cap": Data([1, 32, 3]),
"testing123": Data([12, 24, 45]),
"sys.admin": Data([16, 23, 13]),
"test.123": Data([15, 26, 54]),
"extendedattribute.test": Data([15, 26, 54, 1, 2, 4, 6, 7, 7]),
]
init() throws {
// create layer1
let layer1Path = FileManager.default.uniqueTemporaryDirectory()
.appendingPathComponent("layer1.tar.gz", isDirectory: false)
let layer1Archiver = try ArchiveWriter(
configuration: ArchiveWriterConfiguration(format: .paxRestricted, filter: .gzip))
try layer1Archiver.open(file: layer1Path)
// create 2 directories and fill them with files
try layer1Archiver.writeEntry(entry: WriteEntry.dir(path: "/dir1", permissions: 0o755), data: nil)
try layer1Archiver.writeEntry(entry: WriteEntry.file(path: "/dir1/file1", permissions: 0o644), data: nil)
try layer1Archiver.writeEntry(entry: WriteEntry.dir(path: "/dir2", permissions: 0o755), data: nil)
try layer1Archiver.writeEntry(entry: WriteEntry.file(path: "/dir2/file1", permissions: 0o644), data: nil)
try layer1Archiver.finishEncoding()
// create layer2
let layer2Path = FileManager.default.uniqueTemporaryDirectory()
.appendingPathComponent("layer2.tar.gz", isDirectory: false)
let layer2Archiver = try ArchiveWriter(
configuration: ArchiveWriterConfiguration(format: .paxRestricted, filter: .gzip))
try layer2Archiver.open(file: layer2Path)
// create 3 directories and fill them with files and whiteouts
try layer2Archiver.writeEntry(entry: WriteEntry.dir(path: "/dir1", permissions: 0o755), data: nil)
try layer2Archiver.writeEntry(
entry: WriteEntry.file(path: "/dir1/.wh.file1", permissions: 0o644), data: nil)
try layer2Archiver.writeEntry(entry: WriteEntry.dir(path: "/dir2", permissions: 0o755), data: nil)
try layer2Archiver.writeEntry(
entry: WriteEntry.file(path: "/dir2/.wh..wh..opq", permissions: 0o644), data: nil)
try layer2Archiver.writeEntry(entry: WriteEntry.dir(path: "/dir3", permissions: 0o755), data: nil)
try layer2Archiver.writeEntry(
entry: WriteEntry.file(path: "/dir3/file1", permissions: 0o644, xattrs: xattrs), data: nil)
try layer2Archiver.writeEntry(entry: WriteEntry.dir(path: "/dir4", permissions: 0o755), data: nil)
try layer2Archiver.writeEntry(
entry: WriteEntry.file(path: "/dir4/special_ĩ", permissions: 0o644), data: nil)
try layer2Archiver.writeEntry(
entry: WriteEntry.link(path: "/dir4/specialcharacters", permissions: 0o644, target: "special_ĩ"),
data: nil)
// a new layer overwriting over an existing layer
try layer2Archiver.writeEntry(entry: WriteEntry.file(path: "/dir2/file1", permissions: 0o644), data: nil)
try layer2Archiver.finishEncoding()
let unpacker = try EXT4.Formatter(fsPath)
try unpacker.unpack(source: layer1Path)
try unpacker.unpack(source: layer2Path)
try unpacker.close()
}
deinit {
try? FileManager.default.removeItem(at: fsPath.url)
}
@Test func testUnpackBasic() throws {
let ext4 = try EXT4.EXT4Reader(blockDevice: fsPath)
// just a directory
let dir1Inode = try ext4.getInode(number: 12)
#expect(dir1Inode.mode.isDir())
// white out file /dir1/file1
let dir1File1Inode = try ext4.getInode(number: 13)
#expect(dir1File1Inode.dtime != 0)
#expect(dir1File1Inode.linksCount == 0) // deleted
// white out dir /dir2
let dir2Inode = try ext4.getInode(number: 14)
#expect(dir2Inode.dtime == 0)
#expect(dir2Inode.linksCount == 2) // children deleted
// new dir /dir3
let dir3Inode = try ext4.getInode(number: 16)
#expect(dir3Inode.mode.isDir())
#expect(dir3Inode.linksCount == 2)
// new file /dir3/file1
let dir3File1Inode = try ext4.getInode(number: 17)
#expect(dir3File1Inode.mode.isReg())
#expect(dir3File1Inode.linksCount == 1)
#expect(try ext4.getXattrsForInode(inode: dir3File1Inode) == xattrs)
// overwritten dir /dir2
let dir2OverwriteInode = try ext4.getInode(number: 18)
#expect(dir2OverwriteInode.mode.isDir())
#expect(dir2OverwriteInode.linksCount == 2)
// /dir4/special_ĩ
let dir2File1OverwriteInode = try ext4.getInode(number: 19)
#expect(dir2File1OverwriteInode.mode.isReg())
#expect(dir2File1OverwriteInode.linksCount == 1)
let specialFileInode = try ext4.getInode(number: 20)
let bytes = Data(Mirror(reflecting: specialFileInode.block).children.compactMap { $0.value as? UInt8 })
let specialFileTarget = try #require(FilePath(bytes), "Could not parse special file path")
#expect(specialFileTarget.description.hasPrefix("special_ĩ"))
}
}
extension ContainerizationArchive.WriteEntry {
static func dir(path: String, permissions: UInt16) -> WriteEntry {
let entry = WriteEntry()
entry.path = path
entry.fileType = .directory
entry.permissions = permissions
return entry
}
static func file(path: String, permissions: UInt16, size: Int64? = nil, xattrs: [String: Data]? = nil) -> WriteEntry {
let entry = WriteEntry()
entry.path = path
entry.fileType = .regular
entry.permissions = permissions
entry.size = size
if let xattrs {
entry.xattrs = xattrs
}
return entry
}
static func link(path: String, permissions: UInt16, target: String) -> WriteEntry {
let entry = WriteEntry()
entry.path = path
entry.fileType = .symbolicLink
entry.symlinkTarget = target
return entry
}
}
extension EXT4.EXT4Reader {
fileprivate func getXattrsForInode(inode: EXT4.Inode) throws -> [String: Data] {
var attributes: [EXT4.ExtendedAttribute] = []
let buffer: [UInt8] = EXT4.tupleToArray(inode.inlineXattrs)
try attributes.append(contentsOf: Self.readInlineExtendedAttributes(from: buffer))
let block = inode.xattrBlockLow
try self.seek(block: block)
let buf = try self.handle.read(upToCount: Int(self.blockSize))!
try attributes.append(contentsOf: Self.readBlockExtendedAttributes(from: [UInt8](buf)))
var xattrs: [String: Data] = [:]
for attribute in attributes {
guard attribute.fullName != "system.data" else {
continue
}
xattrs[attribute.fullName] = Data(attribute.value)
}
return xattrs
}
}
#endif