Files
wehub-resource-sync 680845cb1c
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
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

167 lines
7.7 KiB
Swift

//===----------------------------------------------------------------------===//
// 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 SystemPackage
import Testing
@testable import ContainerizationEXT4
/// Structural validation tests for journaled EXT4 filesystems.
///
/// Each test creates a minimal image with a specific journal mode, then reads the
/// superblock back via `EXT4.EXT4Reader` and asserts the invariants that the Linux
/// kernel checks before mounting:
///
/// - **Geometry**: `fileSize == blocksCount * blockSize`
/// (the exact invariant behind "bad geometry: block count N exceeds size of device")
/// - **Additive sizing**: `fileSize > minDiskSize`
/// (the journal grows the image rather than carving into the usable capacity)
/// - **Feature flag**: `featureCompat` has `hasJournal` set
/// - **Journal inode**: `journalInum == EXT4.JournalInode` (inode 8)
/// - **Mount options**: `defaultMountOpts` matches the requested mode
@Suite(.serialized)
struct JournalFormatTests {
let minDiskSize: UInt64 = 128.mib()
// MARK: - Per-mode tests
@Test func writeback() throws {
let path = tmpPath("ext4-journal-writeback")
defer { cleanup(path) }
let formatter = try EXT4.Formatter(path, minDiskSize: minDiskSize, journal: .init(defaultMode: .writeback))
try writeHello(formatter, content: "writeback")
try formatter.close()
// Writeback mode: only journal metadata is committed to disk before the data.
// defaultMountOpts encodes this as 0x0060 (data=writeback | barrier).
try verifyStructure(at: path, expectedMountOpts: EXT4.DefaultMountOpts.journalWriteback)
}
@Test func ordered() throws {
let path = tmpPath("ext4-journal-ordered")
defer { cleanup(path) }
let formatter = try EXT4.Formatter(path, minDiskSize: minDiskSize, journal: .init(defaultMode: .ordered))
try writeHello(formatter, content: "ordered")
try formatter.close()
// Ordered mode (the Linux default): data blocks are written before the journal commit.
// defaultMountOpts encodes this as 0x0040 (data=ordered | barrier).
try verifyStructure(at: path, expectedMountOpts: EXT4.DefaultMountOpts.journalOrdered)
}
@Test func journalData() throws {
let path = tmpPath("ext4-journal-data")
defer { cleanup(path) }
let formatter = try EXT4.Formatter(path, minDiskSize: minDiskSize, journal: .init(defaultMode: .journal))
try writeHello(formatter, content: "journal")
try formatter.close()
// Data journaling mode: data and metadata are both journaled.
// defaultMountOpts encodes this as 0x0020 (data=journal | barrier).
try verifyStructure(at: path, expectedMountOpts: EXT4.DefaultMountOpts.journalData)
}
// MARK: - Shared structural helper
private func verifyStructure(at path: FilePath, expectedMountOpts: UInt32) throws {
let fileSize = try fileByteCount(at: path)
let reader = try EXT4.EXT4Reader(blockDevice: path)
let sb = reader.superBlock
let blocksCount = UInt64(sb.blocksCountLow) | (UInt64(sb.blocksCountHigh) << 32)
let blockSize = UInt64(sb.blockSize)
// The Linux kernel rejects mounts where the superblock's block count implies
// a larger device than the actual file backing the filesystem.
#expect(fileSize == blocksCount * blockSize, "geometry mismatch: fileSize=\(fileSize), blocksCount=\(blocksCount), blockSize=\(blockSize)")
// The journal is additive overhead; the image must be larger than minDiskSize.
#expect(fileSize > minDiskSize, "journal did not grow the image: fileSize=\(fileSize), minDiskSize=\(minDiskSize)")
// The COMPAT_HAS_JOURNAL flag must be set for the kernel to look for inode 8.
#expect(sb.featureCompat & EXT4.CompatFeature.hasJournal.rawValue != 0, "COMPAT_HAS_JOURNAL not set in featureCompat (0x\(String(sb.featureCompat, radix: 16)))")
// The journal inode number recorded in the superblock must point at inode 8.
#expect(sb.journalInum == EXT4.JournalInode, "journalInum=\(sb.journalInum), expected \(EXT4.JournalInode)")
// The default mount options must reflect the requested journaling mode.
#expect(sb.defaultMountOpts == expectedMountOpts, "defaultMountOpts=0x\(String(sb.defaultMountOpts, radix: 16)), expected 0x\(String(expectedMountOpts, radix: 16))")
}
// MARK: - Filesystem population helpers
private func writeHello(_ formatter: EXT4.Formatter, content: String) throws {
try formatter.create(path: FilePath("/data"), mode: EXT4.Inode.Mode(.S_IFDIR, 0o755))
let stream = InputStream(data: Data(content.utf8))
stream.open()
defer { stream.close() }
try formatter.create(
path: FilePath("/data/hello.txt"),
mode: EXT4.Inode.Mode(.S_IFREG, 0o644),
buf: stream)
}
// MARK: - File helpers
private func tmpPath(_ name: String) -> FilePath {
FilePath(
FileManager.default.temporaryDirectory
.appendingPathComponent("\(name)-\(UUID().uuidString).img", isDirectory: false)
.path
)
}
private func cleanup(_ path: FilePath) {
try? FileManager.default.removeItem(atPath: path.string)
}
private func fileByteCount(at path: FilePath) throws -> UInt64 {
let attrs = try FileManager.default.attributesOfItem(atPath: path.string)
guard let size = attrs[.size] as? UInt64 else {
throw CocoaError(.fileReadUnknown)
}
return size
}
}
/// Tests that the UInt32 block-count overflow guards fire at the right points.
struct JournalOverflowTests {
// (UInt32.max + 1) blocks at the minimum block size triggers the guard in init()
// before any file is created, so no cleanup is needed.
@Test func initRejectsBlockCountOverflowingUInt32() {
let path = FilePath(
FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: false).path)
#expect(throws: EXT4.Formatter.Error.self) {
try EXT4.Formatter(path, blockSize: 1024, minDiskSize: (UInt64(UInt32.max) + 1) * 1024)
}
}
// minDiskSize is exactly at the UInt32 block count limit; adding any journal pushes
// newSize / blockSize above UInt32.max. The guard in close() fires before the bitmap
// loop, so this test completes quickly despite the large sparse file.
@Test func closeRejectsJournalExpansionOverflowingUInt32() throws {
let path = FilePath(
FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: false).path)
defer { try? FileManager.default.removeItem(atPath: path.string) }
let formatter = try EXT4.Formatter(
path, blockSize: 1024, minDiskSize: UInt64(UInt32.max) * 1024,
journal: .init(defaultMode: .writeback))
#expect(throws: EXT4.Formatter.Error.self) {
try formatter.close()
}
}
}