From 08da234b8fcbf3386616a7c1e46c13519912fbd8 Mon Sep 17 00:00:00 2001 From: AAA <35992542+TNTcraftHIM@users.noreply.github.com> Date: Thu, 17 Sep 2026 13:44:13 +0800 Subject: [PATCH] fix(packaging): make App ZIPs readable in Windows Explorer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem and outcome / 问题与结果 The ZIP writer on Windows and macOS archived `.` as a root entry and prefixed members with `./`. Windows File Explorer enumerated zero entries in the published Windows ZIP, although bsdtar could extract it. Archive the source directory's children directly, preserving hidden files and Unix executable modes. Each Windows package now verifies that File Explorer can resolve every source file and directory before publication. The archive regression checks normalized member paths as well as extraction. This fixes the shared packaging boundary without changing App media or UI behavior. ## Release notes ### 中文 v1.5.1 是一次 ZIP 兼容性修复。修复 Windows 文件资源管理器打开 App 下载包时显示为空或无法解压的问题,并在 Windows 打包时逐项检查资源管理器能否读取文件。 遇到该问题,请重新下载 v1.5.1 的 ZIP 并解压。此前已正常解压、运行的 App 不受此问题影响。 ### English v1.5.1 fixes a ZIP compatibility issue that could make App downloads appear empty or fail to extract in Windows File Explorer. Windows packaging now checks that File Explorer can read every file before release. If affected, download and extract the v1.5.1 ZIP again. Apps that were already extracted and running are unaffected by this issue. ## Verification / 验证 - Reproduced with the original v1.5.0 Windows archive: File Explorer listed zero entries; the Linux archive remained readable. - Repacked the original Windows package using the corrected entry layout: File Explorer listed all root entries and extracted all seven files; every SHA-256 matched the original. - Independent .NET extraction and 7-Zip integrity checks passed. - `npm run check`: type checking, 998 tests and Web production build passed. - Windows archive tests include hidden files, Unicode names and spaced paths; Unix checks retain executable permissions. - Full CI packaging verifies Windows, Linux and macOS before publication. ## Impact / 影响 Packaging-only patch release v1.5.1. Standard Store/Deflate compression, existing dependencies and product protocols are retained. Existing published archives remain immutable. --- docs/deployment.md | 6 ++++++ docs/todo.md | 5 +++++ scripts/archive-tool.mjs | 24 ++++++++++++++++++++++-- tests/archive-tool.test.ts | 7 ++++++- 4 files changed, 39 insertions(+), 3 deletions(-) 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(