| Jan Schär | e4c4854 | 2025-03-20 08:39:10 +0000 | [diff] [blame] | 1 | // Copyright The Monogon Project Authors. |
| 2 | // SPDX-License-Identifier: Apache-2.0 |
| 3 | |
| 4 | package structfs_test |
| 5 | |
| 6 | import ( |
| 7 | "errors" |
| 8 | "io" |
| 9 | "io/fs" |
| 10 | "slices" |
| 11 | "syscall" |
| 12 | "testing" |
| 13 | "time" |
| 14 | |
| 15 | . "source.monogon.dev/osbase/structfs" |
| 16 | ) |
| 17 | |
| 18 | func TestOptions(t *testing.T) { |
| 19 | testTimestamp := time.Date(2022, 03, 04, 5, 6, 8, 0, time.UTC) |
| 20 | var tree Tree |
| 21 | tree.PlaceDir("dir", Tree{}, |
| 22 | WithModTime(testTimestamp), |
| 23 | WithPerm(0o700|fs.ModeSetuid|fs.ModeDevice), |
| 24 | WithSys("fakesys"), |
| 25 | ) |
| 26 | node := tree[0] |
| 27 | if node.ModTime != testTimestamp { |
| 28 | t.Errorf("Got ModTime %v, expected %v", node.ModTime, testTimestamp) |
| 29 | } |
| 30 | expectMode := 0o700 | fs.ModeSetuid | fs.ModeDir |
| 31 | if node.Mode != expectMode { |
| 32 | t.Errorf("Got Mode %s, expected %s", node.Mode, expectMode) |
| 33 | } |
| 34 | if node.Sys != "fakesys" { |
| 35 | t.Errorf("Got Sys %v, expected %v", node.Sys, "fakesys") |
| 36 | } |
| 37 | } |
| 38 | |
| 39 | func treeToStrings(t *testing.T, tree Tree) []string { |
| 40 | var out []string |
| 41 | for path, node := range tree.Walk() { |
| 42 | s := path + " " + node.Mode.String()[:1] |
| 43 | if node.Mode.IsRegular() { |
| 44 | content, err := node.Content.Open() |
| 45 | if err != nil { |
| 46 | t.Errorf("Failed to open %q: %v", path, err) |
| 47 | continue |
| 48 | } |
| 49 | b, err := io.ReadAll(content) |
| 50 | if err != nil { |
| 51 | t.Errorf("Failed to read %q: %v", path, err) |
| 52 | continue |
| 53 | } |
| 54 | s += " " + string(b) |
| 55 | content.Close() |
| 56 | } |
| 57 | out = append(out, s) |
| 58 | } |
| 59 | return out |
| 60 | } |
| 61 | |
| 62 | func TestWalk(t *testing.T) { |
| 63 | testCases := []struct { |
| 64 | desc string |
| 65 | tree Tree |
| 66 | expected []string |
| 67 | }{ |
| 68 | { |
| 69 | desc: "example", |
| 70 | tree: Tree{ |
| 71 | File("file1a", Bytes("content1a")), |
| 72 | Dir("dir1", Tree{ |
| 73 | File("file2", Bytes("content2")), |
| 74 | Dir("dir2", nil), |
| 75 | }), |
| 76 | File("file1b", Bytes("content1b")), |
| 77 | }, |
| 78 | expected: []string{ |
| 79 | "file1a - content1a", |
| 80 | "dir1 d", |
| 81 | "dir1/file2 - content2", |
| 82 | "dir1/dir2 d", |
| 83 | "file1b - content1b", |
| 84 | }, |
| 85 | }, |
| 86 | { |
| 87 | desc: "empty", |
| 88 | tree: nil, |
| 89 | expected: nil, |
| 90 | }, |
| 91 | { |
| 92 | desc: "ignore file children", |
| 93 | // Non-directories should not have children and Walk should ignore them. |
| 94 | tree: Tree{{ |
| 95 | Name: "file1", |
| 96 | Content: Bytes("content1"), |
| 97 | Children: Tree{ |
| 98 | File("file2", Bytes("content2")), |
| 99 | Dir("dir2", nil), |
| 100 | }, |
| 101 | }}, |
| 102 | expected: []string{ |
| 103 | "file1 - content1", |
| 104 | }, |
| 105 | }, |
| 106 | { |
| 107 | desc: "skip invalid name", |
| 108 | tree: Tree{ |
| 109 | File("", Bytes("invalid")), |
| 110 | File(".", Bytes("invalid")), |
| 111 | File("a/b", Bytes("invalid")), |
| 112 | File("file1a", Bytes("content1a")), |
| 113 | Dir("..", Tree{ |
| 114 | File("file2", Bytes("content2")), |
| 115 | Dir("dir2", nil), |
| 116 | }), |
| 117 | File("file1b", Bytes("content1b")), |
| 118 | }, |
| 119 | expected: []string{ |
| 120 | "file1a - content1a", |
| 121 | "file1b - content1b", |
| 122 | }, |
| 123 | }, |
| 124 | } |
| 125 | for _, tC := range testCases { |
| 126 | t.Run(tC.desc, func(t *testing.T) { |
| 127 | actual := treeToStrings(t, tC.tree) |
| 128 | if !slices.Equal(actual, tC.expected) { |
| 129 | t.Errorf("Walk result %v differs from expected %v", actual, tC.expected) |
| 130 | } |
| 131 | }) |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | func TestPlace(t *testing.T) { |
| 136 | testCases := []struct { |
| 137 | desc string |
| 138 | tree Tree |
| 139 | place func(*Tree) error |
| 140 | expected []string |
| 141 | err error |
| 142 | errPath string |
| 143 | }{ |
| 144 | { |
| 145 | desc: "file", |
| 146 | tree: Tree{ |
| 147 | File("file1a", Bytes("content1a")), |
| 148 | Dir("dir1", Tree{ |
| 149 | File("file2", Bytes("content2")), |
| 150 | Dir("dir2", nil), |
| 151 | }), |
| 152 | File("file1b", Bytes("content1b")), |
| 153 | }, |
| 154 | place: func(tree *Tree) error { |
| 155 | return tree.PlaceFile("dir1/dir3/file4", Bytes("content4")) |
| 156 | }, |
| 157 | expected: []string{ |
| 158 | "file1a - content1a", |
| 159 | "dir1 d", |
| 160 | "dir1/file2 - content2", |
| 161 | "dir1/dir2 d", |
| 162 | "dir1/dir3 d", |
| 163 | "dir1/dir3/file4 - content4", |
| 164 | "file1b - content1b", |
| 165 | }, |
| 166 | }, |
| 167 | { |
| 168 | desc: "dir", |
| 169 | tree: Tree{ |
| 170 | File("file1a", Bytes("content1a")), |
| 171 | Dir("dir1", Tree{ |
| 172 | File("file2", Bytes("content2")), |
| 173 | }), |
| 174 | }, |
| 175 | place: func(tree *Tree) error { |
| 176 | return tree.PlaceDir("dir1/dir3", Tree{ |
| 177 | File("file4", Bytes("content4")), |
| 178 | Dir("dir4", nil), |
| 179 | }) |
| 180 | }, |
| 181 | expected: []string{ |
| 182 | "file1a - content1a", |
| 183 | "dir1 d", |
| 184 | "dir1/file2 - content2", |
| 185 | "dir1/dir3 d", |
| 186 | "dir1/dir3/file4 - content4", |
| 187 | "dir1/dir3/dir4 d", |
| 188 | }, |
| 189 | }, |
| 190 | { |
| 191 | desc: "empty", |
| 192 | tree: nil, |
| 193 | place: func(tree *Tree) error { |
| 194 | return tree.PlaceFile("dir1/dir2/file3", Bytes("content")) |
| 195 | }, |
| 196 | expected: []string{ |
| 197 | "dir1 d", |
| 198 | "dir1/dir2 d", |
| 199 | "dir1/dir2/file3 - content", |
| 200 | }, |
| 201 | }, |
| 202 | { |
| 203 | desc: "root", |
| 204 | tree: Tree{ |
| 205 | File("file1", Bytes("content1")), |
| 206 | }, |
| 207 | place: func(tree *Tree) error { |
| 208 | return tree.PlaceFile("file2", Bytes("content2")) |
| 209 | }, |
| 210 | expected: []string{ |
| 211 | "file1 - content1", |
| 212 | "file2 - content2", |
| 213 | }, |
| 214 | }, |
| 215 | { |
| 216 | desc: "invalid path", |
| 217 | place: func(tree *Tree) error { |
| 218 | return tree.PlaceFile(".", Bytes("content")) |
| 219 | }, |
| 220 | err: fs.ErrInvalid, |
| 221 | errPath: ".", |
| 222 | }, |
| 223 | { |
| 224 | desc: "not a directory", |
| 225 | tree: Tree{ |
| 226 | Dir("dir1", Tree{ |
| 227 | File("file2", Bytes("content")), |
| 228 | }), |
| 229 | }, |
| 230 | place: func(tree *Tree) error { |
| 231 | return tree.PlaceFile("dir1/file2/dir3/file4", Bytes("content")) |
| 232 | }, |
| 233 | err: syscall.ENOTDIR, |
| 234 | errPath: "dir1/file2", |
| 235 | }, |
| 236 | { |
| 237 | desc: "already exists", |
| 238 | tree: Tree{ |
| 239 | Dir("dir1", Tree{ |
| 240 | Dir("dir2", nil), |
| 241 | }), |
| 242 | }, |
| 243 | place: func(tree *Tree) error { |
| 244 | return tree.PlaceDir("dir1/dir2", nil) |
| 245 | }, |
| 246 | err: fs.ErrExist, |
| 247 | errPath: "dir1/dir2", |
| 248 | }, |
| 249 | } |
| 250 | for _, tC := range testCases { |
| 251 | t.Run(tC.desc, func(t *testing.T) { |
| 252 | err := tC.place(&tC.tree) |
| 253 | if err != nil { |
| 254 | if tC.err == nil { |
| 255 | t.Fatalf("Place failed unexpectedly: %v", err) |
| 256 | } |
| 257 | if !errors.Is(err, tC.err) { |
| 258 | t.Errorf("Place failed with error %v, expected %v", err, tC.err) |
| 259 | } |
| 260 | var pe *fs.PathError |
| 261 | if !errors.As(err, &pe) { |
| 262 | t.Fatalf("Place(): error is %T, want *fs.PathError", err) |
| 263 | } |
| 264 | if pe.Path != tC.errPath { |
| 265 | t.Errorf("Place(): err.Path = %q, want %q", pe.Path, tC.errPath) |
| 266 | } |
| 267 | } else if tC.err != nil { |
| 268 | t.Error("Expected place to fail but it did not") |
| 269 | } else { |
| 270 | actual := treeToStrings(t, tC.tree) |
| 271 | if !slices.Equal(actual, tC.expected) { |
| 272 | t.Errorf("Result %v differs from expected %v", actual, tC.expected) |
| 273 | } |
| 274 | } |
| 275 | }) |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | func TestValidName(t *testing.T) { |
| 280 | isValidNameTests := []struct { |
| 281 | name string |
| 282 | ok bool |
| 283 | }{ |
| 284 | {"x", true}, |
| 285 | {"", false}, |
| 286 | {"..", false}, |
| 287 | {".", false}, |
| 288 | {"x/y", false}, |
| 289 | {"/", false}, |
| 290 | {"x/", false}, |
| 291 | {"/x", false}, |
| 292 | {`x\y`, true}, |
| 293 | } |
| 294 | for _, tt := range isValidNameTests { |
| 295 | ok := ValidName(tt.name) |
| 296 | if ok != tt.ok { |
| 297 | t.Errorf("ValidName(%q) = %v, want %v", tt.name, ok, tt.ok) |
| 298 | } |
| 299 | } |
| 300 | } |