Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 30 additions & 30 deletions Sources/ContainerizationEXT4/EXT4+Reader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,61 +192,61 @@ extension EXT4 {
func getExtents(inode: InodeNumber) throws -> [(start: UInt32, end: UInt32)]? {
let inode = try self.getInode(number: inode)
let inodeBlock = Data(tupleToArray(inode.block))
var offset = 0
var extents: [(start: UInt32, end: UInt32)] = []

let extentHeaderSize = MemoryLayout<ExtentHeader>.size
let extentIndexSize = MemoryLayout<ExtentIndex>.size
let extentLeafSize = MemoryLayout<ExtentLeaf>.size
// read extent header
let header = inodeBlock.subdata(in: offset..<offset + extentHeaderSize).withUnsafeBytes {
let header = inodeBlock.subdata(in: 0..<extentHeaderSize).withUnsafeBytes {
$0.loadLittleEndian(as: ExtentHeader.self)
}
guard header.magic == EXT4.ExtentHeaderMagic else {
return []
}
offset += extentHeaderSize // Jump to entries
switch header.depth {
case 0:
// When depth is 0 the extent header is followed by extent leaves
var extents: [(start: UInt32, end: UInt32)] = []
try readExtentNode(data: inodeBlock, header: header, into: &extents)
return extents
}

/// Recursively reads extent tree nodes. For leaf nodes (depth 0), appends
/// extent mappings directly. For index nodes (depth > 0), follows each
/// index entry to a child block and recurses.
private func readExtentNode(
data: Data,
header: ExtentHeader,
into extents: inout [(start: UInt32, end: UInt32)]
) throws {
let extentHeaderSize = MemoryLayout<ExtentHeader>.size
let extentIndexSize = MemoryLayout<ExtentIndex>.size
let extentLeafSize = MemoryLayout<ExtentLeaf>.size
var offset = extentHeaderSize

if header.depth == 0 {
// Leaf node: entries are ExtentLeaf mappings
for _ in 0..<header.entries {
let leaf = inodeBlock.subdata(in: offset..<offset + extentLeafSize).withUnsafeBytes {
$0.load(as: ExtentLeaf.self)
let leaf = data.subdata(in: offset..<offset + extentLeafSize).withUnsafeBytes {
$0.loadLittleEndian(as: ExtentLeaf.self)
}
extents.append((leaf.startLow, leaf.startLow + UInt32(leaf.length)))
offset += extentLeafSize
}
case 1:
// When depth is 1 the extent header is followed by extent indices which point to leaves
} else {
// Index node: entries are ExtentIndex pointers to child blocks
for _ in 0..<header.entries {
let indexNode = inodeBlock.subdata(in: offset..<offset + extentIndexSize).withUnsafeBytes {
$0.load(as: ExtentIndex.self)
let indexNode = data.subdata(in: offset..<offset + extentIndexSize).withUnsafeBytes {
$0.loadLittleEndian(as: ExtentIndex.self)
}
try self.seek(block: indexNode.leafLow)
guard let block = try self.handle.read(upToCount: Int(self.blockSize)) else {
throw EXT4.Error.couldNotReadBlock(indexNode.leafLow)
}
var blockOffset = 0
let leafHeader = block.subdata(in: blockOffset..<extentHeaderSize).withUnsafeBytes {
let childHeader = block.subdata(in: 0..<extentHeaderSize).withUnsafeBytes {
$0.loadLittleEndian(as: ExtentHeader.self)
}
guard leafHeader.magic == EXT4.ExtentHeaderMagic else {
guard childHeader.magic == EXT4.ExtentHeaderMagic else {
throw Error.invalidExtents
}
blockOffset += extentHeaderSize
for _ in 0..<leafHeader.entries {
let leaf = block.subdata(in: blockOffset..<blockOffset + extentLeafSize).withUnsafeBytes {
$0.loadLittleEndian(as: ExtentLeaf.self)
}
extents.append((leaf.startLow, leaf.startLow + UInt32(leaf.length)))
blockOffset += extentLeafSize
}
try readExtentNode(data: block, header: childHeader, into: &extents)
offset += extentIndexSize
}
default:
throw Error.deepExtentsUnimplemented
}
return extents
}

// MARK: Internal functions
Expand Down
32 changes: 32 additions & 0 deletions Tests/ContainerizationEXT4Tests/TestEXT4Reader+IO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,38 @@ struct EXT4PathIOTests {
#expect(String(decoding: partialData, as: UTF8.self) == expectedContent)
}

@Test
func multiExtentFileReadback() throws {
// Create an image large enough that the formatter produces multiple extents
// by interleaving file creation (which can cause non-contiguous allocation).
// This exercises the recursive readExtentNode() path for depth 0 and depth 1.
let url = try buildFS(minDiskSize: 32 * 1024 * 1024) { fmt in
// Create many files to consume blocks, then a large file that may
// span multiple extents due to intervening allocations.
for i in 0..<50 {
let content = String(repeating: Character(UnicodeScalar(65 + (i % 26))!), count: 8192)
try self.createFile(fmt, "/filler_\(i).txt", content)
}
// Create a large file — the formatter writes this contiguously, but the
// reader still exercises getExtents() → readExtentNode() with depth 0.
let bigContent = String(repeating: "X", count: 512 * 1024)
try self.createFile(fmt, "/big.bin", bigContent)
}
defer { try? FileManager.default.removeItem(at: url) }

let r = try openReader(url)

// Verify the large file reads back correctly
let data = try r.readFile(at: FilePath("/big.bin"))
#expect(data.count == 512 * 1024)
#expect(data.allSatisfy { $0 == UInt8(ascii: "X") })

// Verify filler files also read correctly
let filler0 = try r.readFile(at: FilePath("/filler_0.txt"))
#expect(filler0.count == 8192)
#expect(filler0.allSatisfy { $0 == UInt8(ascii: "A") })
}

@Test
func largeFileReadAcrossBlocks() throws {
// Keep this modest to avoid slow CI while still crossing multiple blocks.
Expand Down