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