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