Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 1 | // Package gpt implements reading and writing GUID Partition Tables as specified |
| 2 | // in the UEFI Specification. It only implements up to 128 partitions per table |
| 3 | // (same as most other implementations) as more would require a dynamic table |
| 4 | // size, significantly complicating the code for little gain. |
| 5 | package gpt |
| 6 | |
| 7 | import ( |
| 8 | "bytes" |
| 9 | "encoding/binary" |
| 10 | "errors" |
| 11 | "fmt" |
| 12 | "hash/crc32" |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 13 | "sort" |
| 14 | "strings" |
| 15 | "unicode/utf16" |
| 16 | |
| 17 | "github.com/google/uuid" |
Lorenz Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 18 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 19 | "source.monogon.dev/metropolis/pkg/blockdev" |
Lorenz Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 20 | "source.monogon.dev/metropolis/pkg/msguid" |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 21 | ) |
| 22 | |
| 23 | var gptSignature = [8]byte{'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T'} |
| 24 | var gptRevision uint32 = 0x00010000 // First 2 bytes major, second 2 bytes minor |
| 25 | |
| 26 | // See UEFI Specification 2.9 Table 5-5 |
| 27 | type header struct { |
| 28 | Signature [8]byte |
| 29 | Revision uint32 |
| 30 | HeaderSize uint32 |
| 31 | HeaderCRC32 uint32 |
| 32 | _ [4]byte |
| 33 | |
| 34 | HeaderBlock uint64 |
| 35 | AlternateHeaderBlock uint64 |
| 36 | FirstUsableBlock uint64 |
| 37 | LastUsableBlock uint64 |
| 38 | |
| 39 | ID [16]byte |
| 40 | |
| 41 | PartitionEntriesStartBlock uint64 |
| 42 | PartitionEntryCount uint32 |
| 43 | PartitionEntrySize uint32 |
| 44 | PartitionEntriesCRC32 uint32 |
| 45 | } |
| 46 | |
| 47 | // See UEFI Specification 2.9 Table 5-6 |
| 48 | type partition struct { |
| 49 | Type [16]byte |
| 50 | ID [16]byte |
| 51 | FirstBlock uint64 |
| 52 | LastBlock uint64 |
| 53 | Attributes uint64 |
| 54 | Name [36]uint16 |
| 55 | } |
| 56 | |
| 57 | var ( |
| 58 | PartitionTypeEFISystem = uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B") |
| 59 | ) |
| 60 | |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 61 | // Attribute is a bitfield of attributes set on a partition. Bits 0 to 47 are |
| 62 | // reserved for UEFI specification use and all current assignments are in the |
| 63 | // following const block. Bits 48 to 64 are available for per-Type use by |
| 64 | // the organization controlling the partition Type. |
| 65 | type Attribute uint64 |
| 66 | |
| 67 | const ( |
| 68 | // AttrRequiredPartition indicates that this partition is required for the |
| 69 | // platform to function. Mostly used by vendors to mark things like recovery |
| 70 | // partitions. |
| 71 | AttrRequiredPartition = 1 << 0 |
| 72 | // AttrNoBlockIOProto indicates that EFI firmware must not provide an EFI |
| 73 | // block device (EFI_BLOCK_IO_PROTOCOL) for this partition. |
| 74 | AttrNoBlockIOProto = 1 << 1 |
| 75 | // AttrLegacyBIOSBootable indicates to special-purpose software outside of |
| 76 | // UEFI that this partition can be booted using a traditional PC BIOS. |
| 77 | // Don't use this unless you know that you need it specifically. |
| 78 | AttrLegacyBIOSBootable = 1 << 2 |
| 79 | ) |
| 80 | |
| 81 | // PerTypeAttrs returns the top 24 bits which are reserved for custom per-Type |
| 82 | // attributes. The top 8 bits of the returned uint32 are always 0. |
| 83 | func (a Attribute) PerTypeAttrs() uint32 { |
| 84 | return uint32(a >> 48) |
| 85 | } |
| 86 | |
| 87 | // SetPerTypeAttrs sets the top 24 bits which are reserved for custom per-Type |
| 88 | // attributes. It does not touch the lower attributes which are specified by the |
| 89 | // UEFI specification. The top 8 bits of v are silently discarded. |
| 90 | func (a *Attribute) SetPerTypeAttrs(v uint32) { |
| 91 | *a &= 0x000000FF_FFFFFFFF |
| 92 | *a |= Attribute(v) << 48 |
| 93 | } |
| 94 | |
| 95 | type Partition struct { |
| 96 | // Name of the partition, will be truncated if it expands to more than 36 |
| 97 | // UTF-16 code points. Not all systems can display non-BMP code points. |
| 98 | Name string |
| 99 | // Type is the type of Table partition, can either be one of the predefined |
| 100 | // constants by the UEFI specification or a custom type identifier. |
| 101 | // Note that the all-zero UUID denotes an empty partition slot, so this |
| 102 | // MUST be set to something, otherwise it is not treated as a partition. |
| 103 | Type uuid.UUID |
| 104 | // ID is a unique identifier for this specific partition. It should be |
| 105 | // changed when cloning the partition. |
| 106 | ID uuid.UUID |
| 107 | // The first logical block of the partition (inclusive) |
| 108 | FirstBlock uint64 |
| 109 | // The last logical block of the partition (inclusive) |
| 110 | LastBlock uint64 |
| 111 | // Bitset of attributes of this partition. |
| 112 | Attributes Attribute |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 113 | |
| 114 | *blockdev.Section |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 115 | } |
| 116 | |
| 117 | // SizeBlocks returns the size of the partition in blocks |
| 118 | func (p *Partition) SizeBlocks() uint64 { |
| 119 | return 1 + p.LastBlock - p.FirstBlock |
| 120 | } |
| 121 | |
| 122 | // IsUnused checks if the partition is unused, i.e. it is nil or its type is |
| 123 | // the null UUID. |
| 124 | func (p *Partition) IsUnused() bool { |
| 125 | if p == nil { |
| 126 | return true |
| 127 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 128 | return p.Type == uuid.Nil |
| 129 | } |
| 130 | |
| 131 | // New returns an empty table on the given block device. |
| 132 | // It does not read any existing GPT on the disk (use Read for that), nor does |
| 133 | // it write anything until Write is called. |
| 134 | func New(b blockdev.BlockDev) (*Table, error) { |
| 135 | return &Table{ |
| 136 | b: b, |
| 137 | }, nil |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 138 | } |
| 139 | |
| 140 | type Table struct { |
| 141 | // ID is the unique identifier of this specific disk / GPT. |
| 142 | // If this is left uninitialized/all-zeroes a new random ID is automatically |
| 143 | // generated when writing. |
| 144 | ID uuid.UUID |
| 145 | |
| 146 | // Data put at the start of the very first block. Gets loaded and executed |
| 147 | // by a legacy BIOS bootloader. This can be used to make GPT-partitioned |
| 148 | // disks bootable by legacy systems or display a nice error message. |
| 149 | // Maximum length is 440 bytes, if that is exceeded Write returns an error. |
| 150 | // Should be left empty if the device is not bootable and/or compatibility |
| 151 | // with BIOS booting is not required. Only useful on x86 systems. |
| 152 | BootCode []byte |
| 153 | |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 154 | // Partitions contains the list of partitions in this table. This is |
| 155 | // artificially limited to 128 partitions. |
| 156 | Partitions []*Partition |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 157 | |
| 158 | b blockdev.BlockDev |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 159 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 160 | |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 161 | type addOptions struct { |
| 162 | preferEnd bool |
| 163 | keepEmptyEntries bool |
| 164 | alignment int64 |
| 165 | } |
| 166 | |
| 167 | // AddOption is a bitset controlling various |
| 168 | type AddOption func(*addOptions) |
| 169 | |
| 170 | // WithPreferEnd tries to put the partition as close to the end as possible |
| 171 | // instead of as close to the start. |
| 172 | func WithPreferEnd() AddOption { |
| 173 | return func(options *addOptions) { |
| 174 | options.preferEnd = true |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | // WithKeepEmptyEntries does not fill up empty entries which are followed by |
| 179 | // filled ones. It always appends the partition after the last used entry. |
| 180 | // Without this flag, the partition is placed in the first empty entry. |
| 181 | func WithKeepEmptyEntries() AddOption { |
| 182 | return func(options *addOptions) { |
| 183 | options.keepEmptyEntries = true |
| 184 | } |
| 185 | } |
| 186 | |
| 187 | // WithAlignment allows aligning the partition start block to a non-default |
| 188 | // value. By default, these are aligned to 1MiB. |
Lorenz Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 189 | // Only use this flag if you are certain you need it, it can cause quite severe |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 190 | // performance degradation under certain conditions. |
| 191 | func WithAlignment(alignmenet int64) AddOption { |
| 192 | return func(options *addOptions) { |
| 193 | options.alignment = alignmenet |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | // AddPartition takes a pointer to a partition and adds it, placing it into |
| 198 | // the first (or last using WithPreferEnd) continuous free space which fits it. |
| 199 | // It writes the placement information (FirstBlock, LastBlock) back to p. |
| 200 | // By default, AddPartition aligns FirstBlock to 1MiB boundaries, but this can |
| 201 | // be overridden using WithAlignment. |
| 202 | func (g *Table) AddPartition(p *Partition, size int64, options ...AddOption) error { |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 203 | blockSize := g.b.BlockSize() |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 204 | var opts addOptions |
| 205 | // Align to 1MiB or the block size, whichever is bigger |
| 206 | opts.alignment = 1 * 1024 * 1024 |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 207 | if blockSize > opts.alignment { |
| 208 | opts.alignment = blockSize |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 209 | } |
| 210 | for _, o := range options { |
| 211 | o(&opts) |
| 212 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 213 | if opts.alignment%blockSize != 0 { |
| 214 | return fmt.Errorf("requested alignment (%d bytes) is not an integer multiple of the block size (%d), unable to align", opts.alignment, blockSize) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 215 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 216 | if p.ID == uuid.Nil { |
| 217 | p.ID = uuid.New() |
| 218 | } |
| 219 | |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 220 | fs, _, err := g.GetFreeSpaces() |
| 221 | if err != nil { |
| 222 | return fmt.Errorf("unable to determine free space: %v", err) |
| 223 | } |
| 224 | if opts.preferEnd { |
| 225 | // Reverse fs slice to start iteration at the end |
| 226 | for i, j := 0, len(fs)-1; i < j; i, j = i+1, j-1 { |
| 227 | fs[i], fs[j] = fs[j], fs[i] |
| 228 | } |
| 229 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 230 | // Number of blocks the partition should occupy, rounded up. |
| 231 | blocks := (size + blockSize - 1) / blockSize |
| 232 | if size == -1 { |
| 233 | var largestFreeSpace int64 |
| 234 | for _, freeInt := range fs { |
| 235 | intSz := freeInt[1] - freeInt[0] |
| 236 | if intSz > largestFreeSpace { |
| 237 | largestFreeSpace = intSz |
| 238 | } |
| 239 | } |
| 240 | blocks = largestFreeSpace |
| 241 | } |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 242 | var maxFreeBlocks int64 |
| 243 | for _, freeInt := range fs { |
| 244 | start := freeInt[0] |
| 245 | end := freeInt[1] |
| 246 | freeBlocks := end - start |
| 247 | // Align start properly |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 248 | alignTo := (opts.alignment / blockSize) |
| 249 | // Go doesn't implement the euclidean modulus, thus this construction |
| 250 | // is necessary. |
| 251 | paddingBlocks := ((alignTo - start) % alignTo) % alignTo |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 252 | freeBlocks -= paddingBlocks |
| 253 | start += paddingBlocks |
| 254 | if maxFreeBlocks < freeBlocks { |
| 255 | maxFreeBlocks = freeBlocks |
| 256 | } |
| 257 | if freeBlocks >= blocks { |
| 258 | if !opts.preferEnd { |
| 259 | p.FirstBlock = uint64(start) |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 260 | p.LastBlock = uint64(start + blocks - 1) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 261 | } else { |
| 262 | // Realign FirstBlock. This will always succeed as |
| 263 | // there is enough space to align to the start. |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 264 | moveLeft := (end - blocks - 1) % (opts.alignment / blockSize) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 265 | p.FirstBlock = uint64(end - (blocks + 1 + moveLeft)) |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 266 | p.LastBlock = uint64(end - (2 + moveLeft)) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 267 | } |
| 268 | newPartPos := -1 |
| 269 | if !opts.keepEmptyEntries { |
| 270 | for i, part := range g.Partitions { |
| 271 | if part.IsUnused() { |
| 272 | newPartPos = i |
| 273 | break |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | if newPartPos == -1 { |
| 278 | g.Partitions = append(g.Partitions, p) |
| 279 | } else { |
| 280 | g.Partitions[newPartPos] = p |
| 281 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 282 | p.Section = blockdev.NewSection(g.b, int64(p.FirstBlock), int64(p.LastBlock)+1) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 283 | return nil |
| 284 | } |
| 285 | } |
| 286 | |
| 287 | return fmt.Errorf("no space for partition of %d blocks, largest continuous free space after alignment is %d blocks", blocks, maxFreeBlocks) |
| 288 | } |
| 289 | |
| 290 | // FirstUsableBlock returns the first usable (i.e. a partition can start there) |
| 291 | // block. |
| 292 | func (g *Table) FirstUsableBlock() int64 { |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 293 | blockSize := g.b.BlockSize() |
| 294 | partitionEntryBlocks := (16384 + blockSize - 1) / blockSize |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 295 | return 2 + partitionEntryBlocks |
| 296 | } |
| 297 | |
| 298 | // LastUsableBlock returns the last usable (i.e. a partition can end there) |
| 299 | // block. This block is inclusive. |
| 300 | func (g *Table) LastUsableBlock() int64 { |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 301 | blockSize := g.b.BlockSize() |
| 302 | partitionEntryBlocks := (16384 + blockSize - 1) / blockSize |
| 303 | return g.b.BlockCount() - (2 + partitionEntryBlocks) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 304 | } |
| 305 | |
| 306 | // GetFreeSpaces returns a slice of tuples, each containing a half-closed |
| 307 | // interval of logical blocks not occupied by the GPT itself or any partition. |
| 308 | // The returned intervals are always in ascending order as well as |
| 309 | // non-overlapping. It also returns if it detected any overlaps between |
| 310 | // partitions or partitions and the GPT. It returns an error if and only if any |
| 311 | // partition has its FirstBlock before the LastBlock or exceeds the amount of |
| 312 | // blocks on the block device. |
| 313 | // |
| 314 | // Note that the most common use cases for this function are covered by |
| 315 | // AddPartition, you're encouraged to use it instead. |
| 316 | func (g *Table) GetFreeSpaces() ([][2]int64, bool, error) { |
| 317 | // This implements an efficient algorithm for finding free intervals given |
| 318 | // a set of potentially overlapping occupying intervals. It uses O(n*log n) |
| 319 | // time for n being the amount of intervals, i.e. partitions. It uses O(n) |
| 320 | // additional memory. This makes it de facto infinitely scalable in the |
| 321 | // context of partition tables as the size of the block device is not part |
| 322 | // of its cyclomatic complexity and O(n*log n) is tiny for even very big |
| 323 | // partition tables. |
| 324 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 325 | blockCount := g.b.BlockCount() |
| 326 | |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 327 | // startBlocks contains the start blocks (inclusive) of all occupied |
| 328 | // intervals. |
| 329 | var startBlocks []int64 |
| 330 | // endBlocks contains the end blocks (exclusive!) of all occupied intervals. |
| 331 | // The interval at index i is given by [startBlock[i], endBlock[i]). |
| 332 | var endBlocks []int64 |
| 333 | |
| 334 | // Reserve the primary GPT interval including the protective MBR. |
| 335 | startBlocks = append(startBlocks, 0) |
| 336 | endBlocks = append(endBlocks, g.FirstUsableBlock()) |
| 337 | |
| 338 | // Reserve the alternate GPT interval (needs +1 for exclusive interval) |
| 339 | startBlocks = append(startBlocks, g.LastUsableBlock()+1) |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 340 | endBlocks = append(endBlocks, blockCount) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 341 | |
| 342 | for i, part := range g.Partitions { |
| 343 | if part.IsUnused() { |
| 344 | continue |
| 345 | } |
| 346 | // Bail if partition does not contain a valid interval. These are open |
| 347 | // intervals, thus part.FirstBlock == part.LastBlock denotes a valid |
| 348 | // partition with a size of one block. |
| 349 | if part.FirstBlock > part.LastBlock { |
| 350 | return nil, false, fmt.Errorf("partition %d has a LastBlock smaller than its FirstBlock, its interval is [%d, %d]", i, part.FirstBlock, part.LastBlock) |
| 351 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 352 | if part.FirstBlock >= uint64(blockCount) || part.LastBlock >= uint64(blockCount) { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 353 | return nil, false, fmt.Errorf("partition %d exceeds the block count of the block device", i) |
| 354 | } |
| 355 | startBlocks = append(startBlocks, int64(part.FirstBlock)) |
| 356 | // Algorithm needs open-closed intervals, thus add +1 to the end. |
| 357 | endBlocks = append(endBlocks, int64(part.LastBlock)+1) |
| 358 | } |
| 359 | // Sort both sets of blocks independently in ascending order. Note that it |
| 360 | // is now no longer possible to extract the original intervals. Integers |
| 361 | // have no identity thus it doesn't matter if the sort is stable or not. |
| 362 | sort.Slice(startBlocks, func(i, j int) bool { return startBlocks[i] < startBlocks[j] }) |
| 363 | sort.Slice(endBlocks, func(i, j int) bool { return endBlocks[i] < endBlocks[j] }) |
| 364 | |
| 365 | var freeSpaces [][2]int64 |
| 366 | |
| 367 | // currentIntervals contains the number of intervals which contain the |
| 368 | // position currently being iterated over. If currentIntervals is ever |
| 369 | // bigger than 1, there is overlap within the given intervals. |
| 370 | currentIntervals := 0 |
| 371 | var hasOverlap bool |
| 372 | |
| 373 | // Iterate for as long as there are interval boundaries to be processed. |
| 374 | for len(startBlocks) != 0 || len(endBlocks) != 0 { |
| 375 | // Short-circuit boundary processing. If an interval ends at x and the |
| 376 | // next one starts at x (this is using half-open intervals), it would |
| 377 | // otherwise perform useless processing as well as create an empty free |
| 378 | // interval which would then need to be filtered back out. |
| 379 | if len(startBlocks) != 0 && len(endBlocks) != 0 && startBlocks[0] == endBlocks[0] { |
| 380 | startBlocks = startBlocks[1:] |
| 381 | endBlocks = endBlocks[1:] |
| 382 | continue |
| 383 | } |
| 384 | // Pick the lowest boundary from either startBlocks or endBlocks, |
| 385 | // preferring endBlocks if they are equal. Don't try to pick from empty |
| 386 | // slices. |
| 387 | if (len(startBlocks) != 0 && len(endBlocks) != 0 && startBlocks[0] < endBlocks[0]) || len(endBlocks) == 0 { |
| 388 | // If currentIntervals == 0 a free space region ends here. |
| 389 | // Since this algorithm creates the free space interval at the end |
| 390 | // of an occupied interval, for the first interval there is no free |
| 391 | // space entry. But in this case it's fine to just ignore it as the |
| 392 | // first interval always starts at 0 because of the GPT. |
| 393 | if currentIntervals == 0 && len(freeSpaces) != 0 { |
| 394 | freeSpaces[len(freeSpaces)-1][1] = startBlocks[0] |
| 395 | } |
| 396 | // This is the start of an interval, increase the number of active |
| 397 | // intervals. |
| 398 | currentIntervals++ |
| 399 | hasOverlap = hasOverlap || currentIntervals > 1 |
| 400 | // Drop processed startBlock from slice. |
| 401 | startBlocks = startBlocks[1:] |
| 402 | } else { |
| 403 | // This is the end of an interval, decrease the number of active |
| 404 | // intervals. |
| 405 | currentIntervals-- |
| 406 | // If currentIntervals == 0 a free space region starts here. |
| 407 | // Same as with the startBlocks, ignore a potential free block after |
| 408 | // the final range as the GPT occupies the last blocks anyway. |
| 409 | if currentIntervals == 0 && len(startBlocks) != 0 { |
| 410 | freeSpaces = append(freeSpaces, [2]int64{endBlocks[0], 0}) |
| 411 | } |
| 412 | endBlocks = endBlocks[1:] |
| 413 | } |
| 414 | } |
| 415 | return freeSpaces, hasOverlap, nil |
| 416 | } |
| 417 | |
| 418 | // Overhead returns the number of blocks the GPT partitioning itself consumes, |
| 419 | // i.e. aren't usable for user data. |
| 420 | func Overhead(blockSize int64) int64 { |
| 421 | // 3 blocks + 2x 16384 bytes (partition entry space) |
| 422 | partitionEntryBlocks := (16384 + blockSize - 1) / blockSize |
| 423 | return 3 + (2 * partitionEntryBlocks) |
| 424 | } |
| 425 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 426 | // Write writes the two GPTs, first the alternate, then the primary to the |
| 427 | // block device. If gpt.ID or any of the partition IDs are the all-zero UUID, |
| 428 | // new random ones are generated and written back. If the output is supposed |
| 429 | // to be reproducible, generate the UUIDs beforehand. |
| 430 | func (gpt *Table) Write() error { |
| 431 | blockSize := gpt.b.BlockSize() |
| 432 | blockCount := gpt.b.BlockCount() |
| 433 | if blockSize < 512 { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 434 | return errors.New("block size is smaller than 512 bytes, this is unsupported") |
| 435 | } |
| 436 | // Layout looks as follows: |
| 437 | // Block 0: Protective MBR |
| 438 | // Block 1: GPT Header |
| 439 | // Block 2-(16384 bytes): GPT partition entries |
| 440 | // Block (16384 bytes)-n: GPT partition entries alternate copy |
| 441 | // Block n: GPT Header alternate copy |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 442 | partitionEntryCount := 128 |
| 443 | if len(gpt.Partitions) > partitionEntryCount { |
| 444 | return errors.New("bigger-than default GPTs (>128 partitions) are unimplemented") |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 445 | } |
| 446 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 447 | partitionEntryBlocks := (16384 + blockSize - 1) / blockSize |
| 448 | if blockCount < 3+(2*partitionEntryBlocks) { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 449 | return errors.New("not enough blocks to write GPT") |
| 450 | } |
| 451 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 452 | if gpt.ID == uuid.Nil { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 453 | gpt.ID = uuid.New() |
| 454 | } |
| 455 | |
| 456 | partSize := binary.Size(partition{}) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 457 | var partitionEntriesData bytes.Buffer |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 458 | for i := 0; i < partitionEntryCount; i++ { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 459 | if len(gpt.Partitions) <= i || gpt.Partitions[i] == nil { |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 460 | // Write an empty entry |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 461 | partitionEntriesData.Write(make([]byte, partSize)) |
| 462 | continue |
| 463 | } |
| 464 | p := gpt.Partitions[i] |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 465 | if p.ID == uuid.Nil { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 466 | p.ID = uuid.New() |
| 467 | } |
| 468 | rawP := partition{ |
Lorenz Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 469 | Type: msguid.From(p.Type), |
| 470 | ID: msguid.From(p.ID), |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 471 | FirstBlock: p.FirstBlock, |
| 472 | LastBlock: p.LastBlock, |
| 473 | Attributes: uint64(p.Attributes), |
| 474 | } |
| 475 | nameUTF16 := utf16.Encode([]rune(p.Name)) |
| 476 | // copy will automatically truncate if target is too short |
| 477 | copy(rawP.Name[:], nameUTF16) |
| 478 | binary.Write(&partitionEntriesData, binary.LittleEndian, rawP) |
| 479 | } |
| 480 | |
| 481 | hdr := header{ |
| 482 | Signature: gptSignature, |
| 483 | Revision: gptRevision, |
| 484 | HeaderSize: uint32(binary.Size(&header{})), |
Lorenz Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 485 | ID: msguid.From(gpt.ID), |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 486 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 487 | PartitionEntryCount: uint32(partitionEntryCount), |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 488 | PartitionEntrySize: uint32(partSize), |
| 489 | |
| 490 | FirstUsableBlock: uint64(2 + partitionEntryBlocks), |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 491 | LastUsableBlock: uint64(blockCount - (2 + partitionEntryBlocks)), |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 492 | } |
| 493 | hdr.PartitionEntriesCRC32 = crc32.ChecksumIEEE(partitionEntriesData.Bytes()) |
| 494 | |
| 495 | hdrChecksum := crc32.NewIEEE() |
| 496 | |
| 497 | // Write alternate header first, as otherwise resizes are unsafe. If the |
| 498 | // alternate is currently not at the end of the block device, it cannot |
| 499 | // be found. Thus if the write operation is aborted abnormally, the |
| 500 | // primary GPT is corrupted and the alternate cannot be found because it |
| 501 | // is not at its canonical location. Rewriting the alternate first avoids |
| 502 | // this problem. |
| 503 | |
| 504 | // Alternate header |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 505 | hdr.HeaderBlock = uint64(blockCount - 1) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 506 | hdr.AlternateHeaderBlock = 1 |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 507 | hdr.PartitionEntriesStartBlock = uint64(blockCount - (1 + partitionEntryBlocks)) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 508 | |
| 509 | hdrChecksum.Reset() |
| 510 | hdr.HeaderCRC32 = 0 |
| 511 | binary.Write(hdrChecksum, binary.LittleEndian, &hdr) |
| 512 | hdr.HeaderCRC32 = hdrChecksum.Sum32() |
| 513 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 514 | for partitionEntriesData.Len()%int(blockSize) != 0 { |
| 515 | partitionEntriesData.WriteByte(0x00) |
| 516 | } |
| 517 | if _, err := gpt.b.WriteAt(partitionEntriesData.Bytes(), int64(hdr.PartitionEntriesStartBlock)*blockSize); err != nil { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 518 | return fmt.Errorf("failed to write alternate partition entries: %w", err) |
| 519 | } |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 520 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 521 | var hdrRaw bytes.Buffer |
| 522 | if err := binary.Write(&hdrRaw, binary.LittleEndian, &hdr); err != nil { |
| 523 | return fmt.Errorf("failed to encode alternate header: %w", err) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 524 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 525 | for hdrRaw.Len()%int(blockSize) != 0 { |
| 526 | hdrRaw.WriteByte(0x00) |
| 527 | } |
| 528 | if _, err := gpt.b.WriteAt(hdrRaw.Bytes(), (blockCount-1)*blockSize); err != nil { |
| 529 | return fmt.Errorf("failed to write alternate header: %v", err) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 530 | } |
| 531 | |
| 532 | // Primary header |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 533 | hdr.HeaderBlock = 1 |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 534 | hdr.AlternateHeaderBlock = uint64(blockCount - 1) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 535 | hdr.PartitionEntriesStartBlock = 2 |
| 536 | |
| 537 | hdrChecksum.Reset() |
| 538 | hdr.HeaderCRC32 = 0 |
| 539 | binary.Write(hdrChecksum, binary.LittleEndian, &hdr) |
| 540 | hdr.HeaderCRC32 = hdrChecksum.Sum32() |
| 541 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 542 | hdrRaw.Reset() |
| 543 | |
| 544 | if err := makeProtectiveMBR(&hdrRaw, blockCount, gpt.BootCode); err != nil { |
| 545 | return fmt.Errorf("failed creating protective MBR: %w", err) |
| 546 | } |
| 547 | for hdrRaw.Len()%int(blockSize) != 0 { |
| 548 | hdrRaw.WriteByte(0x00) |
| 549 | } |
| 550 | if err := binary.Write(&hdrRaw, binary.LittleEndian, &hdr); err != nil { |
| 551 | panic(err) |
| 552 | } |
| 553 | for hdrRaw.Len()%int(blockSize) != 0 { |
| 554 | hdrRaw.WriteByte(0x00) |
| 555 | } |
| 556 | hdrRaw.Write(partitionEntriesData.Bytes()) |
| 557 | for hdrRaw.Len()%int(blockSize) != 0 { |
| 558 | hdrRaw.WriteByte(0x00) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 559 | } |
| 560 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 561 | if _, err := gpt.b.WriteAt(hdrRaw.Bytes(), 0); err != nil { |
| 562 | return fmt.Errorf("failed to write primary GPT: %w", err) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 563 | } |
| 564 | return nil |
| 565 | } |
| 566 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 567 | // Read reads a Table from a block device. |
| 568 | func Read(r blockdev.BlockDev) (*Table, error) { |
| 569 | if Overhead(r.BlockSize()) > r.BlockCount() { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 570 | return nil, errors.New("disk cannot contain a GPT as the block count is too small to store one") |
| 571 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 572 | zeroBlock := make([]byte, r.BlockSize()) |
| 573 | if _, err := r.ReadAt(zeroBlock, 0); err != nil { |
| 574 | return nil, fmt.Errorf("failed to read first block: %w", err) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 575 | } |
| 576 | |
| 577 | var m mbr |
| 578 | if err := binary.Read(bytes.NewReader(zeroBlock[:512]), binary.LittleEndian, &m); err != nil { |
| 579 | panic(err) // Read is from memory and with enough data |
| 580 | } |
| 581 | // The UEFI standard says that the only acceptable MBR for a GPT-partitioned |
| 582 | // device is a pure protective MBR with one partition of type 0xEE covering |
| 583 | // the entire disk. But reality is sadly not so simple. People have come up |
| 584 | // with hacks like Hybrid MBR which is basically a way to expose partitions |
| 585 | // as both GPT partitions and MBR partitions. There are also GPTs without |
| 586 | // any MBR at all. |
| 587 | // Following the standard strictly when reading means that this library |
| 588 | // would fail to read valid GPT disks where such schemes are employed. |
| 589 | // On the other hand just looking at the GPT signature is also dangerous |
| 590 | // as not all tools clear the second block where the GPT resides when |
| 591 | // writing an MBR, which results in reading a wrong/obsolete GPT. |
| 592 | // As a pragmatic solution this library treats any disk as GPT-formatted if |
| 593 | // the first block does not contain an MBR signature or at least one MBR |
| 594 | // partition has type 0xEE (GPT). It does however not care in which slot |
| 595 | // this partition is or if it begins at the start of the disk. |
| 596 | // |
| 597 | // Note that the block signatures for MBR and FAT are shared. This is a |
| 598 | // historical artifact from DOS. It is not reliably possible to |
| 599 | // differentiate the two as either has boot code where the other has meta- |
| 600 | // data and both lack any checksums. Because the MBR partition table is at |
| 601 | // the very end of the FAT bootcode section the following code always |
| 602 | // assumes that it is dealing with an MBR. This is both more likely and |
| 603 | // the 0xEE marker is rarer and thus more specific than FATs 0x00, 0x80 and |
| 604 | // 0x02. |
| 605 | var bootCode []byte |
| 606 | hasDOSBootSig := m.Signature == mbrSignature |
| 607 | if hasDOSBootSig { |
| 608 | var isGPT bool |
| 609 | for _, p := range m.PartitionRecords { |
| 610 | if p.Type == 0xEE { |
| 611 | isGPT = true |
| 612 | } |
| 613 | } |
| 614 | // Note that there is a small but non-zero chance that isGPT is true |
| 615 | // for a raw FAT filesystem if the bootcode contains a "valid" MBR. |
| 616 | // The next error message mentions that possibility. |
| 617 | if !isGPT { |
| 618 | return nil, errors.New("block device contains an MBR table without a GPT marker or a raw FAT filesystem") |
| 619 | } |
| 620 | // Trim right zeroes away as they are padded back when writing. This |
| 621 | // makes BootCode empty when it is all-zeros, making it easier to work |
| 622 | // with while still round-tripping correctly. |
| 623 | bootCode = bytes.TrimRight(m.BootCode[:], "\x00") |
| 624 | } |
| 625 | // Read the primary GPT. If it is damaged and/or broken, read the alternate. |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 626 | primaryGPT, err := readSingleGPT(r, 1) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 627 | if err != nil { |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 628 | alternateGPT, err2 := readSingleGPT(r, r.BlockCount()-1) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 629 | if err2 != nil { |
| 630 | return nil, fmt.Errorf("failed to read both GPTs: primary GPT (%v), secondary GPT (%v)", err, err2) |
| 631 | } |
| 632 | alternateGPT.BootCode = bootCode |
| 633 | return alternateGPT, nil |
| 634 | } |
| 635 | primaryGPT.BootCode = bootCode |
| 636 | return primaryGPT, nil |
| 637 | } |
| 638 | |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 639 | func readSingleGPT(r blockdev.BlockDev, headerBlockPos int64) (*Table, error) { |
| 640 | hdrBlock := make([]byte, r.BlockSize()) |
| 641 | if _, err := r.ReadAt(hdrBlock, r.BlockSize()*headerBlockPos); err != nil { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 642 | return nil, fmt.Errorf("failed to read GPT header block: %w", err) |
| 643 | } |
| 644 | hdrBlockReader := bytes.NewReader(hdrBlock) |
| 645 | var hdr header |
| 646 | if err := binary.Read(hdrBlockReader, binary.LittleEndian, &hdr); err != nil { |
| 647 | panic(err) // Read from memory with enough bytes, should not fail |
| 648 | } |
| 649 | if hdr.Signature != gptSignature { |
| 650 | return nil, errors.New("no GPT signature found") |
| 651 | } |
| 652 | if hdr.HeaderSize < uint32(binary.Size(hdr)) { |
| 653 | return nil, fmt.Errorf("GPT header size is too small, likely corrupted") |
| 654 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 655 | if int64(hdr.HeaderSize) > r.BlockSize() { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 656 | return nil, fmt.Errorf("GPT header size is bigger than block size, likely corrupted") |
| 657 | } |
| 658 | // Use reserved bytes to hash, but do not expose them to the user. |
| 659 | // If someone has a need to process them, they should extend this library |
| 660 | // with whatever an updated UEFI specification contains. |
| 661 | // It has been considered to store these in the user-exposed GPT struct to |
| 662 | // be able to round-trip them cleanly, but there is significant complexity |
| 663 | // and risk involved in doing so. |
| 664 | reservedBytes := hdrBlock[binary.Size(hdr):hdr.HeaderSize] |
| 665 | hdrExpectedCRC := hdr.HeaderCRC32 |
| 666 | hdr.HeaderCRC32 = 0 |
| 667 | hdrCRC := crc32.NewIEEE() |
| 668 | binary.Write(hdrCRC, binary.LittleEndian, &hdr) |
| 669 | hdrCRC.Write(reservedBytes) |
| 670 | if hdrCRC.Sum32() != hdrExpectedCRC { |
| 671 | return nil, fmt.Errorf("GPT header checksum mismatch, probably corrupted") |
| 672 | } |
| 673 | if hdr.HeaderBlock != uint64(headerBlockPos) { |
| 674 | return nil, errors.New("GPT header indicates wrong block") |
| 675 | } |
| 676 | if hdr.PartitionEntrySize < uint32(binary.Size(partition{})) { |
| 677 | return nil, errors.New("partition entry size too small") |
| 678 | } |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 679 | if hdr.PartitionEntriesStartBlock > uint64(r.BlockCount()) { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 680 | return nil, errors.New("partition entry start block is out of range") |
| 681 | } |
| 682 | // Sanity-check total size of the partition entry area. Otherwise, this is a |
| 683 | // trivial DoS as it could cause allocation of gigabytes of memory. |
| 684 | // 4MiB is equivalent to around 45k partitions at the current size. |
| 685 | // I know of no operating system which would handle even a fraction of this. |
| 686 | if uint64(hdr.PartitionEntryCount)*uint64(hdr.PartitionEntrySize) > 4*1024*1024 { |
| 687 | return nil, errors.New("partition entry area bigger than 4MiB, refusing to read") |
| 688 | } |
| 689 | partitionEntryData := make([]byte, hdr.PartitionEntrySize*hdr.PartitionEntryCount) |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 690 | if _, err := r.ReadAt(partitionEntryData, r.BlockSize()*int64(hdr.PartitionEntriesStartBlock)); err != nil { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 691 | return nil, fmt.Errorf("failed to read partition entries: %w", err) |
| 692 | } |
| 693 | if crc32.ChecksumIEEE(partitionEntryData) != hdr.PartitionEntriesCRC32 { |
| 694 | return nil, errors.New("GPT partition entry table checksum mismatch") |
| 695 | } |
| 696 | var g Table |
Lorenz Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 697 | g.ID = msguid.To(hdr.ID) |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 698 | for i := uint32(0); i < hdr.PartitionEntryCount; i++ { |
| 699 | entryReader := bytes.NewReader(partitionEntryData[i*hdr.PartitionEntrySize : (i+1)*hdr.PartitionEntrySize]) |
| 700 | var part partition |
| 701 | if err := binary.Read(entryReader, binary.LittleEndian, &part); err != nil { |
| 702 | panic(err) // Should not happen |
| 703 | } |
| 704 | // If the partition type is the all-zero UUID, this slot counts as |
| 705 | // unused. |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 706 | if part.Type == uuid.Nil { |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 707 | g.Partitions = append(g.Partitions, nil) |
| 708 | continue |
| 709 | } |
| 710 | g.Partitions = append(g.Partitions, &Partition{ |
Lorenz Brun | 60d6b90 | 2023-06-20 16:02:40 +0200 | [diff] [blame] | 711 | ID: msguid.To(part.ID), |
| 712 | Type: msguid.To(part.Type), |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 713 | Name: strings.TrimRight(string(utf16.Decode(part.Name[:])), "\x00"), |
| 714 | FirstBlock: part.FirstBlock, |
| 715 | LastBlock: part.LastBlock, |
| 716 | Attributes: Attribute(part.Attributes), |
| 717 | }) |
| 718 | } |
| 719 | // Remove long list of nils at the end as it's inconvenient to work with |
| 720 | // (append doesn't work, debug prints are very long) and it round-trips |
| 721 | // correctly even without it as it gets zero-padded when writing anyway. |
| 722 | var maxValidPartition int |
| 723 | for i, p := range g.Partitions { |
| 724 | if !p.IsUnused() { |
| 725 | maxValidPartition = i |
| 726 | } |
| 727 | } |
| 728 | g.Partitions = g.Partitions[:maxValidPartition+1] |
Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 729 | g.b = r |
Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 730 | return &g, nil |
| 731 | } |