|
1 | | -import { moveDirAtomic } from "builder-util" |
| 1 | +import { exec, moveDirAtomic } from "builder-util" |
2 | 2 | import { extractArchive, isSafeExtractPath } from "app-builder-lib/src/util/electronGet" |
| 3 | +import { getPath7za } from "app-builder-lib/src/toolsets/7zip" |
3 | 4 | import * as fs from "fs/promises" |
4 | 5 | import * as os from "os" |
5 | 6 | import * as path from "path" |
| 7 | +import * as tar from "tar" |
6 | 8 | import { afterEach, describe, test, vi } from "vitest" |
7 | 9 |
|
8 | 10 | afterEach(() => { |
@@ -151,6 +153,60 @@ describe("extractArchive ZIP security guards", () => { |
151 | 153 | }) |
152 | 154 | }) |
153 | 155 |
|
| 156 | +// ─── extractArchive .tar.7z (snap template layout) ──────────────────────────── |
| 157 | + |
| 158 | +/** |
| 159 | + * Crafts a .tar.7z archive that mirrors the snap-template-electron-*.tar.7z assets from |
| 160 | + * electron-builder-binaries: a 7z compression layer around a tar whose entries are all |
| 161 | + * "./"-prefixed, containing a mode-755 desktop-init.sh at the root plus a nested tree. |
| 162 | + */ |
| 163 | +async function createSnapTemplateLikeTar7z(workDir: string, archiveName: string): Promise<string> { |
| 164 | + const contentDir = path.join(workDir, "content") |
| 165 | + await fs.mkdir(path.join(contentDir, "usr", "share"), { recursive: true }) |
| 166 | + await fs.writeFile(path.join(contentDir, "desktop-init.sh"), "#!/bin/bash\ntrue\n", { mode: 0o755 }) |
| 167 | + await fs.writeFile(path.join(contentDir, "usr", "share", "marker.txt"), "ok") |
| 168 | + |
| 169 | + // Explicit "./"-prefixed entries make node-tar store "./"-prefixed names, matching the real |
| 170 | + // template tars, so tar.extract({ strip: 1 }) is what flattens them into the extraction root. |
| 171 | + const innerTar = path.join(workDir, archiveName.replace(/\.7z$/, "")) |
| 172 | + const entries = (await fs.readdir(contentDir)).map(e => `./${e}`) |
| 173 | + await tar.create({ file: innerTar, cwd: contentDir }, entries) |
| 174 | + |
| 175 | + const archivePath = path.join(workDir, archiveName) |
| 176 | + await exec(await getPath7za(), ["a", "-t7z", archivePath, innerTar]) |
| 177 | + return archivePath |
| 178 | +} |
| 179 | + |
| 180 | +describe("extractArchive .tar.7z", () => { |
| 181 | + // Regression test for https://github.com/electron-userland/electron-builder/issues/10002: |
| 182 | + // .tar.7z fell through to the plain .7z branch, so only the outer 7z layer was removed and the |
| 183 | + // extraction dir contained a single inner .tar instead of the template contents. Snap builds |
| 184 | + // then packed that tar as-is and the resulting snaps failed at launch (desktop-init.sh missing). |
| 185 | + test("extracts both layers of a .tar.7z, not just the outer 7z", async ({ expect, tmpDir }) => { |
| 186 | + const tmpDirPath = await tmpDir.createTempDir() |
| 187 | + const archivePath = await createSnapTemplateLikeTar7z(tmpDirPath, "snap-template-test-amd64.tar.7z") |
| 188 | + const extractDir = path.join(tmpDirPath, "out") |
| 189 | + |
| 190 | + await extractArchive(archivePath, extractDir) |
| 191 | + |
| 192 | + const entries = await fs.readdir(extractDir) |
| 193 | + // template contents must land at the extraction root... |
| 194 | + expect(entries).toContain("desktop-init.sh") |
| 195 | + expect(entries).toContain("usr") |
| 196 | + // ...and the inner tar must not be left behind as a file (the 26.15.x symptom) |
| 197 | + expect(entries.filter(e => e.endsWith(".tar"))).toEqual([]) |
| 198 | + |
| 199 | + expect(await fs.readFile(path.join(extractDir, "desktop-init.sh"), "utf-8")).toBe("#!/bin/bash\ntrue\n") |
| 200 | + expect(await fs.readFile(path.join(extractDir, "usr", "share", "marker.txt"), "utf-8")).toBe("ok") |
| 201 | + |
| 202 | + if (process.platform !== "win32") { |
| 203 | + // the template's launch scripts must stay executable, otherwise the snap dies at startup |
| 204 | + const stat = await fs.stat(path.join(extractDir, "desktop-init.sh")) |
| 205 | + expect(stat.mode & 0o100).toBeTruthy() |
| 206 | + } |
| 207 | + }) |
| 208 | +}) |
| 209 | + |
154 | 210 | // ─── isSafeExtractPath ──────────────────────────────────────────────────────── |
155 | 211 |
|
156 | 212 | describe("isSafeExtractPath", () => { |
|
0 commit comments