diff --git a/docs/deployment.md b/docs/deployment.md index 70276e0..9c6b9d9 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -100,6 +100,12 @@ The wrapper downloads the pinned public-link sidecar, verifies its digest, builds the target App and available capture process, executes every packaged runtime from the extracted ZIP, and emits one `.zip` plus its SHA-256 file. +App ZIPs use ordinary Store/Deflate compression and relative entry names without +`.` or `..` path segments; include hidden files and preserve executable modes on +Unix. Windows packaging also checks every entry through Windows Explorer's ZIP +reader. A successful extraction by the writer's own library does not establish +compatibility with the user's default archive tool. + Retain the descriptor and successful deployment output as release metadata. Do not create a follow-up source commit solely to duplicate their revision, asset, or hashes. diff --git a/docs/todo.md b/docs/todo.md index 2816fee..4853e73 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -14,6 +14,11 @@ deployments remain separate. Physical limits are owned by ## Awaiting Device Or Reporter Evidence +- [ ] **Brief connection followed by repeated Viewer loss on v1.5.0.** One + Viewer reportedly drops just after connection details appear, while other + Viewers work; App public-link mode is suspected. Check signaling, first-frame + admission and current-edge recovery separately. The affected environment and + paired diagnostics are unavailable; no network or product cause is established. - [ ] **App source-discovery field acceptance.** Retest the missing-window report with the current App and page. Obtain OS/browser, version and paired Debug reports to distinguish discovery, capture capability and enumeration failures. diff --git a/scripts/archive-tool.mjs b/scripts/archive-tool.mjs index ddfc3e9..358b2c1 100644 --- a/scripts/archive-tool.mjs +++ b/scripts/archive-tool.mjs @@ -1,5 +1,5 @@ import { execFileSync } from "node:child_process"; -import { existsSync } from "node:fs"; +import { existsSync, readdirSync } from "node:fs"; import { join, resolve } from "node:path"; export function tarExecutable() { @@ -15,10 +15,30 @@ export function createZip(source, archive) { if (process.platform === "linux") { execFileSync("zip", ["-qry", archive, "."], { cwd: source, stdio: "pipe" }); } else { - execFileSync(tarExecutable(), ["--format=zip", "-cf", archive, "."], { + // Archiving "." adds ./ members that Windows Explorer treats as an empty ZIP. + execFileSync(tarExecutable(), ["--format=zip", "-cf", archive, "--", ...readdirSync(source).sort()], { cwd: source, stdio: "pipe", windowsHide: true, }); } + if (process.platform === "win32") { + // The writer can read its own incompatible ZIPs. Check Explorer separately, + // including nested helpers and hidden files, before publishing the archive. + execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", ` +$ErrorActionPreference = 'Stop' +$shell = New-Object -ComObject Shell.Application +function Check-Folder($folder, [string]$source) { + if ($null -eq $folder) { throw 'Windows Explorer cannot open the ZIP folder' } + foreach ($entry in Get-ChildItem -LiteralPath $source -Force) { + $item = $folder.ParseName($entry.Name) + if ($null -eq $item) { throw "Windows Explorer cannot read ZIP entry: $($entry.Name)" } + if ($entry.PSIsContainer) { Check-Folder $item.GetFolder $entry.FullName } + } +} +Check-Folder $shell.NameSpace($env:PIIK_ZIP_ARCHIVE) $env:PIIK_ZIP_SOURCE +`], { stdio: "pipe", windowsHide: true, timeout: 30_000, + env: { ...process.env, PIIK_ZIP_ARCHIVE: archive, PIIK_ZIP_SOURCE: resolve(source) }, + }); + } } export function extractZip(archive, destination) { diff --git a/tests/archive-tool.test.ts b/tests/archive-tool.test.ts index 62dfd42..05d4582 100644 --- a/tests/archive-tool.test.ts +++ b/tests/archive-tool.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { describe, expect, it } from "vitest"; -import { createZip, extractZip } from "../scripts/archive-tool.mjs"; +import { createZip, extractZip, tarExecutable } from "../scripts/archive-tool.mjs"; describe("App ZIP distribution", () => { it("round-trips hidden files, spaced paths and executable permissions", () => { @@ -28,6 +28,11 @@ describe("App ZIP distribution", () => { for (const name of executables) chmodSync(join(source, name), 0o755); createZip(source, archive); expect(readFileSync(archive).subarray(0, 4)).toEqual(Buffer.from([0x50, 0x4b, 0x03, 0x04])); + const entries = execFileSync(process.platform === "linux" ? "unzip" : tarExecutable(), + process.platform === "linux" ? ["-Z", "-1", archive] : ["-tf", archive], + { encoding: "utf8", windowsHide: true }).trim().split(/\r?\n/); + expect(entries).toHaveLength(readdirSync(source, { recursive: true }).length); + for (const name of entries) expect(name).not.toMatch(/(^|\/)\.\.?($|\/)/); mkdirSync(extracted); extractZip(archive, extracted); expect(readdirSync(extracted, { recursive: true }).sort()).toEqual(