| 1 | //go:build linux |
| 2 | |
| 3 | package host |
| 4 | |
| 5 | import ( |
| 6 | _ "embed" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "strconv" |
| 10 | "strings" |
| 11 | "testing" |
| 12 | ) |
| 13 | |
| 14 | // leo-climbing's real tree as metadata only (dev cohort-1, step 8 readiness): |
| 15 | // every path under /data, /opt/carlos, /etc/systemd/system and /etc/carlos |
| 16 | // on 2026-10-19 at 21:07Z, read-only over SSM with find -xdev -printf |
| 17 | // '<root>\t%y\t%u:%g\t%m\t%s\t%n\t%P\t%l\n'. No file contents. The /etc/carlos |
| 18 | // names that could be secrets are redacted. leo-units-post-8.1.tsv is what |
| 19 | // the window's step 8.1 changes; leo-routes.tsv is leo's one registry route. |
| 20 | var ( |
| 21 | //go:embed testdata/leo-step8/leo-tree-snapshot.tsv |
| 22 | leoTreeSnapshot string |
| 23 | //go:embed testdata/leo-step8/leo-units-post-8.1.tsv |
| 24 | leoUnitsPostWindow string |
| 25 | //go:embed testdata/leo-step8/leo-routes.tsv |
| 26 | leoRoutes string |
| 27 | ) |
| 28 | |
| 29 | type leoRow struct { |
| 30 | Root, Type, Owner string |
| 31 | Mode os.FileMode |
| 32 | Size int64 |
| 33 | Path, Target string |
| 34 | State string // post-8.1 rows only |
| 35 | } |
| 36 | |
| 37 | func parseLeoRows(t *testing.T, raw string) []leoRow { |
| 38 | t.Helper() |
| 39 | var rows []leoRow |
| 40 | for i, line := range strings.Split(strings.TrimRight(raw, "\n"), "\n") { |
| 41 | if i == 0 { |
| 42 | continue |
| 43 | } |
| 44 | f := strings.Split(line, "\t") |
| 45 | if len(f) < 8 { |
| 46 | t.Fatalf("snapshot line %d: %q", i+1, line) |
| 47 | } |
| 48 | mode, err := strconv.ParseUint(f[3], 8, 32) |
| 49 | if err != nil { |
| 50 | t.Fatalf("snapshot line %d mode: %v", i+1, err) |
| 51 | } |
| 52 | size, err := strconv.ParseInt(f[4], 10, 64) |
| 53 | if err != nil { |
| 54 | t.Fatal(err) |
| 55 | } |
| 56 | row := leoRow{Root: f[0], Type: f[1], Owner: f[2], Mode: unixMode(uint32(mode)), Size: size, Path: f[6], Target: f[7]} |
| 57 | if len(f) > 8 { |
| 58 | row.State = f[8] |
| 59 | } |
| 60 | rows = append(rows, row) |
| 61 | } |
| 62 | return rows |
| 63 | } |
| 64 | |
| 65 | func unixMode(m uint32) os.FileMode { |
| 66 | mode := os.FileMode(m & 0o777) |
| 67 | if m&0o4000 != 0 { |
| 68 | mode |= os.ModeSetuid |
| 69 | } |
| 70 | if m&0o2000 != 0 { |
| 71 | mode |= os.ModeSetgid |
| 72 | } |
| 73 | if m&0o1000 != 0 { |
| 74 | mode |= os.ModeSticky |
| 75 | } |
| 76 | return mode |
| 77 | } |
| 78 | |
| 79 | // leoTreeRows is the snapshot with step 8.1 applied: the twelve window units |
| 80 | // replaced by /dev/null links and the edge's wants link removed. The held |
| 81 | // originals (under /etc/carlos) are left out: the window directory is not a |
| 82 | // census root, and a witness makes its own. |
| 83 | func leoTreeRows(t *testing.T, window bool) []leoRow { |
| 84 | t.Helper() |
| 85 | rows := parseLeoRows(t, leoTreeSnapshot) |
| 86 | if !window { |
| 87 | return rows |
| 88 | } |
| 89 | post := map[string]leoRow{} |
| 90 | for _, r := range parseLeoRows(t, leoUnitsPostWindow) { |
| 91 | if r.Root == "/etc/systemd/system" { |
| 92 | post[r.Path] = r |
| 93 | } |
| 94 | } |
| 95 | var out []leoRow |
| 96 | for _, r := range rows { |
| 97 | if p, ok := post[r.Path]; ok && r.Root == "/etc/systemd/system" { |
| 98 | if p.State == "expected-post-8.1-absent" { |
| 99 | continue |
| 100 | } |
| 101 | out = append(out, p) |
| 102 | continue |
| 103 | } |
| 104 | out = append(out, r) |
| 105 | } |
| 106 | return out |
| 107 | } |
| 108 | |
| 109 | // leoTreeOptions maps snapshot roots to where a test builds them, and |
| 110 | // rewrites absolute link targets that point into a rebuilt root. |
| 111 | type leoTreeOptions struct { |
| 112 | Roots map[string]string // snapshot root -> directory |
| 113 | // Content gives the synthetic bytes for a file a reader needs; nil |
| 114 | // writes an empty file. |
| 115 | Content func(root, path string) []byte |
| 116 | // Owner, when set, applies a row's owner (root runner only). |
| 117 | Owner func(path, owner string) error |
| 118 | // Skip leaves a row out (and, for a directory, nothing below it is |
| 119 | // expected to be skipped by the caller). |
| 120 | Skip func(r leoRow) bool |
| 121 | } |
| 122 | |
| 123 | func buildLeoTree(t *testing.T, rows []leoRow, o leoTreeOptions) { |
| 124 | t.Helper() |
| 125 | type dirMode struct { |
| 126 | path string |
| 127 | mode os.FileMode |
| 128 | } |
| 129 | var dirs []dirMode |
| 130 | for _, r := range rows { |
| 131 | base, ok := o.Roots[r.Root] |
| 132 | if !ok || (o.Skip != nil && o.Skip(r)) { |
| 133 | continue |
| 134 | } |
| 135 | path := base |
| 136 | if r.Path != "." { |
| 137 | path = filepath.Join(base, r.Path) |
| 138 | } |
| 139 | switch r.Type { |
| 140 | case "d": |
| 141 | if r.Path != "." { |
| 142 | if err := os.Mkdir(path, 0o700); err != nil && !os.IsExist(err) { |
| 143 | t.Fatal(err) |
| 144 | } |
| 145 | } |
| 146 | dirs = append(dirs, dirMode{path, r.Mode}) |
| 147 | case "f": |
| 148 | var body []byte |
| 149 | if o.Content != nil { |
| 150 | body = o.Content(r.Root, r.Path) |
| 151 | } |
| 152 | if err := os.WriteFile(path, body, 0o600); err != nil { |
| 153 | t.Fatal(err) |
| 154 | } |
| 155 | if err := os.Chmod(path, r.Mode); err != nil { |
| 156 | t.Fatal(err) |
| 157 | } |
| 158 | case "l": |
| 159 | target := r.Target |
| 160 | for root, dir := range o.Roots { |
| 161 | if target == root || strings.HasPrefix(target, root+"/") { |
| 162 | target = dir + strings.TrimPrefix(target, root) |
| 163 | } |
| 164 | } |
| 165 | if err := os.Symlink(target, path); err != nil { |
| 166 | t.Fatal(err) |
| 167 | } |
| 168 | default: |
| 169 | t.Fatalf("snapshot type %q at %s", r.Type, r.Path) |
| 170 | } |
| 171 | if o.Owner != nil { |
| 172 | if err := o.Owner(path, r.Owner); err != nil { |
| 173 | t.Fatal(err) |
| 174 | } |
| 175 | } |
| 176 | } |
| 177 | // Directory modes last, deepest first, so a 0700 or 0750 parent does |
| 178 | // not stop the build of what is below it. |
| 179 | for i := len(dirs) - 1; i >= 0; i-- { |
| 180 | if err := os.Chmod(dirs[i].path, dirs[i].mode); err != nil { |
| 181 | t.Fatal(err) |
| 182 | } |
| 183 | } |
| 184 | } |
| 185 | |