| 1 | package installlayout |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "runtime" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | ) |
| 10 | |
| 11 | var shellTreeNames = []string{ |
| 12 | AppShellDirName + "/" + ShellExecutableName(), |
| 13 | AppShellDirName + "/resources/app.asar", |
| 14 | AppShellDirName + "/locales/en-US.pak", |
| 15 | } |
| 16 | |
| 17 | func treeMemberNames() []string { |
| 18 | return append(AllowedVersionMembers(), shellTreeNames...) |
| 19 | } |
| 20 | |
| 21 | func writeTreeMembers(t *testing.T, src, body string) []Member { |
| 22 | t.Helper() |
| 23 | names := treeMemberNames() |
| 24 | members := make([]Member, 0, len(names)) |
| 25 | for _, name := range names { |
| 26 | path := filepath.Join(src, filepath.FromSlash(name)) |
| 27 | if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | if err := os.WriteFile(path, []byte(body+"-"+name), 0o755); err != nil { |
| 31 | t.Fatal(err) |
| 32 | } |
| 33 | members = append(members, Member{Name: name, Path: path}) |
| 34 | } |
| 35 | return members |
| 36 | } |
| 37 | |
| 38 | func activateTree(t *testing.T, root, version, requestID string, members []Member) error { |
| 39 | t.Helper() |
| 40 | return ActivateVersion(ActivationRequest{ |
| 41 | InstallRoot: root, |
| 42 | Version: version, |
| 43 | RequestID: requestID, |
| 44 | Members: members, |
| 45 | RequiredNames: treeMemberNames(), |
| 46 | }) |
| 47 | } |
| 48 | |
| 49 | func TestActivateVersionPublishesAppShellTree(t *testing.T) { |
| 50 | root := t.TempDir() |
| 51 | src := t.TempDir() |
| 52 | if err := activateTree(t, root, "v1.30.0", "tree-publish", writeTreeMembers(t, src, "new")); err != nil { |
| 53 | t.Fatal(err) |
| 54 | } |
| 55 | versionDir := filepath.Join(root, VersionsDirName, "v1.30.0") |
| 56 | for _, name := range shellTreeNames { |
| 57 | raw, err := os.ReadFile(filepath.Join(versionDir, filepath.FromSlash(name))) |
| 58 | if err != nil || string(raw) != "new-"+name { |
| 59 | t.Fatalf("tree member %s = %q err=%v", name, raw, err) |
| 60 | } |
| 61 | } |
| 62 | desktop, err := ActiveDesktopPath(root) |
| 63 | if err != nil || filepath.Base(desktop) != DesktopBinaryName() { |
| 64 | t.Fatalf("active desktop = %s err=%v", desktop, err) |
| 65 | } |
| 66 | relaunch, err := StableRelaunchPath(root) |
| 67 | if err != nil || relaunch != desktop { |
| 68 | t.Fatalf("stable relaunch = %s err=%v, want %s", relaunch, err, desktop) |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | func TestActivateVersionTreeFailureKeepsOldPointerAndTree(t *testing.T) { |
| 73 | root := t.TempDir() |
| 74 | if err := activateTree(t, root, "v1.30.0", "tree-seed", writeTreeMembers(t, t.TempDir(), "old")); err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | badSrc := t.TempDir() |
| 78 | members := writeTreeMembers(t, badSrc, "new") |
| 79 | asar := filepath.Join(badSrc, AppShellDirName, "resources", "app.asar") |
| 80 | if err := os.Remove(asar); err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | if err := os.Mkdir(asar, 0o755); err != nil { |
| 84 | t.Fatal(err) |
| 85 | } |
| 86 | if err := activateTree(t, root, "v1.31.0", "tree-fault", members); err == nil { |
| 87 | t.Fatal("expected activation failure for a non-regular tree source") |
| 88 | } |
| 89 | ptr, err := ReadCurrent(root) |
| 90 | if err != nil || ptr.ActiveVersion != "v1.30.0" { |
| 91 | t.Fatalf("pointer=%+v err=%v", ptr, err) |
| 92 | } |
| 93 | if _, err := os.Stat(filepath.Join(root, VersionsDirName, "v1.31.0")); !os.IsNotExist(err) { |
| 94 | t.Fatalf("failed version directory survived: %v", err) |
| 95 | } |
| 96 | shell := filepath.Join(root, VersionsDirName, "v1.30.0", AppShellDirName, ShellExecutableName()) |
| 97 | if raw, err := os.ReadFile(shell); err != nil || !strings.HasPrefix(string(raw), "old-") { |
| 98 | t.Fatalf("active shell = %q err=%v", raw, err) |
| 99 | } |
| 100 | entries, err := os.ReadDir(filepath.Join(root, VersionsDirName)) |
| 101 | if err != nil { |
| 102 | t.Fatal(err) |
| 103 | } |
| 104 | for _, entry := range entries { |
| 105 | if strings.HasPrefix(entry.Name(), ".staging-") { |
| 106 | t.Fatalf("staging directory %s survived the failed activation", entry.Name()) |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | func TestActivateVersionRejectsTreeMembersOutsideWhitelist(t *testing.T) { |
| 112 | root := t.TempDir() |
| 113 | members := writeTreeMembers(t, t.TempDir(), "x") |
| 114 | if err := ActivateVersion(ActivationRequest{ |
| 115 | InstallRoot: root, |
| 116 | Version: "v1.30.0", |
| 117 | RequestID: "tree-default-list", |
| 118 | Members: members, |
| 119 | }); err == nil { |
| 120 | t.Fatal("default whitelist must reject app/ members") |
| 121 | } |
| 122 | flat := members[:len(AllowedVersionMembers())] |
| 123 | if err := ActivateVersion(ActivationRequest{ |
| 124 | InstallRoot: root, |
| 125 | Version: "v1.30.0", |
| 126 | RequestID: "tree-root", |
| 127 | Members: flat, |
| 128 | RootMembers: []Member{{Name: shellTreeNames[0], Path: members[len(flat)].Path}}, |
| 129 | RequiredRootNames: []string{shellTreeNames[0]}, |
| 130 | }); err == nil { |
| 131 | t.Fatal("root entries must stay flat") |
| 132 | } |
| 133 | if HasCurrent(root) { |
| 134 | t.Fatal("current.json must not exist after rejected activations") |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | func TestActivateVersionRejectsTreeTraversalNames(t *testing.T) { |
| 139 | root := t.TempDir() |
| 140 | src := t.TempDir() |
| 141 | members := writeTreeMembers(t, src, "x") |
| 142 | for _, bad := range []string{ |
| 143 | "../" + AppShellDirName + "/x", |
| 144 | AppShellDirName + "/../" + DesktopBinaryName(), |
| 145 | AppShellDirName + "/./x", |
| 146 | AppShellDirName + "//x", |
| 147 | AppShellDirName + "/x/", |
| 148 | AppShellDirName + `\x`, |
| 149 | "/" + AppShellDirName + "/x", |
| 150 | "lib/x", |
| 151 | AppShellDirName + "/c:x", |
| 152 | " " + AppShellDirName + "/x", |
| 153 | } { |
| 154 | req := ActivationRequest{ |
| 155 | InstallRoot: root, |
| 156 | Version: "v1.30.0", |
| 157 | RequestID: "tree-traversal", |
| 158 | Members: append(append([]Member(nil), members...), Member{Name: bad, Path: members[0].Path}), |
| 159 | RequiredNames: append(treeMemberNames(), bad), |
| 160 | } |
| 161 | if err := ActivateVersion(req); err == nil { |
| 162 | t.Fatalf("member name %q was accepted", bad) |
| 163 | } |
| 164 | if err := ValidateMemberName(bad); err == nil { |
| 165 | t.Fatalf("ValidateMemberName(%q) accepted", bad) |
| 166 | } |
| 167 | } |
| 168 | for _, good := range append([]string{DesktopBinaryName()}, shellTreeNames...) { |
| 169 | if err := ValidateMemberName(good); err != nil { |
| 170 | t.Fatalf("ValidateMemberName(%q) = %v", good, err) |
| 171 | } |
| 172 | } |
| 173 | if HasCurrent(root) { |
| 174 | t.Fatal("current.json must not exist after rejected activations") |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | func TestActivateVersionRejectsTreeSymlinkSource(t *testing.T) { |
| 179 | if runtime.GOOS == "windows" { |
| 180 | t.Skip("symlink privilege varies on Windows CI") |
| 181 | } |
| 182 | root := t.TempDir() |
| 183 | src := t.TempDir() |
| 184 | members := writeTreeMembers(t, src, "x") |
| 185 | shell := filepath.Join(src, AppShellDirName, ShellExecutableName()) |
| 186 | real := writeTempMember(t, src, "real-shell", "body") |
| 187 | if err := os.Remove(shell); err != nil { |
| 188 | t.Fatal(err) |
| 189 | } |
| 190 | if err := os.Symlink(real, shell); err != nil { |
| 191 | t.Fatal(err) |
| 192 | } |
| 193 | if err := activateTree(t, root, "v1.30.0", "tree-symlink", members); err == nil { |
| 194 | t.Fatal("expected symlink tree source rejection") |
| 195 | } |
| 196 | if HasCurrent(root) { |
| 197 | t.Fatal("current.json must not be written after failed activation") |
| 198 | } |
| 199 | } |
| 200 |