Skip to content

Commit 5c0fe36

Browse files
committed
Support partial last EXT4 block group
1 parent f2c4240 commit 5c0fe36

2 files changed

Lines changed: 72 additions & 51 deletions

File tree

Sources/ContainerizationEXT4/EXT4+Formatter.swift

Lines changed: 53 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -671,14 +671,10 @@ extension EXT4 {
671671
let bitmapOffset = self.currentBlock
672672
let bitmapBlocks: UInt32 = blockGroupSize.blockGroups * 2 // each group has two bitmaps - for inodes, and for blocks
673673
let dataBlocks: UInt32 = bitmapOffset + bitmapBlocks // last data block
674-
var diskBlocks = dataBlocks
675674
var contentRequiredBlocks = (blockGroupSize.blockGroups - 1) * self.blocksPerGroup + 1
676675
if blockGroupSize.blockGroups == 1 {
677676
contentRequiredBlocks = self.blocksPerGroup // at least 1 block group
678677
}
679-
if diskBlocks < contentRequiredBlocks { // for data + metadata
680-
diskBlocks = contentRequiredBlocks
681-
}
682678
let contentRequiredSize = UInt64(contentRequiredBlocks) * self.blockSize
683679
// minDiskSize is usable capacity; the journal is additive on top.
684680
var newSize = self.size + journalByteCount
@@ -697,23 +693,27 @@ extension EXT4 {
697693
var groupDescriptors: [GroupDescriptor] = []
698694

699695
let minGroups = (((self.pos / UInt64(self.blockSize)) - 1) / UInt64(self.blocksPerGroup)) + 1
700-
if newSize < minGroups * blocksPerGroup * blockSize {
701-
newSize = UInt64(minGroups * blocksPerGroup * blockSize)
702-
}
703-
let totalGroups = (((newSize / UInt64(self.blockSize)) - 1) / UInt64(self.blocksPerGroup)) + 1
704-
705-
// If the provided disk size is not aligned to a blockgroup boundary, it needs to
706-
// be expanded to the next blockgroup boundary.
707-
// Example:
708-
// Provided disk size: 2 GB + 100MB: 2148 MB
709-
// BlockSize: 4096
710-
// Blockgroup size: 32768 blocks: 128MB
711-
// Number of blocks: 549888
712-
// Number of blockgroups = 549888 / 32768 = 16.78125
713-
// Aligned disk size = 557056 blocks = 17 blockgroups: 2176 MB
714-
if newSize < totalGroups * blocksPerGroup * blockSize {
715-
newSize = UInt64(totalGroups * blocksPerGroup * blockSize)
696+
let minBlocksForWrittenMetadata = (minGroups - 1) * UInt64(self.blocksPerGroup) + 1
697+
let minSizeForWrittenMetadata = minBlocksForWrittenMetadata * UInt64(self.blockSize)
698+
if newSize < minSizeForWrittenMetadata {
699+
newSize = minSizeForWrittenMetadata
700+
}
701+
702+
let filesystemBlocks64 = (newSize + UInt64(self.blockSize) - 1) / UInt64(self.blockSize)
703+
guard filesystemBlocks64 <= UInt64(UInt32.max) else {
704+
throw Error.cannotResizeFS(newSize)
705+
}
706+
let filesystemBlocks = UInt32(filesystemBlocks64)
707+
let totalGroups = ((filesystemBlocks - 1) / self.blocksPerGroup) + 1
708+
709+
func blocksInGroup(_ group: UInt32) -> UInt32 {
710+
let groupStart = group * self.blocksPerGroup
711+
guard groupStart < filesystemBlocks else {
712+
return 0
713+
}
714+
return min(self.blocksPerGroup, filesystemBlocks - groupStart)
716715
}
716+
717717
// Snapshot groupDescriptorBlocks before self.size potentially changes: the bitmap
718718
// loop uses this to identify which GDT slots were physically reserved at init time,
719719
// so it can mark any unused slots as free without accidentally freeing content blocks
@@ -739,15 +739,15 @@ extension EXT4 {
739739
var blocks: UInt32 = 0
740740
// blocks bitmap
741741
var bitmap: [UInt8] = .init(repeating: 0, count: self.blockSize * 2) // 1 for blocks, 1 for inodes
742-
if (group + 1) * UInt32(self.blocksPerGroup) <= dataBlocks { // fully allocated group
743-
for i in 0..<(self.blockSize) {
744-
bitmap[Int(i)] = 0xff // mark as allocated
745-
}
746-
blocks = UInt32(self.blocksPerGroup)
747-
} else if group * UInt32(self.blocksPerGroup) < dataBlocks { // partially allocated group
748-
for i in 0..<dataBlocks - group * UInt32(self.blocksPerGroup) {
742+
let groupStart = group * self.blocksPerGroup
743+
let groupBlocks = blocksInGroup(group)
744+
if groupStart < dataBlocks {
745+
let usedBlocks = min(dataBlocks - groupStart, self.blocksPerGroup)
746+
for i in 0..<usedBlocks {
749747
bitmap[Int(i / 8)] |= 1 << (i % 8)
750-
blocks += 1
748+
if i < groupBlocks {
749+
blocks += 1
750+
}
751751
}
752752
}
753753

@@ -760,23 +760,21 @@ extension EXT4 {
760760
}
761761
if usedGroupDescriptorBlocks + 1 <= reservedDescriptorBlocks {
762762
for i in usedGroupDescriptorBlocks + 1...reservedDescriptorBlocks {
763+
if i < groupBlocks {
764+
blocks -= UInt32((bitmap[Int(i / 8)] >> (i % 8)) & 1)
765+
}
763766
bitmap[Int(i / 8)] &= ~(1 << (i % 8))
764-
blocks -= 1
765767
}
766768
}
767769
}
768770

769-
// last blockGroup if not aligned with total size should be marked as allocated
770-
let remainingBlocks = diskBlocks % self.blocksPerGroup
771-
if group == totalGroups - 1 && remainingBlocks != 0 && self.size / self.blockSize < self.blocksPerGroup {
772-
for i in remainingBlocks..<self.blocksPerGroup {
771+
// The final block group may be shorter than blocksPerGroup. Mark blocks
772+
// outside the filesystem as used in the bitmap, but do not count them as
773+
// allocated filesystem blocks.
774+
if groupBlocks < self.blocksPerGroup {
775+
for i in groupBlocks..<self.blocksPerGroup {
773776
bitmap[Int(i / 8)] |= 1 << (i % 8)
774777
}
775-
if remainingBlocks < self.size / self.blockSize {
776-
for i in remainingBlocks..<self.size / self.blockSize {
777-
bitmap[Int(i / 8)] &= ~(1 << (i % 8))
778-
}
779-
}
780778
}
781779

782780
// mark deleted blocks as free
@@ -819,7 +817,7 @@ extension EXT4 {
819817
let blockBitmap = UInt64(bitmapOffset + 2 * group)
820818
let inodeBitmap = UInt64(bitmapOffset + 2 * group + 1)
821819
let inodeTable = inodeTableOffset + UInt64(group * inodeTableSizePerGroup)
822-
let freeBlocksCount = UInt32(self.blocksPerGroup - blocks)
820+
let freeBlocksCount = UInt32(groupBlocks - blocks)
823821
let freeInodesCount = UInt32(blockGroupSize.inodesPerGroup - inodes)
824822
groupDescriptors.append(
825823
// low bits
@@ -851,13 +849,20 @@ extension EXT4 {
851849
for i in 0..<UInt16(blockGroupSize.inodesPerGroup) {
852850
inodeBitmap[Int(i) / 8] &= ~(1 << (i % 8))
853851
}
854-
for group in blockGroupSize.blockGroups..<totalGroups.lo {
855-
let blocksInGroup = UInt32(self.blocksPerGroup)
852+
for group in blockGroupSize.blockGroups..<totalGroups {
853+
let groupBlocks = blocksInGroup(group)
856854
let blockBitmapOffset = UInt64(group * self.blocksPerGroup + inodeTableSizePerGroup)
857855
let inodeBitmapOffset = UInt64(group * self.blocksPerGroup + inodeTableSizePerGroup + 1)
858856
let inodeTableOffset = UInt64(self.blocksPerGroup) * group
859-
let freeBlocksCount = UInt32(blocksInGroup - inodeTableSizePerGroup - 2)
857+
let metadataBlocks = min(groupBlocks, inodeTableSizePerGroup + 2)
858+
let freeBlocksCount = UInt32(groupBlocks - metadataBlocks)
860859
let freeInodesCount = UInt32(blockGroupSize.inodesPerGroup)
860+
var groupBlockBitmap = blockBitmap
861+
if groupBlocks < self.blocksPerGroup {
862+
for i in groupBlocks..<self.blocksPerGroup {
863+
groupBlockBitmap[Int(i / 8)] |= 1 << (i % 8)
864+
}
865+
}
861866
groupDescriptors.append(
862867
// low bits
863868
GroupDescriptor(
@@ -874,9 +879,9 @@ extension EXT4 {
874879
itableUnusedLow: 0x0000,
875880
checksum: 0x0000
876881
))
877-
totalBlocks += (inodeTableSizePerGroup + 2)
882+
totalBlocks += metadataBlocks
878883
try self.seek(block: group * self.blocksPerGroup + inodeTableSizePerGroup)
879-
try self.handle.write(contentsOf: blockBitmap)
884+
try self.handle.write(contentsOf: groupBlockBitmap)
880885
try self.handle.write(contentsOf: inodeBitmap)
881886
}
882887

@@ -892,23 +897,20 @@ extension EXT4 {
892897
try self.handle.write(contentsOf: Array<UInt8>.init(repeating: 0, count: 1024))
893898

894899
let computedInodes = totalGroups * blockGroupSize.inodesPerGroup
895-
var blocksCount = totalGroups * self.blocksPerGroup
896-
while blocksCount < totalBlocks {
897-
blocksCount = UInt64(totalBlocks)
898-
}
900+
let blocksCount = UInt64(filesystemBlocks)
899901
let totalFreeBlocks: UInt64
900902
if totalBlocks > blocksCount {
901903
totalFreeBlocks = 0
902904
} else {
903-
totalFreeBlocks = blocksCount - totalBlocks
905+
totalFreeBlocks = blocksCount - UInt64(totalBlocks)
904906
}
905907
var superblock = SuperBlock()
906-
superblock.inodesCount = computedInodes.lo
908+
superblock.inodesCount = computedInodes
907909
superblock.blocksCountLow = blocksCount.lo
908910
superblock.blocksCountHigh = blocksCount.hi
909911
superblock.freeBlocksCountLow = totalFreeBlocks.lo
910912
superblock.freeBlocksCountHigh = totalFreeBlocks.hi
911-
let freeInodesCount = computedInodes.lo - totalInodes
913+
let freeInodesCount = computedInodes - totalInodes
912914
superblock.freeInodesCount = freeInodesCount
913915
superblock.firstDataBlock = 0
914916
superblock.logBlockSize = logBlockSize

Tests/ContainerizationEXT4Tests/TestEXT4Format.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,23 @@ struct Ext4FormatEmptyRangeTests {
259259
let formatter = try EXT4.Formatter(fsPath, minDiskSize: 32.kib())
260260
try formatter.close()
261261
}
262+
263+
@Test func partialLastBlockGroupPreservesRequestedSize() throws {
264+
let fsPath = FilePath(
265+
FileManager.default.temporaryDirectory
266+
.appendingPathComponent(UUID().uuidString, isDirectory: false))
267+
defer { try? FileManager.default.removeItem(at: fsPath.url) }
268+
269+
let formatter = try EXT4.Formatter(fsPath, minDiskSize: 160.mib())
270+
try formatter.close()
271+
272+
let file = try FileHandle(forReadingFrom: fsPath.url)
273+
#expect(try file.seekToEnd() == 160.mib())
274+
275+
let ext4 = try EXT4.EXT4Reader(blockDevice: fsPath)
276+
#expect(ext4.superBlock.blocksCountLow == 40_960)
277+
278+
let partialGroup = try ext4.getGroupDescriptor(1)
279+
#expect(partialGroup.freeBlocksCountLow == 7_678)
280+
}
262281
}

0 commit comments

Comments
 (0)