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
263 lines
11 KiB
Swift
263 lines
11 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.
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
// swiftlint: disable force_try shorthand_operator static_over_final_class
|
|
|
|
import Foundation
|
|
import SystemPackage
|
|
import Testing
|
|
|
|
@testable import ContainerizationEXT4
|
|
|
|
struct Ext4FormatTests: ~Copyable {
|
|
let fsPath = FilePath(
|
|
FileManager.default.uniqueTemporaryDirectory()
|
|
.appendingPathComponent("ext4.img.delme.format", isDirectory: false))
|
|
|
|
// This test creates a file named "ext4.img.delme"
|
|
// Since there are no tools yet in osx/swift to test the created filesystem,
|
|
// the tests below perform the same checks as the following manual commands
|
|
//
|
|
// From project root
|
|
// $> backpack run -it -v SwiftExt4/Tests/SwiftExt4Tests/:/test -w test ubuntu:latest
|
|
// $> ls -lrth ext4.img.delme # should be only 44K
|
|
// $> e2fsck ext4.img.delme # should return 0
|
|
// $> dumpe2fs ext4.img.delme # should print info and return 0
|
|
// $> debugfs ext4.img.delme # should open the fs
|
|
// debugfs 1.46.5 (30-Dec-2021)
|
|
// debugfs: ls
|
|
// 2 (12) . 2 (12) .. 15 (12) x 11 (20) lost+found 12 (12) ase
|
|
// 16 (12) y 0 (4016)
|
|
//
|
|
// # check directory
|
|
//
|
|
// debugfs: stat /test
|
|
// Inode: 12 Type: directory Mode: 01274 Flags: 0xc0000
|
|
// Generation: 0 Version: 0x00000000:00000000
|
|
// User: 0 Group: 0 Size: 4096
|
|
// File ACL: 0
|
|
// Links: 3 Blockcount: 1
|
|
// Fragment: Address: 0 Number: 0 Size: 0
|
|
// ctime: 0x6614b59f:8cdf6a34 -- Tue Apr 9 03:27:27 2024
|
|
// atime: 0x6614b59f:8cdf6a34 -- Tue Apr 9 03:27:27 2024
|
|
// mtime: 0x6614b59f:8cdf6a34 -- Tue Apr 9 03:27:27 2024
|
|
// crtime: 0x6614b59f:8cdf6a34 -- Tue Apr 9 03:27:27 2024
|
|
// Size of extra inode fields: 24
|
|
// EXTENTS:
|
|
// (0):5
|
|
//
|
|
// # check regular file
|
|
//
|
|
// debugfs: stat /test/foo/bar/x
|
|
// Inode: 15 Type: regular Mode: 01363 Flags: 0xc0000
|
|
// Generation: 0 Version: 0x00000000:00000000
|
|
// User: 0 Group: 0 Size: 4
|
|
// File ACL: 0
|
|
// Links: 2 Blockcount: 1
|
|
// Fragment: Address: 0 Number: 0 Size: 0
|
|
// ctime: 0x6614b59f:8ce91ef8 -- Tue Apr 9 03:27:27 2024
|
|
// atime: 0x6614b59f:8ce91ef8 -- Tue Apr 9 03:27:27 2024
|
|
// mtime: 0x6614b59f:8ce91ef8 -- Tue Apr 9 03:27:27 2024
|
|
// crtime: 0x6614b59f:8ce91ef8 -- Tue Apr 9 03:27:27 2024
|
|
// Size of extra inode fields: 24
|
|
// EXTENTS:
|
|
// (0):2
|
|
//
|
|
// # check symlink
|
|
//
|
|
// debugfs: stat /y
|
|
// Inode: 16 Type: symlink Mode: 01675 Flags: 0x0
|
|
// Generation: 0 Version: 0x00000000:00000000
|
|
// User: 0 Group: 0 Size: 19
|
|
// File ACL: 0
|
|
// Links: 1 Blockcount: 0
|
|
// Fragment: Address: 0 Number: 0 Size: 0
|
|
// ctime: 0x6614b59f:8cf052fc -- Tue Apr 9 03:27:27 2024
|
|
// atime: 0x6614b59f:8cf052fc -- Tue Apr 9 03:27:27 2024
|
|
// mtime: 0x6614b59f:8cf052fc -- Tue Apr 9 03:27:27 2024
|
|
// crtime: 0x6614b59f:8cf052fc -- Tue Apr 9 03:27:27 2024
|
|
// Size of extra inode fields: 24
|
|
// Fast link dest: "test/foo"
|
|
//
|
|
// # check hard link
|
|
//
|
|
// debugfs: stat x
|
|
// Inode: 15 Type: regular Mode: 01363 Flags: 0xc0000
|
|
// Generation: 0 Version: 0x00000000:00000000
|
|
// User: 0 Group: 0 Size: 4
|
|
// File ACL: 0
|
|
// Links: 2 Blockcount: 1
|
|
// Fragment: Address: 0 Number: 0 Size: 0
|
|
// ctime: 0x6614b59f:8ce91ef8 -- Tue Apr 9 03:27:27 2024
|
|
// atime: 0x6614b59f:8ce91ef8 -- Tue Apr 9 03:27:27 2024
|
|
// mtime: 0x6614b59f:8ce91ef8 -- Tue Apr 9 03:27:27 2024
|
|
// crtime: 0x6614b59f:8ce91ef8 -- Tue Apr 9 03:27:27 2024
|
|
// Size of extra inode fields: 24
|
|
// EXTENTS:
|
|
// (0):2
|
|
//
|
|
// Mount and check
|
|
//
|
|
// $> mkdir -p mntpnt
|
|
// $> mount -t ext4 ext4.img.delme mntpnt
|
|
// $> # explore file tree
|
|
init() throws {
|
|
let formatter = try EXT4.Formatter(fsPath, minDiskSize: 32.kib())
|
|
try formatter.create(path: FilePath("/test"), mode: EXT4.Inode.Mode(.S_IFDIR, 0o700))
|
|
try formatter.create(path: FilePath("/test/foo"), mode: EXT4.Inode.Mode(.S_IFDIR, 0o700))
|
|
try formatter.create(path: FilePath("/test/foo/bar"), mode: EXT4.Inode.Mode(.S_IFDIR, 0o700))
|
|
let inputStream = InputStream(data: "test".data(using: .utf8)!)
|
|
inputStream.open()
|
|
try formatter.create(
|
|
path: FilePath("/test/foo/bar/x"), mode: EXT4.Inode.Mode(.S_IFREG, 0o755),
|
|
buf: inputStream) // create a regular file
|
|
inputStream.close()
|
|
try formatter.link(link: FilePath("/x"), target: FilePath("/test/foo/bar/x"))
|
|
try formatter.create(
|
|
path: FilePath("/y"), link: FilePath("test/foo"), mode: EXT4.Inode.Mode(.S_IFLNK, 0o700)) // create a symlink
|
|
|
|
try formatter.close()
|
|
}
|
|
|
|
deinit {
|
|
try? FileManager.default.removeItem(at: fsPath.url)
|
|
}
|
|
|
|
/// This test checks that the size of the FS at fsPath is the minimum possible
|
|
/// for its data + metadata. It should be 44 kib or 11 blocks, expanded to accommodate
|
|
/// data requiring > 32KiB of space
|
|
@Test func fileSize() throws {
|
|
let f = try FileHandle(forReadingFrom: fsPath.url)
|
|
let size = try f.seekToEnd()
|
|
#expect(size == 128.mib())
|
|
}
|
|
|
|
/// This test checks that the superblock was created correctly
|
|
@Test func superblock() throws {
|
|
let f = try EXT4.EXT4Reader(blockDevice: fsPath)
|
|
#expect(f.superBlock.blocksCountLow == 32768)
|
|
#expect(f.superBlock.freeBlocksCountLow == 32246) // total - 512 inode blocks
|
|
}
|
|
|
|
/// This test checks that the group descriptor has been set correctly
|
|
@Test func groupDescriptors() throws {
|
|
let f = try EXT4.EXT4Reader(blockDevice: fsPath)
|
|
let gd = try f.getGroupDescriptor(0)
|
|
#expect(gd.blockBitmapLow == 551) // move over by 512 blocks (for inodes)
|
|
#expect(gd.inodeBitmapLow == 552) // move over by 512 blocks (for inodes)
|
|
#expect(gd.inodeTableLow == 39)
|
|
#expect(gd.freeBlocksCountLow == 32246) // 512 block used by larger inode table per block group
|
|
#expect(gd.freeInodesCountLow == 8176) // 512 times the inodes
|
|
#expect(gd.usedDirsCountLow == 5)
|
|
}
|
|
|
|
/// This test checks that the block bitmap has been set correctly
|
|
@Test func blockBitmap() throws {
|
|
let ext4 = try EXT4.EXT4Reader(blockDevice: fsPath)
|
|
let gd = try ext4.getGroupDescriptor(1)
|
|
let blockBitmapOffset = gd.blockBitmapLow
|
|
let f = try #require(FileHandle(forReadingFrom: fsPath))
|
|
try f.seek(toOffset: ext4.blockSize * blockBitmapOffset)
|
|
let bitmapSize = ext4.superBlock.blocksPerGroup / 8
|
|
#expect(bitmapSize == 4096)
|
|
let _ = try f.read(
|
|
upToCount: Int(ext4.superBlock.blocksCountLow - ext4.superBlock.freeBlocksCountLow - 1) / 8 + 1)
|
|
}
|
|
|
|
/// This test checks that the inode bitmap has been set correctly
|
|
@Test func inodeBitmap() throws {
|
|
let ext4 = try EXT4.EXT4Reader(blockDevice: fsPath)
|
|
let gd = try ext4.getGroupDescriptor(1)
|
|
let inodeBitmapOffset = gd.inodeBitmapLow
|
|
let f = try #require(FileHandle(forReadingFrom: fsPath))
|
|
try f.seek(toOffset: ext4.blockSize * inodeBitmapOffset)
|
|
let bitmapSize = ext4.superBlock.inodesPerGroup / 8
|
|
#expect(bitmapSize == 1024)
|
|
}
|
|
|
|
/// This test checks that the inode table has been set correctly
|
|
@Test func inodeTable() throws {
|
|
let ext4 = try EXT4.EXT4Reader(blockDevice: fsPath)
|
|
let gd = try ext4.getGroupDescriptor(0)
|
|
let inodeTableOffset = gd.inodeTableLow
|
|
let f = try #require(FileHandle(forReadingFrom: fsPath))
|
|
try f.seek(toOffset: ext4.blockSize * inodeTableOffset)
|
|
let inodeTableSize = ext4.superBlock.inodesPerGroup * UInt32(ext4.superBlock.inodeSize)
|
|
#expect(inodeTableSize == 2_097_152)
|
|
let inodeTableData = try #require(try f.read(upToCount: Int(inodeTableSize)))
|
|
let inodeAt: (Int) -> EXT4.Inode = { inodeNum in
|
|
var inodeBytes: [UInt8] = .init(repeating: 0, count: Int(ext4.superBlock.inodeSize))
|
|
let inodeStart = Int(ext4.superBlock.inodeSize) * (inodeNum - 1)
|
|
var j: Int = 0
|
|
for i in inodeStart..<inodeStart + Int(ext4.superBlock.inodeSize) {
|
|
inodeBytes[j] = inodeTableData[i]
|
|
j = j + 1
|
|
}
|
|
return inodeBytes.withUnsafeBytes { ptr in
|
|
ptr.loadLittleEndian(as: EXT4.Inode.self)
|
|
}
|
|
}
|
|
let root = inodeAt(2)
|
|
#expect(root.mode.isDir())
|
|
#expect(root.linksCount == 4)
|
|
|
|
let regFile = inodeAt(15)
|
|
#expect(regFile.mode.isReg())
|
|
#expect(regFile.sizeLow == 4)
|
|
}
|
|
}
|
|
|
|
@Suite(.serialized)
|
|
struct NegativeTimestampRoundtripTests {
|
|
private let fsPath = FilePath(
|
|
FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("ext4-pre1970-roundtrip.img", isDirectory: false))
|
|
private let apollo11MoonLanding: Date = {
|
|
let f = ISO8601DateFormatter()
|
|
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
return f.date(from: "1969-07-20T20:17:39.9Z")!
|
|
}()
|
|
|
|
@Test func encodeNegativeTimestamp() throws {
|
|
let formatter = try EXT4.Formatter(fsPath, minDiskSize: 32.kib())
|
|
defer { try? formatter.close() }
|
|
let ts = FileTimestamps(access: apollo11MoonLanding, modification: apollo11MoonLanding, creation: apollo11MoonLanding)
|
|
try formatter.create(path: FilePath("/file"), mode: EXT4.Inode.Mode(.S_IFREG, 0o755), ts: ts, buf: nil)
|
|
}
|
|
|
|
@Test func decodeNegativeTimestamp() throws {
|
|
let reader = try EXT4.EXT4Reader(blockDevice: fsPath)
|
|
let (_, inode) = try reader.stat(FilePath("/file"))
|
|
let mtime = Date(fsTimestamp: UInt64(inode.mtime) | (UInt64(inode.mtimeExtra) << 32))
|
|
let atime = Date(fsTimestamp: UInt64(inode.atime) | (UInt64(inode.atimeExtra) << 32))
|
|
let crtime = Date(fsTimestamp: UInt64(inode.crtime) | (UInt64(inode.crtimeExtra) << 32))
|
|
#expect(mtime == apollo11MoonLanding)
|
|
#expect(atime == apollo11MoonLanding)
|
|
#expect(crtime == apollo11MoonLanding)
|
|
}
|
|
}
|
|
|
|
struct Ext4FormatEmptyRangeTests {
|
|
@Test func closeEmptyFilesystem() throws {
|
|
let fsPath = FilePath(
|
|
FileManager.default.temporaryDirectory
|
|
.appendingPathComponent(UUID().uuidString, isDirectory: false))
|
|
defer { try? FileManager.default.removeItem(at: fsPath.url) }
|
|
let formatter = try EXT4.Formatter(fsPath, minDiskSize: 32.kib())
|
|
try formatter.close()
|
|
}
|
|
}
|