返回 DeepSeek-Reasonix
updater_linux_tree_test.go
根目录 / desktop / updater_linux_tree_test.go
1 package main
2
3 import (
4 "archive/tar"
5 "bytes"
6 "compress/gzip"
7 "math"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "reasonix/internal/installlayout"
14 )
15
16 func TestLinuxShellRejectsExpandedSizeOverflow(t *testing.T) {
17 var archive bytes.Buffer
18 gz := gzip.NewWriter(&archive)
19 tw := tar.NewWriter(gz)
20 if err := tw.WriteHeader(&tar.Header{Name: "reasonix", Mode: 0755, Size: 1}); err != nil {
21 t.Fatal(err)
22 }
23 if _, err := tw.Write([]byte("x")); err != nil {
24 t.Fatal(err)
25 }
26 if err := tw.WriteHeader(&tar.Header{Name: "app/large", Mode: 0644, Size: math.MaxInt64}); err != nil {
27 t.Fatal(err)
28 }
29 // The oversized member has no body: the limit must reject its header
30 // before attempting to copy, even when the summed sizes would overflow.
31 if err := gz.Close(); err != nil {
32 t.Fatal(err)
33 }
34 _, _, err := stageLinuxShellRelease(archive.Bytes(), t.TempDir())
35 if err == nil || !strings.Contains(err.Error(), "extraction limit") {
36 t.Fatalf("oversized header must fail before body copy, got %v", err)
37 }
38 }
39
40 func linuxShellArchive(t *testing.T, extra *tar.Header, omit string) []byte {
41 t.Helper()
42 var out bytes.Buffer
43 gz := gzip.NewWriter(&out)
44 tw := tar.NewWriter(gz)
45 names := append([]string{"reasonix-desktop", "reasonix", "reasonix-launcher", "reasonix-guard"}, installlayout.ShellRequiredNames("linux")...)
46 names = append(names, "app/chrome-sandbox", "app/locales/en-US.pak")
47 for _, name := range names {
48 if name == omit {
49 continue
50 }
51 data := []byte("new-" + name)
52 h := &tar.Header{Name: name, Mode: 0755, Size: int64(len(data)), Typeflag: tar.TypeReg}
53 if err := tw.WriteHeader(h); err != nil {
54 t.Fatal(err)
55 }
56 if _, err := tw.Write(data); err != nil {
57 t.Fatal(err)
58 }
59 }
60 if extra != nil {
61 if err := tw.WriteHeader(extra); err != nil {
62 t.Fatal(err)
63 }
64 }
65 if err := tw.Close(); err != nil {
66 t.Fatal(err)
67 }
68 if err := gz.Close(); err != nil {
69 t.Fatal(err)
70 }
71 return out.Bytes()
72 }
73
74 func TestLinuxShellUpdateAtomicallyPublishesResourcesAndLauncher(t *testing.T) {
75 root := t.TempDir()
76 archive := linuxShellArchive(t, nil, "")
77 for _, version := range []string{"v1.39.0", "v1.39.1"} {
78 if err := activateLinuxShellRelease(archive, version, root); err != nil {
79 t.Fatal(err)
80 }
81 }
82 ptr, err := installlayout.ReadCurrent(root)
83 if err != nil || ptr.ActiveVersion != "v1.39.1" {
84 t.Fatalf("pointer=%+v %v", ptr, err)
85 }
86 for _, name := range append(installlayout.ShellRequiredNames("linux"), "app/locales/en-US.pak", "reasonix-desktop", "reasonix") {
87 p := filepath.Join(root, "versions", ptr.ActiveVersion, filepath.FromSlash(name))
88 data, err := os.ReadFile(p)
89 if err != nil || string(data) != "new-"+name {
90 t.Errorf("bad %s: %v", name, err)
91 }
92 }
93 if _, err := os.Stat(filepath.Join(root, "reasonix-launcher")); err != nil {
94 t.Fatal(err)
95 }
96 }
97
98 func TestLinuxShellUpdateRejectsPartialOrUnsafeTreeWithoutMovingPointer(t *testing.T) {
99 cases := map[string]struct {
100 header *tar.Header
101 omit string
102 }{
103 "missing renderer": {nil, "app/resources/app.asar"},
104 "traversal": {&tar.Header{Name: "app/../outside", Typeflag: tar.TypeReg}, ""},
105 "symlink": {&tar.Header{Name: "app/link", Linkname: "/tmp", Typeflag: tar.TypeSymlink}, ""},
106 "duplicate": {&tar.Header{Name: "app/Reasonix", Typeflag: tar.TypeReg}, ""},
107 }
108 for name, tc := range cases {
109 t.Run(name, func(t *testing.T) {
110 root := t.TempDir()
111 if err := activateLinuxShellRelease(linuxShellArchive(t, nil, ""), "v1.39.0", root); err != nil {
112 t.Fatal(err)
113 }
114 if err := activateLinuxShellRelease(linuxShellArchive(t, tc.header, tc.omit), "v1.39.1", root); err == nil {
115 t.Fatal("accepted invalid release")
116 }
117 ptr, err := installlayout.ReadCurrent(root)
118 if err != nil || ptr.ActiveVersion != "v1.39.0" {
119 t.Fatalf("changed old pointer: %+v %v", ptr, err)
120 }
121 })
122 }
123 }
124
125 func TestLinuxShellExtractionCannotFollowPreexistingLinksOutsideStaging(t *testing.T) {
126 for _, name := range []string{"app", "app/resources", "reasonix-desktop"} {
127 t.Run(name, func(t *testing.T) {
128 staging, outside := t.TempDir(), t.TempDir()
129 sentinel := filepath.Join(outside, "keep")
130 if err := os.WriteFile(sentinel, []byte("original"), 0600); err != nil {
131 t.Fatal(err)
132 }
133 target := outside
134 if name == "reasonix-desktop" {
135 target = sentinel
136 }
137 link := filepath.Join(staging, filepath.FromSlash(name))
138 if err := os.MkdirAll(filepath.Dir(link), 0755); err != nil {
139 t.Fatal(err)
140 }
141 if err := os.Symlink(target, link); err != nil {
142 t.Skipf("symlinks unavailable: %v", err)
143 }
144 if _, _, err := stageLinuxShellRelease(linuxShellArchive(t, nil, ""), staging); err == nil {
145 t.Fatal("extraction accepted a symlink outside staging")
146 }
147 entries, err := os.ReadDir(outside)
148 if err != nil || len(entries) != 1 || entries[0].Name() != "keep" {
149 t.Fatalf("extraction created files outside staging: %v %v", entries, err)
150 }
151 data, err := os.ReadFile(sentinel)
152 if err != nil || string(data) != "original" {
153 t.Fatalf("extraction changed outside file: %q %v", data, err)
154 }
155 })
156 }
157 }
158
159 func TestLinuxShellExtractionRejectsEscapingPathsWithoutOutsideWrites(t *testing.T) {
160 parent := t.TempDir()
161 outside := filepath.Join(parent, "outside")
162 for _, name := range []string{"../outside", "app/../../outside", outside, `app\..\..\outside`} {
163 t.Run(name, func(t *testing.T) {
164 staging, err := os.MkdirTemp(parent, "staging-")
165 if err != nil {
166 t.Fatal(err)
167 }
168 archive := linuxShellArchive(t, &tar.Header{Name: name, Typeflag: tar.TypeReg}, "")
169 if _, _, err := stageLinuxShellRelease(archive, staging); err == nil {
170 t.Fatal("extraction accepted an escaping path")
171 }
172 if _, err := os.Stat(outside); !os.IsNotExist(err) {
173 t.Fatalf("extraction created an outside file: %v", err)
174 }
175 })
176 }
177 }
178
178 lines GO