| Tim Windelschmidt | 6d33a43 | 2025-02-04 14:34:25 +0100 | [diff] [blame^] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 4 | package gpt |
| 5 | |
| 6 | import ( |
| 7 | "bytes" |
| 8 | "crypto/sha256" |
| 9 | "io" |
| 10 | "os" |
| Jan Schär | e479eee | 2024-08-21 16:01:39 +0200 | [diff] [blame] | 11 | "strings" |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 12 | "testing" |
| 13 | |
| 14 | "github.com/google/uuid" |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 15 | |
| Tim Windelschmidt | 9f21f53 | 2024-05-07 15:14:20 +0200 | [diff] [blame] | 16 | "source.monogon.dev/osbase/blockdev" |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 17 | ) |
| 18 | |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 19 | func TestFreeSpaces(t *testing.T) { |
| 20 | cases := []struct { |
| 21 | name string |
| 22 | parts []*Partition |
| 23 | expected [][2]int64 |
| 24 | expectedOverlap bool |
| 25 | }{ |
| 26 | {"Empty", []*Partition{}, [][2]int64{{34, 2015}}, false}, |
| 27 | {"OnePart", []*Partition{ |
| 28 | {Type: PartitionTypeEFISystem, FirstBlock: 200, LastBlock: 1499}, |
| 29 | }, [][2]int64{ |
| 30 | {34, 200}, |
| 31 | {1500, 2015}, |
| 32 | }, false}, |
| 33 | {"TwoOverlappingParts", []*Partition{ |
| 34 | {Type: PartitionTypeEFISystem, FirstBlock: 200, LastBlock: 1499}, |
| 35 | {Type: PartitionTypeEFISystem, FirstBlock: 1000, LastBlock: 1999}, |
| 36 | }, [][2]int64{ |
| 37 | {34, 200}, |
| 38 | {2000, 2015}, |
| 39 | }, true}, |
| 40 | {"Full", []*Partition{ |
| 41 | {Type: PartitionTypeEFISystem, FirstBlock: 34, LastBlock: 999}, |
| 42 | {Type: PartitionTypeEFISystem, FirstBlock: 1000, LastBlock: 2014}, |
| 43 | }, [][2]int64{}, false}, |
| 44 | {"TwoSpacedParts", []*Partition{ |
| 45 | {Type: PartitionTypeEFISystem, FirstBlock: 500, LastBlock: 899}, |
| 46 | {Type: PartitionTypeEFISystem, FirstBlock: 1200, LastBlock: 1799}, |
| 47 | }, [][2]int64{ |
| 48 | {34, 500}, |
| 49 | {900, 1200}, |
| 50 | {1800, 2015}, |
| 51 | }, false}, |
| 52 | } |
| 53 | |
| 54 | // Partitions are created manually as AddPartition calls FreeSpaces itself, |
| 55 | // which makes the test unreliable as well as making failures very hard to |
| 56 | // debug. |
| 57 | for _, c := range cases { |
| 58 | t.Run(c.name, func(t *testing.T) { |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 59 | d := blockdev.MustNewMemory(512, 2048) // 1MiB |
| 60 | g, err := New(d) |
| 61 | if err != nil { |
| 62 | panic(err) |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 63 | } |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 64 | g.Partitions = c.parts |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 65 | fs, overlap, err := g.GetFreeSpaces() |
| 66 | if err != nil { |
| 67 | t.Fatal(err) |
| 68 | } |
| 69 | if overlap != c.expectedOverlap { |
| 70 | t.Errorf("expected overlap %v, got %v", c.expectedOverlap, overlap) |
| 71 | } |
| 72 | if len(fs) != len(c.expected) { |
| 73 | t.Fatalf("expected %v, got %v", c.expected, fs) |
| 74 | } |
| 75 | for i := range fs { |
| 76 | if fs[i] != c.expected[i] { |
| 77 | t.Errorf("free space mismatch at pos %d: got [%d, %d), expected [%d, %d)", i, fs[i][0], fs[i][1], c.expected[i][0], c.expected[i][1]) |
| 78 | } |
| 79 | } |
| 80 | }) |
| 81 | } |
| 82 | } |
| 83 | |
| Jan Schär | e479eee | 2024-08-21 16:01:39 +0200 | [diff] [blame] | 84 | func TestAddPartition(t *testing.T) { |
| 85 | if os.Getenv("IN_KTEST") == "true" { |
| 86 | t.Skip("In ktest") |
| 87 | } |
| 88 | cases := []struct { |
| 89 | name string |
| 90 | parts []*Partition |
| 91 | addSize int64 |
| 92 | addOptions []AddOption |
| 93 | expectErr string |
| 94 | expectParts []*Partition |
| 95 | }{ |
| 96 | { |
| 97 | name: "empty-basic", |
| 98 | addSize: 9 * 512, |
| 99 | expectParts: []*Partition{{Name: "added", FirstBlock: 2048, LastBlock: 2048 + 9 - 1}}, |
| 100 | }, |
| 101 | { |
| 102 | name: "empty-fill", |
| 103 | addSize: -1, |
| Jan Schär | 02d7217 | 2024-09-02 17:47:27 +0200 | [diff] [blame] | 104 | expectParts: []*Partition{{Name: "added", FirstBlock: 2048, LastBlock: 5*2048 - 2048 - 1}}, |
| Jan Schär | e479eee | 2024-08-21 16:01:39 +0200 | [diff] [blame] | 105 | }, |
| 106 | { |
| 107 | name: "empty-end", |
| 108 | addSize: 9 * 512, |
| 109 | addOptions: []AddOption{WithPreferEnd()}, |
| 110 | expectParts: []*Partition{{Name: "added", FirstBlock: 4 * 2048, LastBlock: 4*2048 + 9 - 1}}, |
| 111 | }, |
| 112 | { |
| 113 | name: "empty-align0", |
| 114 | addSize: 9 * 512, |
| 115 | addOptions: []AddOption{WithAlignment(0)}, |
| 116 | expectErr: "must be positive", |
| 117 | }, |
| 118 | { |
| 119 | name: "empty-align512", |
| 120 | addSize: 9 * 512, |
| 121 | addOptions: []AddOption{WithAlignment(512)}, |
| 122 | expectParts: []*Partition{{Name: "added", FirstBlock: 2 + 16384/512, LastBlock: 2 + 16384/512 + 9 - 1}}, |
| 123 | }, |
| 124 | { |
| 125 | name: "empty-zero-size", |
| 126 | addSize: 0, |
| 127 | expectErr: "must be positive", |
| 128 | }, |
| 129 | { |
| 130 | name: "full-fill", |
| 131 | parts: []*Partition{{Name: "full", FirstBlock: 2048, LastBlock: 5*2048 - 16384/512 - 2}}, |
| 132 | addSize: -1, |
| 133 | expectErr: "no free space", |
| 134 | expectParts: []*Partition{{Name: "full", FirstBlock: 2048, LastBlock: 5*2048 - 16384/512 - 2}}, |
| 135 | }, |
| 136 | { |
| 137 | name: "haveone-basic", |
| 138 | parts: []*Partition{{Name: "one", FirstBlock: 2048, LastBlock: 2048 + 5}}, |
| 139 | addSize: 9 * 512, |
| 140 | expectParts: []*Partition{ |
| 141 | {Name: "one", FirstBlock: 2048, LastBlock: 2048 + 5}, |
| 142 | {Name: "added", FirstBlock: 2 * 2048, LastBlock: 2*2048 + 9 - 1}, |
| 143 | }, |
| 144 | }, |
| 145 | { |
| 146 | name: "havemiddle-basic", |
| 147 | parts: []*Partition{{Name: "middle", FirstBlock: 2 * 2048, LastBlock: 2*2048 + 5}}, |
| 148 | addSize: 9 * 512, |
| 149 | expectParts: []*Partition{ |
| 150 | {Name: "middle", FirstBlock: 2 * 2048, LastBlock: 2*2048 + 5}, |
| 151 | {Name: "added", FirstBlock: 2048, LastBlock: 2048 + 9 - 1}, |
| 152 | }, |
| 153 | }, |
| 154 | { |
| 155 | name: "havemiddle-end", |
| 156 | parts: []*Partition{{Name: "middle", FirstBlock: 2 * 2048, LastBlock: 2*2048 + 5}}, |
| 157 | addSize: 9 * 512, |
| 158 | addOptions: []AddOption{WithPreferEnd()}, |
| 159 | expectParts: []*Partition{ |
| 160 | {Name: "middle", FirstBlock: 2 * 2048, LastBlock: 2*2048 + 5}, |
| 161 | {Name: "added", FirstBlock: 4 * 2048, LastBlock: 4*2048 + 9 - 1}, |
| 162 | }, |
| 163 | }, |
| 164 | { |
| 165 | name: "end-aligned", |
| 166 | parts: []*Partition{{Name: "endalign", FirstBlock: 4 * 2048, LastBlock: 4*2048 + 8}}, |
| 167 | addSize: 2048 * 512, |
| 168 | addOptions: []AddOption{WithPreferEnd()}, |
| 169 | expectParts: []*Partition{ |
| 170 | {Name: "endalign", FirstBlock: 4 * 2048, LastBlock: 4*2048 + 8}, |
| 171 | {Name: "added", FirstBlock: 3 * 2048, LastBlock: 4*2048 - 1}, |
| 172 | }, |
| 173 | }, |
| 174 | { |
| 175 | name: "emptyslots-basic", |
| 176 | parts: []*Partition{ |
| 177 | {Name: "one", FirstBlock: 2048, LastBlock: 2048}, |
| 178 | nil, nil, |
| 179 | {Name: "two", FirstBlock: 2048 + 1, LastBlock: 2048 + 1}, |
| 180 | }, |
| 181 | addSize: 9 * 512, |
| 182 | expectParts: []*Partition{ |
| 183 | {Name: "one", FirstBlock: 2048, LastBlock: 2048}, |
| 184 | {Name: "added", FirstBlock: 2 * 2048, LastBlock: 2*2048 + 9 - 1}, |
| 185 | nil, |
| 186 | {Name: "two", FirstBlock: 2048 + 1, LastBlock: 2048 + 1}, |
| 187 | }, |
| 188 | }, |
| 189 | { |
| 190 | name: "emptyslots-keep", |
| 191 | parts: []*Partition{ |
| 192 | {Name: "one", FirstBlock: 2048, LastBlock: 2048}, |
| 193 | nil, nil, |
| 194 | {Name: "two", FirstBlock: 2048 + 1, LastBlock: 2048 + 1}, |
| 195 | }, |
| 196 | addSize: 9 * 512, |
| 197 | addOptions: []AddOption{WithKeepEmptyEntries()}, |
| 198 | expectParts: []*Partition{ |
| 199 | {Name: "one", FirstBlock: 2048, LastBlock: 2048}, |
| 200 | nil, nil, |
| 201 | {Name: "two", FirstBlock: 2048 + 1, LastBlock: 2048 + 1}, |
| 202 | {Name: "added", FirstBlock: 2 * 2048, LastBlock: 2*2048 + 9 - 1}, |
| 203 | }, |
| 204 | }, |
| 205 | } |
| 206 | |
| 207 | for _, c := range cases { |
| 208 | t.Run(c.name, func(t *testing.T) { |
| 209 | for _, part := range c.parts { |
| 210 | if part != nil { |
| 211 | part.Type = PartitionTypeEFISystem |
| 212 | } |
| 213 | } |
| 214 | addPart := &Partition{Name: "added", Type: PartitionTypeEFISystem} |
| 215 | d := blockdev.MustNewMemory(512, 5*2048) // 5MiB |
| 216 | g, err := New(d) |
| 217 | if err != nil { |
| 218 | panic(err) |
| 219 | } |
| 220 | g.Partitions = c.parts |
| 221 | err = g.AddPartition(addPart, c.addSize, c.addOptions...) |
| 222 | if (err == nil) != (c.expectErr == "") || (err != nil && !strings.Contains(err.Error(), c.expectErr)) { |
| 223 | t.Errorf("expected %q, got %v", c.expectErr, err) |
| 224 | } |
| 225 | _, overlap, err := g.GetFreeSpaces() |
| 226 | if err != nil { |
| 227 | t.Fatal(err) |
| 228 | } |
| 229 | if overlap { |
| 230 | t.Errorf("partitions overlap") |
| 231 | } |
| 232 | if len(g.Partitions) != len(c.expectParts) { |
| 233 | t.Fatalf("expected %+v, got %+v", c.expectParts, g.Partitions) |
| 234 | } |
| 235 | for i := range g.Partitions { |
| 236 | gotPart, wantPart := g.Partitions[i], c.expectParts[i] |
| 237 | if (gotPart == nil) != (wantPart == nil) { |
| 238 | t.Errorf("partition %d: got %+v, expected %+v", i, gotPart, wantPart) |
| 239 | } |
| 240 | if gotPart == nil || wantPart == nil { |
| 241 | continue |
| 242 | } |
| 243 | if gotPart.Name != wantPart.Name { |
| 244 | t.Errorf("partition %d: got Name %q, expected %q", i, gotPart.Name, wantPart.Name) |
| 245 | } |
| 246 | if gotPart.FirstBlock != wantPart.FirstBlock { |
| 247 | t.Errorf("partition %d: got FirstBlock %d, expected %d", i, gotPart.FirstBlock, wantPart.FirstBlock) |
| 248 | } |
| 249 | if gotPart.LastBlock != wantPart.LastBlock { |
| 250 | t.Errorf("partition %d: got LastBlock %d, expected %d", i, gotPart.LastBlock, wantPart.LastBlock) |
| 251 | } |
| 252 | } |
| 253 | }) |
| 254 | } |
| 255 | } |
| 256 | |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 257 | func TestRoundTrip(t *testing.T) { |
| 258 | if os.Getenv("IN_KTEST") == "true" { |
| 259 | t.Skip("In ktest") |
| 260 | } |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 261 | d := blockdev.MustNewMemory(512, 2048) // 1 MiB |
| 262 | |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 263 | g := Table{ |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 264 | ID: uuid.NewSHA1(uuid.Nil, []byte("test")), |
| 265 | BootCode: []byte("just some test code"), |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 266 | Partitions: []*Partition{ |
| 267 | nil, |
| 268 | // This emoji is very complex and exercises UTF16 surrogate encoding |
| 269 | // and composing. |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 270 | {Name: "Test 🏃♂️", FirstBlock: 10, LastBlock: 19, Type: PartitionTypeEFISystem, ID: uuid.NewSHA1(uuid.Nil, []byte("test1")), Attributes: AttrNoBlockIOProto}, |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 271 | nil, |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 272 | {Name: "Test2", FirstBlock: 20, LastBlock: 49, Type: PartitionTypeEFISystem, ID: uuid.NewSHA1(uuid.Nil, []byte("test2")), Attributes: 0}, |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 273 | }, |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 274 | b: d, |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 275 | } |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 276 | if err := g.Write(); err != nil { |
| 277 | t.Fatalf("Error while writing Table: %v", err) |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 278 | } |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 279 | |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 280 | originalHash := sha256.New() |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 281 | sr1 := io.NewSectionReader(d, 0, d.BlockSize()*d.BlockCount()) |
| 282 | if _, err := io.CopyBuffer(originalHash, sr1, make([]byte, d.OptimalBlockSize())); err != nil { |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 283 | panic(err) |
| 284 | } |
| 285 | |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 286 | g2, err := Read(d) |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 287 | if err != nil { |
| 288 | t.Fatalf("Failed to read back GPT: %v", err) |
| 289 | } |
| 290 | if g2.ID != g.ID { |
| 291 | t.Errorf("Disk UUID changed when reading back: %v", err) |
| 292 | } |
| 293 | // Destroy primary GPT |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 294 | d.Zero(1*d.BlockSize(), 5*d.BlockSize()) |
| 295 | g3, err := Read(d) |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 296 | if err != nil { |
| 297 | t.Fatalf("Failed to read back GPT with primary GPT destroyed: %v", err) |
| 298 | } |
| 299 | if g3.ID != g.ID { |
| 300 | t.Errorf("Disk UUID changed when reading back: %v", err) |
| 301 | } |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 302 | if err := g3.Write(); err != nil { |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 303 | t.Fatalf("Error while writing back GPT: %v", err) |
| 304 | } |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 305 | rewrittenHash := sha256.New() |
| Lorenz Brun | ad13188 | 2023-06-28 16:42:20 +0200 | [diff] [blame] | 306 | sr2 := io.NewSectionReader(d, 0, d.BlockSize()*d.BlockCount()) |
| 307 | if _, err := io.CopyBuffer(rewrittenHash, sr2, make([]byte, d.OptimalBlockSize())); err != nil { |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 308 | panic(err) |
| 309 | } |
| Tim Windelschmidt | 4beb06e | 2024-04-17 00:56:53 +0200 | [diff] [blame] | 310 | if !bytes.Equal(originalHash.Sum(nil), rewrittenHash.Sum(nil)) { |
| 311 | t.Errorf("Write/Read/Write test was not reproducible: %x != %x", originalHash.Sum(nil), rewrittenHash.Sum(nil)) |
| Lorenz Brun | ee17d83 | 2022-10-18 12:02:45 +0000 | [diff] [blame] | 312 | } |
| 313 | } |