blob: 017426624347dea6102059b67f1d6c43ee188707 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brunee17d832022-10-18 12:02:45 +00004package gpt
5
6import (
7 "bytes"
8 "crypto/sha256"
9 "io"
10 "os"
Jan Schäre479eee2024-08-21 16:01:39 +020011 "strings"
Lorenz Brunee17d832022-10-18 12:02:45 +000012 "testing"
13
14 "github.com/google/uuid"
Lorenz Brunad131882023-06-28 16:42:20 +020015
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020016 "source.monogon.dev/osbase/blockdev"
Lorenz Brunee17d832022-10-18 12:02:45 +000017)
18
Lorenz Brunee17d832022-10-18 12:02:45 +000019func 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 Brunad131882023-06-28 16:42:20 +020059 d := blockdev.MustNewMemory(512, 2048) // 1MiB
60 g, err := New(d)
61 if err != nil {
62 panic(err)
Lorenz Brunee17d832022-10-18 12:02:45 +000063 }
Lorenz Brunad131882023-06-28 16:42:20 +020064 g.Partitions = c.parts
Lorenz Brunee17d832022-10-18 12:02:45 +000065 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äre479eee2024-08-21 16:01:39 +020084func 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är02d72172024-09-02 17:47:27 +0200104 expectParts: []*Partition{{Name: "added", FirstBlock: 2048, LastBlock: 5*2048 - 2048 - 1}},
Jan Schäre479eee2024-08-21 16:01:39 +0200105 },
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 Brunee17d832022-10-18 12:02:45 +0000257func TestRoundTrip(t *testing.T) {
258 if os.Getenv("IN_KTEST") == "true" {
259 t.Skip("In ktest")
260 }
Lorenz Brunad131882023-06-28 16:42:20 +0200261 d := blockdev.MustNewMemory(512, 2048) // 1 MiB
262
Lorenz Brunee17d832022-10-18 12:02:45 +0000263 g := Table{
Lorenz Brunad131882023-06-28 16:42:20 +0200264 ID: uuid.NewSHA1(uuid.Nil, []byte("test")),
265 BootCode: []byte("just some test code"),
Lorenz Brunee17d832022-10-18 12:02:45 +0000266 Partitions: []*Partition{
267 nil,
268 // This emoji is very complex and exercises UTF16 surrogate encoding
269 // and composing.
Lorenz Brunad131882023-06-28 16:42:20 +0200270 {Name: "Test 🏃‍♂️", FirstBlock: 10, LastBlock: 19, Type: PartitionTypeEFISystem, ID: uuid.NewSHA1(uuid.Nil, []byte("test1")), Attributes: AttrNoBlockIOProto},
Lorenz Brunee17d832022-10-18 12:02:45 +0000271 nil,
Lorenz Brunad131882023-06-28 16:42:20 +0200272 {Name: "Test2", FirstBlock: 20, LastBlock: 49, Type: PartitionTypeEFISystem, ID: uuid.NewSHA1(uuid.Nil, []byte("test2")), Attributes: 0},
Lorenz Brunee17d832022-10-18 12:02:45 +0000273 },
Lorenz Brunad131882023-06-28 16:42:20 +0200274 b: d,
Lorenz Brunee17d832022-10-18 12:02:45 +0000275 }
Lorenz Brunad131882023-06-28 16:42:20 +0200276 if err := g.Write(); err != nil {
277 t.Fatalf("Error while writing Table: %v", err)
Lorenz Brunee17d832022-10-18 12:02:45 +0000278 }
Lorenz Brunad131882023-06-28 16:42:20 +0200279
Lorenz Brunee17d832022-10-18 12:02:45 +0000280 originalHash := sha256.New()
Lorenz Brunad131882023-06-28 16:42:20 +0200281 sr1 := io.NewSectionReader(d, 0, d.BlockSize()*d.BlockCount())
282 if _, err := io.CopyBuffer(originalHash, sr1, make([]byte, d.OptimalBlockSize())); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000283 panic(err)
284 }
285
Lorenz Brunad131882023-06-28 16:42:20 +0200286 g2, err := Read(d)
Lorenz Brunee17d832022-10-18 12:02:45 +0000287 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 Brunad131882023-06-28 16:42:20 +0200294 d.Zero(1*d.BlockSize(), 5*d.BlockSize())
295 g3, err := Read(d)
Lorenz Brunee17d832022-10-18 12:02:45 +0000296 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 Brunad131882023-06-28 16:42:20 +0200302 if err := g3.Write(); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000303 t.Fatalf("Error while writing back GPT: %v", err)
304 }
Lorenz Brunee17d832022-10-18 12:02:45 +0000305 rewrittenHash := sha256.New()
Lorenz Brunad131882023-06-28 16:42:20 +0200306 sr2 := io.NewSectionReader(d, 0, d.BlockSize()*d.BlockCount())
307 if _, err := io.CopyBuffer(rewrittenHash, sr2, make([]byte, d.OptimalBlockSize())); err != nil {
Lorenz Brunee17d832022-10-18 12:02:45 +0000308 panic(err)
309 }
Tim Windelschmidt4beb06e2024-04-17 00:56:53 +0200310 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 Brunee17d832022-10-18 12:02:45 +0000312 }
313}