Skip to content

Commit b60c975

Browse files
committed
Add deep extent tree support (depth >= 2) in EXT4 reader and formatter
1 parent 5691645 commit b60c975

3 files changed

Lines changed: 228 additions & 86 deletions

File tree

Sources/ContainerizationEXT4/EXT4+Formatter.swift

Lines changed: 166 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,130 @@ extension EXT4 {
10391039
}
10401040
}
10411041

1042+
/// Recursively writes extent tree nodes to disk and returns the
1043+
/// ExtentIndex entries that the parent should store.
1044+
///
1045+
/// At depth 1, this writes leaf blocks (containing ExtentLeaf entries).
1046+
/// At depth > 1, this recurses to build child subtrees first, then
1047+
/// writes index blocks pointing to those children.
1048+
private func writeExtentSubtree(
1049+
depth: UInt16,
1050+
numExtents: UInt32,
1051+
numBlocks: UInt32,
1052+
start: UInt32,
1053+
entriesPerBlock: UInt32,
1054+
extentBlockCount: inout UInt32
1055+
) throws -> [ExtentIndex] {
1056+
var indices: [ExtentIndex] = []
1057+
let childDepth = depth - 1
1058+
1059+
// How many child blocks do we need at this level?
1060+
// Each child block can ultimately cover entriesPerBlock^childDepth leaf extents.
1061+
var leafCapacityPerChild: UInt32 = entriesPerBlock
1062+
for _ in 1..<depth {
1063+
leafCapacityPerChild *= entriesPerBlock
1064+
}
1065+
let numChildren = (numExtents + leafCapacityPerChild - 1) / leafCapacityPerChild
1066+
var extentsWritten: UInt32 = 0
1067+
1068+
for _ in 0..<numChildren {
1069+
let extentsForChild = min(numExtents - extentsWritten, leafCapacityPerChild)
1070+
let logicalOffset = extentsWritten * EXT4.MaxBlocksPerExtent
1071+
1072+
if childDepth == 0 {
1073+
// Write a leaf block containing ExtentLeaf entries.
1074+
if self.pos % self.blockSize != 0 {
1075+
try self.seek(block: self.currentBlock + 1)
1076+
}
1077+
let leafBlockAddr = self.currentBlock
1078+
extentBlockCount += 1
1079+
1080+
let leafHeader = ExtentHeader(
1081+
magic: EXT4.ExtentHeaderMagic,
1082+
entries: UInt16(extentsForChild),
1083+
max: UInt16(entriesPerBlock),
1084+
depth: 0,
1085+
generation: 0
1086+
)
1087+
var leafNode = ExtentLeafNode(header: leafHeader, leaves: [])
1088+
fillExtents(
1089+
node: &leafNode, numExtents: extentsForChild, numBlocks: numBlocks,
1090+
start: start,
1091+
offset: logicalOffset)
1092+
try withUnsafeLittleEndianBytes(of: leafNode.header) { bytes in
1093+
try self.handle.write(contentsOf: bytes)
1094+
}
1095+
for leaf in leafNode.leaves {
1096+
try withUnsafeLittleEndianBytes(of: leaf) { bytes in
1097+
try self.handle.write(contentsOf: bytes)
1098+
}
1099+
}
1100+
let checksum = leafNode.leaves.last?.block ?? 0
1101+
let extentTail = ExtentTail(checksum: checksum)
1102+
try withUnsafeLittleEndianBytes(of: extentTail) { bytes in
1103+
try self.handle.write(contentsOf: bytes)
1104+
}
1105+
1106+
indices.append(
1107+
ExtentIndex(
1108+
block: logicalOffset,
1109+
leafLow: leafBlockAddr,
1110+
leafHigh: 0,
1111+
unused: 0
1112+
))
1113+
} else {
1114+
// Recurse to build the child subtree, then write an index
1115+
// block pointing to the returned child indices.
1116+
let childIndices = try writeExtentSubtree(
1117+
depth: childDepth,
1118+
numExtents: extentsForChild,
1119+
numBlocks: numBlocks,
1120+
start: start,
1121+
entriesPerBlock: entriesPerBlock,
1122+
extentBlockCount: &extentBlockCount
1123+
)
1124+
1125+
// Write the index block for this subtree.
1126+
if self.pos % self.blockSize != 0 {
1127+
try self.seek(block: self.currentBlock + 1)
1128+
}
1129+
let indexBlockAddr = self.currentBlock
1130+
extentBlockCount += 1
1131+
1132+
let indexHeader = ExtentHeader(
1133+
magic: EXT4.ExtentHeaderMagic,
1134+
entries: UInt16(childIndices.count),
1135+
max: UInt16(entriesPerBlock),
1136+
depth: childDepth,
1137+
generation: 0
1138+
)
1139+
try withUnsafeLittleEndianBytes(of: indexHeader) { bytes in
1140+
try self.handle.write(contentsOf: bytes)
1141+
}
1142+
for childIdx in childIndices {
1143+
try withUnsafeLittleEndianBytes(of: childIdx) { bytes in
1144+
try self.handle.write(contentsOf: bytes)
1145+
}
1146+
}
1147+
let checksum = childIndices.last?.block ?? 0
1148+
let extentTail = ExtentTail(checksum: checksum)
1149+
try withUnsafeLittleEndianBytes(of: extentTail) { bytes in
1150+
try self.handle.write(contentsOf: bytes)
1151+
}
1152+
1153+
indices.append(
1154+
ExtentIndex(
1155+
block: logicalOffset,
1156+
leafLow: indexBlockAddr,
1157+
leafHigh: 0,
1158+
unused: 0
1159+
))
1160+
}
1161+
extentsWritten += extentsForChild
1162+
}
1163+
return indices
1164+
}
1165+
10421166
private func writeExtents(_ inode: Inode, _ blocks: (start: UInt32, end: UInt32)) throws -> Inode {
10431167
var inode = inode
10441168
// rest of code assumes that extents MUST go into a new block
@@ -1048,14 +1172,31 @@ extension EXT4 {
10481172
let dataBlocks = blocks.end - blocks.start
10491173
let numExtents = (dataBlocks + EXT4.MaxBlocksPerExtent - 1) / EXT4.MaxBlocksPerExtent
10501174
var usedBlocks = dataBlocks
1051-
let extentNodeSize = 12
1052-
let extentsPerBlock = self.blockSize / extentNodeSize - 1
1175+
let extentNodeSize: UInt32 = 12
1176+
let entriesPerBlock = self.blockSize / extentNodeSize - 1
10531177
var blockData: [UInt8] = .init(repeating: 0, count: 60)
10541178
var blockIndex: Int = 0
1055-
switch numExtents {
1056-
case 0:
1179+
1180+
guard numExtents > 0 else {
10571181
return inode // noop
1058-
case 1..<5:
1182+
}
1183+
1184+
// Determine the required tree depth.
1185+
// Depth 0: up to 4 extents fit inline in the inode.
1186+
// Depth N (N >= 1): each level multiplies capacity by entriesPerBlock,
1187+
// with up to 4 entries at the root.
1188+
var depth: UInt16 = 0
1189+
var capacity: UInt32 = 4
1190+
while capacity < numExtents {
1191+
depth += 1
1192+
capacity = 4
1193+
for _ in 0..<depth {
1194+
capacity *= entriesPerBlock
1195+
}
1196+
}
1197+
1198+
if depth == 0 {
1199+
// All extents fit inline in the inode's 60-byte block field.
10591200
let extentHeader = ExtentHeader(
10601201
magic: EXT4.ExtentHeaderMagic,
10611202
entries: UInt16(numExtents),
@@ -1079,73 +1220,42 @@ extension EXT4 {
10791220
}
10801221
}
10811222
}
1082-
case 5..<4 * UInt32(extentsPerBlock) + 1:
1083-
let extentBlocks = (numExtents + extentsPerBlock - 1) / extentsPerBlock
1084-
usedBlocks += extentBlocks
1223+
} else {
1224+
// Build the extent tree bottom-up. writeExtentSubtree writes
1225+
// child blocks to disk and returns the index entries for the
1226+
// parent level.
1227+
var extentBlockCount: UInt32 = 0
1228+
let rootIndices = try writeExtentSubtree(
1229+
depth: depth,
1230+
numExtents: numExtents,
1231+
numBlocks: dataBlocks,
1232+
start: blocks.start,
1233+
entriesPerBlock: entriesPerBlock,
1234+
extentBlockCount: &extentBlockCount
1235+
)
1236+
usedBlocks += extentBlockCount
1237+
10851238
let extentHeader = ExtentHeader(
10861239
magic: EXT4.ExtentHeaderMagic,
1087-
entries: UInt16(extentBlocks),
1240+
entries: UInt16(rootIndices.count),
10881241
max: 4,
1089-
depth: 1,
1242+
depth: depth,
10901243
generation: 0
10911244
)
1092-
var root = ExtentIndexNode(header: extentHeader, indices: [])
1093-
for i in 0..<extentBlocks {
1094-
if self.pos % self.blockSize != 0 {
1095-
try self.seek(block: self.currentBlock + 1)
1096-
}
1097-
let extentIdx = ExtentIndex(
1098-
block: i * extentsPerBlock * EXT4.MaxBlocksPerExtent,
1099-
leafLow: self.currentBlock,
1100-
leafHigh: 0,
1101-
unused: 0)
1102-
var extentsInBlock = numExtents - i * extentsPerBlock
1103-
if extentsInBlock > extentsPerBlock {
1104-
extentsInBlock = extentsPerBlock
1105-
}
1106-
let leafHeader = ExtentHeader(
1107-
magic: EXT4.ExtentHeaderMagic,
1108-
entries: UInt16(extentsInBlock),
1109-
max: UInt16(extentsPerBlock),
1110-
depth: 0,
1111-
generation: 0
1112-
)
1113-
var leafNode = ExtentLeafNode(header: leafHeader, leaves: [])
1114-
let offset = i * extentsPerBlock * EXT4.MaxBlocksPerExtent
1115-
fillExtents(
1116-
node: &leafNode, numExtents: extentsInBlock, numBlocks: dataBlocks,
1117-
start: blocks.start,
1118-
offset: offset)
1119-
try withUnsafeLittleEndianBytes(of: leafNode.header) { bytes in
1120-
try self.handle.write(contentsOf: bytes)
1121-
}
1122-
for leaf in leafNode.leaves {
1123-
try withUnsafeLittleEndianBytes(of: leaf) { bytes in
1124-
try self.handle.write(contentsOf: bytes)
1125-
}
1126-
}
1127-
let extentTail = ExtentTail(checksum: leafNode.leaves.last!.block)
1128-
try withUnsafeLittleEndianBytes(of: extentTail) { bytes in
1129-
try self.handle.write(contentsOf: bytes)
1130-
}
1131-
root.indices.append(extentIdx)
1132-
}
1133-
withUnsafeLittleEndianBytes(of: root.header) { bytes in
1245+
withUnsafeLittleEndianBytes(of: extentHeader) { bytes in
11341246
for b in bytes {
11351247
blockData[blockIndex] = b
11361248
blockIndex = blockIndex + 1
11371249
}
11381250
}
1139-
for leaf in root.indices {
1140-
withUnsafeLittleEndianBytes(of: leaf) { bytes in
1251+
for idx in rootIndices {
1252+
withUnsafeLittleEndianBytes(of: idx) { bytes in
11411253
for b in bytes {
11421254
blockData[blockIndex] = b
11431255
blockIndex = blockIndex + 1
11441256
}
11451257
}
11461258
}
1147-
default:
1148-
throw Error.fileTooBig(UInt64(dataBlocks) * self.blockSize)
11491259
}
11501260
inode.block = (
11511261
blockData[0], blockData[1], blockData[2], blockData[3], blockData[4], blockData[5], blockData[6],

Sources/ContainerizationEXT4/EXT4+Reader.swift

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -196,61 +196,61 @@ extension EXT4 {
196196
func getExtents(inode: InodeNumber) throws -> [(start: UInt32, end: UInt32)]? {
197197
let inode = try self.getInode(number: inode)
198198
let inodeBlock = Data(tupleToArray(inode.block))
199-
var offset = 0
200-
var extents: [(start: UInt32, end: UInt32)] = []
201199

202200
let extentHeaderSize = MemoryLayout<ExtentHeader>.size
203-
let extentIndexSize = MemoryLayout<ExtentIndex>.size
204-
let extentLeafSize = MemoryLayout<ExtentLeaf>.size
205-
// read extent header
206-
let header = inodeBlock.subdata(in: offset..<offset + extentHeaderSize).withUnsafeBytes {
201+
let header = inodeBlock.subdata(in: 0..<extentHeaderSize).withUnsafeBytes {
207202
$0.loadLittleEndian(as: ExtentHeader.self)
208203
}
209204
guard header.magic == EXT4.ExtentHeaderMagic else {
210205
return []
211206
}
212-
offset += extentHeaderSize // Jump to entries
213-
switch header.depth {
214-
case 0:
215-
// When depth is 0 the extent header is followed by extent leaves
207+
var extents: [(start: UInt32, end: UInt32)] = []
208+
try readExtentNode(data: inodeBlock, header: header, into: &extents)
209+
return extents
210+
}
211+
212+
/// Recursively reads extent tree nodes. For leaf nodes (depth 0), appends
213+
/// extent mappings directly. For index nodes (depth > 0), follows each
214+
/// index entry to a child block and recurses.
215+
private func readExtentNode(
216+
data: Data,
217+
header: ExtentHeader,
218+
into extents: inout [(start: UInt32, end: UInt32)]
219+
) throws {
220+
let extentHeaderSize = MemoryLayout<ExtentHeader>.size
221+
let extentIndexSize = MemoryLayout<ExtentIndex>.size
222+
let extentLeafSize = MemoryLayout<ExtentLeaf>.size
223+
var offset = extentHeaderSize
224+
225+
if header.depth == 0 {
226+
// Leaf node: entries are ExtentLeaf mappings
216227
for _ in 0..<header.entries {
217-
let leaf = inodeBlock.subdata(in: offset..<offset + extentLeafSize).withUnsafeBytes {
218-
$0.load(as: ExtentLeaf.self)
228+
let leaf = data.subdata(in: offset..<offset + extentLeafSize).withUnsafeBytes {
229+
$0.loadLittleEndian(as: ExtentLeaf.self)
219230
}
220231
extents.append((leaf.startLow, leaf.startLow + UInt32(leaf.length)))
221232
offset += extentLeafSize
222233
}
223-
case 1:
224-
// When depth is 1 the extent header is followed by extent indices which point to leaves
234+
} else {
235+
// Index node: entries are ExtentIndex pointers to child blocks
225236
for _ in 0..<header.entries {
226-
let indexNode = inodeBlock.subdata(in: offset..<offset + extentIndexSize).withUnsafeBytes {
227-
$0.load(as: ExtentIndex.self)
237+
let indexNode = data.subdata(in: offset..<offset + extentIndexSize).withUnsafeBytes {
238+
$0.loadLittleEndian(as: ExtentIndex.self)
228239
}
229240
try self.seek(block: indexNode.leafLow)
230241
guard let block = try self.handle.read(upToCount: Int(self.blockSize)) else {
231242
throw EXT4.Error.couldNotReadBlock(indexNode.leafLow)
232243
}
233-
var blockOffset = 0
234-
let leafHeader = block.subdata(in: blockOffset..<extentHeaderSize).withUnsafeBytes {
244+
let childHeader = block.subdata(in: 0..<extentHeaderSize).withUnsafeBytes {
235245
$0.loadLittleEndian(as: ExtentHeader.self)
236246
}
237-
guard leafHeader.magic == EXT4.ExtentHeaderMagic else {
247+
guard childHeader.magic == EXT4.ExtentHeaderMagic else {
238248
throw Error.invalidExtents
239249
}
240-
blockOffset += extentHeaderSize
241-
for _ in 0..<leafHeader.entries {
242-
let leaf = block.subdata(in: blockOffset..<blockOffset + extentLeafSize).withUnsafeBytes {
243-
$0.loadLittleEndian(as: ExtentLeaf.self)
244-
}
245-
extents.append((leaf.startLow, leaf.startLow + UInt32(leaf.length)))
246-
blockOffset += extentLeafSize
247-
}
250+
try readExtentNode(data: block, header: childHeader, into: &extents)
248251
offset += extentIndexSize
249252
}
250-
default:
251-
throw Error.deepExtentsUnimplemented
252253
}
253-
return extents
254254
}
255255

256256
// MARK: Internal functions

Tests/ContainerizationEXT4Tests/TestEXT4Reader+IO.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,38 @@ struct EXT4PathIOTests {
548548
#expect(String(decoding: partialData, as: UTF8.self) == expectedContent)
549549
}
550550

551+
@Test
552+
func multiExtentFileReadback() throws {
553+
// Create an image large enough that the formatter produces multiple extents
554+
// by interleaving file creation (which can cause non-contiguous allocation).
555+
// This exercises the recursive readExtentNode() path for depth 0 and depth 1.
556+
let url = try buildFS(minDiskSize: 32 * 1024 * 1024) { fmt in
557+
// Create many files to consume blocks, then a large file that may
558+
// span multiple extents due to intervening allocations.
559+
for i in 0..<50 {
560+
let content = String(repeating: Character(UnicodeScalar(65 + (i % 26))!), count: 8192)
561+
try self.createFile(fmt, "/filler_\(i).txt", content)
562+
}
563+
// Create a large file — the formatter writes this contiguously, but the
564+
// reader still exercises getExtents() → readExtentNode() with depth 0.
565+
let bigContent = String(repeating: "X", count: 512 * 1024)
566+
try self.createFile(fmt, "/big.bin", bigContent)
567+
}
568+
defer { try? FileManager.default.removeItem(at: url) }
569+
570+
let r = try openReader(url)
571+
572+
// Verify the large file reads back correctly
573+
let data = try r.readFile(at: FilePath("/big.bin"))
574+
#expect(data.count == 512 * 1024)
575+
#expect(data.allSatisfy { $0 == UInt8(ascii: "X") })
576+
577+
// Verify filler files also read correctly
578+
let filler0 = try r.readFile(at: FilePath("/filler_0.txt"))
579+
#expect(filler0.count == 8192)
580+
#expect(filler0.allSatisfy { $0 == UInt8(ascii: "A") })
581+
}
582+
551583
@Test
552584
func largeFileReadAcrossBlocks() throws {
553585
// Keep this modest to avoid slow CI while still crossing multiple blocks.

0 commit comments

Comments
 (0)