@@ -38,6 +38,8 @@ extension EXT4 {
3838 blockSize * 8 // limited by inode bitmap
3939 }
4040
41+ private let minimumInodesPerGroup : UInt32 = 896
42+
4143 private var groupsPerDescriptorBlock : UInt32 {
4244 blockSize / groupDescriptorSize
4345 }
@@ -55,6 +57,11 @@ extension EXT4 {
5557 ( ( groupCount - 1 ) / groupsPerDescriptorBlock + 1 ) * 32
5658 }
5759
60+ private var blocksInLastGroup : UInt32 {
61+ let remainder = blockCount % blocksPerGroup
62+ return remainder == 0 ? blocksPerGroup : remainder
63+ }
64+
5865 /// Initializes an ext4 filesystem formatter.
5966 ///
6067 /// This constructor creates an instance of the ext4 formatter designed to format a block device
@@ -685,11 +692,6 @@ extension EXT4 {
685692 if newSize < contentRequiredSize {
686693 newSize = contentRequiredSize
687694 }
688- // number of blocks needed for group descriptors
689- let groupDescriptorBlockCount : UInt32 = ( blockGroupSize. blockGroups - 1 ) / self . groupsPerDescriptorBlock + 1
690- guard groupDescriptorBlockCount <= self . groupDescriptorBlocks else {
691- throw Error . insufficientSpaceForGroupDescriptorBlocks
692- }
693695
694696 var totalBlocks : UInt32 = 0
695697 var totalInodes : UInt32 = 0
@@ -700,20 +702,33 @@ extension EXT4 {
700702 if newSize < minGroups * blocksPerGroup * blockSize {
701703 newSize = UInt64 ( minGroups * blocksPerGroup * blockSize)
702704 }
703- let totalGroups = ( ( ( newSize / UInt64( self . blockSize) ) - 1 ) / UInt64( self . blocksPerGroup) ) + 1
704705
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)
706+ // Preserve the requested filesystem size exactly when possible.
707+ // Any trailing partial group is kept as-is; we do not round up to a full
708+ // block-group boundary just to place that group's metadata.
709+ //
710+ // For groups beyond blockGroupSize.blockGroups, metadata is packed into a
711+ // reserved region starting at dataBlocks:
712+ // - inode table: inodeTableSizePerGroup blocks
713+ // - block bitmap: 1 block
714+ // - inode bitmap: 1 block
715+ //
716+ // This keeps descriptor pointers in-bounds even when the last group is tiny
717+ // (for example, 128 MiB + 4 KiB), while still preserving exact-size images
718+ // for larger partial tails (for example, 160 MiB).
719+
720+ let fsBlocks : UInt64 = ( newSize + UInt64( self . blockSize) - 1 ) / UInt64( self . blockSize) // round up to block boundary
721+ let totalGroups = ( ( fsBlocks - 1 ) / UInt64( self . blocksPerGroup) ) + 1 // round up to group boundary
722+ let groupDescriptorBlockCount : UInt32 = ( UInt32 ( totalGroups) - 1 ) / self . groupsPerDescriptorBlock + 1 // round up to descriptor block boundary
723+ guard groupDescriptorBlockCount <= self . groupDescriptorBlocks else {
724+ throw Error . insufficientSpaceForGroupDescriptorBlocks
716725 }
726+ let extraGroupCount = UInt64 ( UInt32 ( totalGroups) - blockGroupSize. blockGroups) // count of groups beyond blockGroupSize.blockGroups that require packed metadata layout
727+ let packedMetadataStart = UInt64 ( dataBlocks) // start block (inclusive) of packed metadata region for extra groups
728+ let packedMetadataBlocks = extraGroupCount * UInt64( inodeTableSizePerGroup + 2 ) // each extra group has inodeTableSizePerGroup blocks for the inode table, plus 1 block for the block bitmap and 1 block for the inode bitmap
729+ let packedMetadataEnd = UInt32 ( packedMetadataStart + packedMetadataBlocks) // end block (exclusive) of packed metadata region for extra groups
730+ let reservedDataBlocks = max ( dataBlocks, packedMetadataEnd) // exclusive upper bound of reserved blocks (data/metadata), used for bitmap marking
731+
717732 // Snapshot groupDescriptorBlocks before self.size potentially changes: the bitmap
718733 // loop uses this to identify which GDT slots were physically reserved at init time,
719734 // so it can mark any unused slots as free without accidentally freeing content blocks
@@ -739,13 +754,13 @@ extension EXT4 {
739754 var blocks : UInt32 = 0
740755 // blocks bitmap
741756 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
757+ if ( group + 1 ) * UInt32( self . blocksPerGroup) <= reservedDataBlocks { // fully allocated group
743758 for i in 0 ..< ( self . blockSize) {
744759 bitmap [ Int ( i) ] = 0xff // mark as allocated
745760 }
746761 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) {
762+ } else if group * UInt32( self . blocksPerGroup) < reservedDataBlocks { // partially allocated group
763+ for i in 0 ..< reservedDataBlocks - group * UInt32( self . blocksPerGroup) {
749764 bitmap [ Int ( i / 8 ) ] |= 1 << ( i % 8 )
750765 blocks += 1
751766 }
@@ -766,17 +781,13 @@ extension EXT4 {
766781 }
767782 }
768783
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 {
784+ // The true last group may be smaller than blocksPerGroup; blocks beyond
785+ // groupBlockCount don't physically exist and must be marked used.
786+ let groupBlockCount = group == totalGroups. lo - 1 ? blocksInLastGroup : self . blocksPerGroup
787+ if groupBlockCount < self . blocksPerGroup {
788+ for i in groupBlockCount..< self . blocksPerGroup {
773789 bitmap [ Int ( i / 8 ) ] |= 1 << ( i % 8 )
774790 }
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- }
780791 }
781792
782793 // mark deleted blocks as free
@@ -819,7 +830,7 @@ extension EXT4 {
819830 let blockBitmap = UInt64 ( bitmapOffset + 2 * group)
820831 let inodeBitmap = UInt64 ( bitmapOffset + 2 * group + 1 )
821832 let inodeTable = inodeTableOffset + UInt64( group * inodeTableSizePerGroup)
822- let freeBlocksCount = UInt32 ( self . blocksPerGroup - blocks)
833+ let freeBlocksCount = UInt32 ( groupBlockCount - blocks)
823834 let freeInodesCount = UInt32 ( blockGroupSize. inodesPerGroup - inodes)
824835 groupDescriptors. append (
825836 // low bits
@@ -843,21 +854,48 @@ extension EXT4 {
843854
844855 // Since the bitmaps for unoccupied block groups are the same, there is no need
845856 // to allocate separate memory or storage for each individual bitmap.
846- var blockBitmap : [ UInt8 ] = . init( repeating: 0 , count: Int ( self . blocksPerGroup) / 8 )
857+ let blockBitmap : [ UInt8 ] = . init( repeating: 0 , count: Int ( self . blocksPerGroup) / 8 )
847858 var inodeBitmap : [ UInt8 ] = . init( repeating: 0xff , count: Int ( self . blocksPerGroup) / 8 )
848- for i in 0 ..< inodeTableSizePerGroup + 2 {
849- blockBitmap [ Int ( i) / 8 ] |= 1 << ( i % 8 )
850- }
851859 for i in 0 ..< UInt16 ( blockGroupSize. inodesPerGroup) {
852860 inodeBitmap [ Int ( i) / 8 ] &= ~ ( 1 << ( i % 8 ) )
853861 }
862+ var packedMetadataCursor = packedMetadataStart
854863 for group in blockGroupSize. blockGroups..< totalGroups. lo {
855- let blocksInGroup = UInt32 ( self . blocksPerGroup)
856- let blockBitmapOffset = UInt64 ( group * self . blocksPerGroup + inodeTableSizePerGroup)
857- let inodeBitmapOffset = UInt64 ( group * self . blocksPerGroup + inodeTableSizePerGroup + 1 )
858- let inodeTableOffset = UInt64 ( self . blocksPerGroup) * group
859- let freeBlocksCount = UInt32 ( blocksInGroup - inodeTableSizePerGroup - 2 )
864+ let groupStart = UInt64 ( group) * UInt64( self . blocksPerGroup)
865+ let blocksInGroup = UInt32 ( min ( UInt64 ( self . blocksPerGroup) , fsBlocks - groupStart) )
866+ let groupEnd = groupStart + UInt64( blocksInGroup)
867+
868+ var groupBlockBitmap = blockBitmap
869+ if blocksInGroup < self . blocksPerGroup {
870+ for i in blocksInGroup..< self . blocksPerGroup {
871+ groupBlockBitmap [ Int ( i / 8 ) ] |= 1 << ( i % 8 )
872+ }
873+ }
874+
875+ let packedStart = max ( groupStart, packedMetadataStart)
876+ let packedEnd = min ( groupEnd, UInt64 ( packedMetadataEnd) )
877+ var usedBlocksInGroup : UInt32 = 0
878+
879+ if packedEnd > packedStart {
880+ let localStart = UInt32 ( packedStart - groupStart)
881+ let localEnd = UInt32 ( packedEnd - groupStart)
882+
883+ for i in localStart..< localEnd {
884+ groupBlockBitmap [ Int ( i / 8 ) ] |= 1 << ( i % 8 )
885+ }
886+
887+ usedBlocksInGroup += localEnd - localStart
888+ }
889+
890+ let inodeTableOffset = packedMetadataCursor
891+ let blockBitmapOffset = inodeTableOffset + UInt64( inodeTableSizePerGroup)
892+ let inodeBitmapOffset = blockBitmapOffset + 1
893+ packedMetadataCursor += UInt64 ( inodeTableSizePerGroup + 2 )
894+ totalBlocks += usedBlocksInGroup
895+ let freeBlocksCount = blocksInGroup - usedBlocksInGroup
860896 let freeInodesCount = UInt32 ( blockGroupSize. inodesPerGroup)
897+ let zeroInodeTable = [ UInt8] ( repeating: 0 , count: Int ( inodeTableSizePerGroup) * Int( self . blockSize) )
898+
861899 groupDescriptors. append (
862900 // low bits
863901 GroupDescriptor (
@@ -874,9 +912,11 @@ extension EXT4 {
874912 itableUnusedLow: 0x0000 ,
875913 checksum: 0x0000
876914 ) )
877- totalBlocks += ( inodeTableSizePerGroup + 2 )
878- try self . seek ( block: group * self . blocksPerGroup + inodeTableSizePerGroup)
879- try self . handle. write ( contentsOf: blockBitmap)
915+ try self . seek ( block: UInt32 ( inodeTableOffset) )
916+ try self . handle. write ( contentsOf: zeroInodeTable)
917+
918+ try self . seek ( block: UInt32 ( blockBitmapOffset) )
919+ try self . handle. write ( contentsOf: groupBlockBitmap)
880920 try self . handle. write ( contentsOf: inodeBitmap)
881921 }
882922
@@ -890,10 +930,9 @@ extension EXT4 {
890930 // write superblock
891931 try self . seek ( block: 0 )
892932 try self . handle. write ( contentsOf: Array< UInt8> . init( repeating: 0 , count: 1024 ) )
893-
894933 let computedInodes = totalGroups * blockGroupSize. inodesPerGroup
895- var blocksCount = totalGroups * self . blocksPerGroup
896- while blocksCount < totalBlocks {
934+ var blocksCount = ( newSize + UInt64 ( self . blockSize ) - 1 ) / UInt64 ( self . blockSize )
935+ if blocksCount < totalBlocks {
897936 blocksCount = UInt64 ( totalBlocks)
898937 }
899938 let totalFreeBlocks : UInt64
@@ -1053,15 +1092,16 @@ extension EXT4 {
10531092
10541093 var groups : UInt32 = UInt32 . max
10551094 var inodesPerGroup : UInt32 = 0
1056- let inc = Int ( self . blockSize * 512 ) / Int ( EXT4 . InodeSize ) // inodesPerGroup
1057- // minimizes the number of blockGroups needed to its lowest value
1058- for ipg in stride ( from: inc , through: Int ( self . maxInodesPerGroup) , by: inc ) {
1095+ let start = Int ( self . minimumInodesPerGroup ) // inodesPerGroup
1096+ let step = Int ( self . minimumInodesPerGroup )
1097+ for ipg in stride ( from: start , through: Int ( self . maxInodesPerGroup) , by: step ) {
10591098 let g = groupCount ( blocks, inodes, UInt32 ( ipg) )
1060- if g < groups {
1099+ if g < groups || ( g == groups && UInt32 ( ipg ) < inodesPerGroup ) {
10611100 groups = g
10621101 inodesPerGroup = UInt32 ( ipg)
10631102 }
10641103 }
1104+
10651105 return ( groups, inodesPerGroup)
10661106 }
10671107
0 commit comments