From 857d15e6395a3706b36995cad48c5ce67862be89 Mon Sep 17 00:00:00 2001 From: theferry Date: Sat, 24 Oct 2026 13:05:21 +0800 Subject: [PATCH] Fix widget ZIP validation and provide manual 1.1.4 recovery package Validate release ZIP headers directly instead of parsing zipinfo text. Keep existing hash, size, path, file-type and expansion limits. Preserve published 1.1.2/1.1.3 packages and the live update feed. --- README.md | 10 +++ SOURCE-SHA256.json | 14 +++-- docs/widget-update-1.1.4.md | 11 ++++ downloads/GPU-Queue-Widget-1.1.4-macOS.zip | Bin 0 -> 29073 bytes downloads/SHA256SUMS | 1 + launchers/macos/widget/Updater.js | 26 +++----- launchers/macos/widget/release.json | 8 +-- launchers/macos/widget/update-model.js | 69 ++++++++++++++++++++- tests/widget-update.test.cjs | 26 ++++++++ 9 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 docs/widget-update-1.1.4.md create mode 100644 downloads/GPU-Queue-Widget-1.1.4-macOS.zip diff --git a/README.md b/README.md index 96ef73a..d0b055e 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,16 @@ [下载 macOS 桌面小组件 1.1.3](https://raw.githubusercontent.com/Ferryyer/gpu-queue/main/downloads/GPU-Queue-Widget-1.1.3-macOS.zip) · [校验和](downloads/SHA256SUMS) +## 1.1.2 更新报“安装包文件清单不符” + +可手动安装 [1.1.4 修复包](https://raw.githubusercontent.com/Ferryyer/gpu-queue/codex/mf-8-widget-update-fix/downloads/GPU-Queue-Widget-1.1.4-macOS.zip)。 +退出小组件及更新窗口,完整解压 ZIP,双击“安装桌面小组件.command”覆盖安装。 +安装保留 SSH 配置、窗口位置和偏好;服务器任务继续运行。旧更新器在安装新版前就会报错,因此需要这一次手动覆盖。 + +1.1.4 直接校验 ZIP 中央目录与文件头,保留下载大小、SHA-256、精确文件白名单和文件类型/大小检查,不再解析 macOS `unzip` 输出。 +修复包目前在上述审阅分支和 issue 附件,正式更新入口仍为 1.1.3;未改动已发布的旧版包。 +验证与限制见 [更新修复说明](docs/widget-update-1.1.4.md)。 + ## 安装桌面小组件 完整解压 `downloads/GPU-Queue-Widget-1.1.3-macOS.zip`,双击“安装桌面小组件.command”。如果旧更新器报“安装包文件路径无效”,先退出旧面板和更新窗口,再覆盖安装一次。 diff --git a/SOURCE-SHA256.json b/SOURCE-SHA256.json index e33c04a..f19252f 100644 --- a/SOURCE-SHA256.json +++ b/SOURCE-SHA256.json @@ -1,13 +1,15 @@ { ".gitignore": "1364e59c28568273da17a19c4b4fb78dd86aaed90f6b7e71d7823585bba84707", ".openai/hosting.json": "8e1049aefe1f5e5853c8bd13a3e4e1b01f9251a452bfd8b5e384a260f4a551b7", - "README.md": "f6cc6f56f7ca81b7a80cf5359a5849216d620fa5719910aee6a8e11289d289be", + "README.md": "fd412097efe493ac38ada05adf4b9190d90e51e4b289f63b51c80830b0f8b81a", "docs/OPERATIONS.md": "f3bf36698f26eb470016f5ed7b64fa174e8b9422a56375a0ccd9efae6b01ab29", "docs/typography-review.md": "57fc36226bcf1b0cff78acc9ce25c63a57e89ca739161f3c2e959f6ec35b4aa7", + "docs/widget-update-1.1.4.md": "e086a0cb5d7123b32aa472a9afa7c42c0c2c645224ccd67decacd93b535c3297", "downloads/GPU-Queue-Widget-1.1.2-macOS.zip": "77b1652f46675cf7f0b57616411df8d66124fbd2da499a270e1ad246c7902bff", "downloads/GPU-Queue-Widget-1.1.3-macOS.zip": "25d8a432737623a950a65115b1bb6b0f72bbfbbd86c783842a93a245a2ea4be1", + "downloads/GPU-Queue-Widget-1.1.4-macOS.zip": "7f1e6d089b2c087ebebb2ab68a2b796fbfdbee3a43ba97eedd2fe401e3a49b70", "downloads/GPU-Queue-Widget-macOS.zip": "25d8a432737623a950a65115b1bb6b0f72bbfbbd86c783842a93a245a2ea4be1", - "downloads/SHA256SUMS": "7d0dd4eca32d576acab9c20e1f7272506587be5cced5c231ae9ae12bf4395dd5", + "downloads/SHA256SUMS": "87234a735f5ad44d66d44bfbc9c72567be1fded0849b19b68a89e1689eab5ccd", "drizzle/0000_gpuq.sql": "0580c9c8e8c8bf6aece28a1d5911941828cd0c85cf10e83a11687f19d1475310", "drizzle/meta/_journal.json": "f5beaf051c66e5b4c8b52b45c804fb37cc01c8cfe572de8303e155d68c99f531", "examples/acceptance.py": "3e41c27096177b5b552fb1466ceb4d40c2ca9cfcc6509549c1e05c2ff80b991a", @@ -27,12 +29,12 @@ "launchers/macos/widget/Disable-Autostart.command": "66f7fa2765054707b037f7f0e9700d1bf74934bbaeaca72a40dec62dd64028fa", "launchers/macos/widget/Enable-Autostart.command": "27c3e63c0bbc27321f3cd5bdabfadf628476ce706bcd14f26d0598fb3649bd41", "launchers/macos/widget/Install.command": "13255aca0302fbd8b6eca7c0ef937e90181c8019c857d1e9d88f392ee78e7381", - "launchers/macos/widget/Updater.js": "e638d64d3079aaaadffd171727055f288290eae0486d96ca3d6cc84b7ff44aca", + "launchers/macos/widget/Updater.js": "673b45e7e89f8c0aacb7b93c8441c303f4d418883f59a3c43346f7b10aeb1e93", "launchers/macos/widget/Widget.js": "69c8d248615db0c764b85b58702e5259d59739cae36ec4e1cd1f30ded685f9e1", "launchers/macos/widget/model.js": "a47bd27263c05efae63d4c463009a615598853897d4be5ba9caa998437015ac5", "launchers/macos/widget/native.js": "f8c352126391697fdecd9ce21911856290a0b0030308d9a61cd8c85c569c2c68", - "launchers/macos/widget/release.json": "200658c6ebe387443088836031b79d69e9cbfea3a5f7971f448aefc3b3258760", - "launchers/macos/widget/update-model.js": "aeb688466bd70a93e1eb8e4a98968440fb2c52bea89065fd879fb1e122d42768", + "launchers/macos/widget/release.json": "e93eee0040e00742b9e4c6ce96f6a870308d2a7a02b6ed117d47fcb34ee9d656", + "launchers/macos/widget/update-model.js": "c34b6d943fbfb049465f85fe8b70223435b3e0988cde6e2c494fea3a113ed683", "package.json": "5366aaa1577e39588b077e7c3026a1dfca9303f3f07d629ae505acdf64b34bf6", "scripts/build.mjs": "d04fab925b83222807df50f6b820be8706cf354ed83e961d511af7a912fe5bc5", "scripts/package-launcher.py": "4141672b119cd3bbd82d6fce2a1cc7c005f994cc5034907da41eea8e0dc02bbe", @@ -54,7 +56,7 @@ "tests/test_stop.py": "39717a7e08eb2bf7c1e2b9b5416ed335bc71c0760210091fd3c2d70ffbf977cd", "tests/test_widget_autostart.py": "7819f157ca95931554a4aba9e4b0c195f99b10e21499b70e8eb9b0aa987bef40", "tests/test_widget_install.py": "bb0c80f1e2d8bb2fa685a51d8d5d9bebd05b4effc485e00d971618facda02b5c", - "tests/widget-update.test.cjs": "7a3a3f5c9e7dd76c0e54968f09bbe48f9cdbf4ad959d46b46eddb3b8e2d48cb5", + "tests/widget-update.test.cjs": "846ebaf0550e0899e5723f939fe9fb2965ac91030ab761f19be50a576bbe02a5", "tests/widget.test.cjs": "21dde14aa712b8f3c814354af8a1323e6c7f92af866169e78dd9b4095f501faf", "updates/gpu-queue-widget.json": "4ff3b46935ab67689c962cc22ceb3155960ff6de7572ff3435f15c481580a475", "web/app.js": "9401da1be8229a99dc311c9e83df76d760a8eb553ac87269e25d87e2c3903ad0", diff --git a/docs/widget-update-1.1.4.md b/docs/widget-update-1.1.4.md new file mode 100644 index 0000000..4a3f91b --- /dev/null +++ b/docs/widget-update-1.1.4.md @@ -0,0 +1,11 @@ +## 1.1.2 清单错误的修复 + +用户在旧更新器中遇到“安装包文件清单不符”。源码中此错误表示 `unzip -Z -1` 输出分行后的数量不等于 9。已核对 GitHub 当前更新入口为 1.1.3,本地的 1.1.2/1.1.3 发布 ZIP 均包含预期 9 个文件。尚未取得 Mac 上的原始清单输出,无法确定多出或缺少行的具体原因。 + +1.1.4 从已通过 SHA-256 和字节数校验的 NSData 读取 Base64,在共享 ES5 校验器中直接检查 ZIP 中央目录与各本地文件头。不再把 zipinfo 的展示文本作为安装清单。支持本项目打包器产生的 UTF-8、deflate、Unix 普通文件;拒绝 ZIP64、多卷、加密、数据描述符、重复/额外路径、符号链接、路径或大小冲突、重叠/隐藏条目和超限展开大小。后续仍由系统 ditto 解压。 + +旧的 1.1.2 更新器不会因下载新包就自动获得新的校验逻辑。请退出小组件和更新窗口,完整解压 1.1.4 包,双击“安装桌面小组件.command”覆盖一次。保留 SSH 配置、窗口位置和自启动偏好,不停止服务器任务。无需卸载原应用或删除配置。 + +修复包位于本分支 downloads/GPU-Queue-Widget-1.1.4-macOS.zip 及 issue 附件。保留正式更新入口与别名包为已发布的 1.1.3,不修改旧版不可变包。发布人员后续可按现有流程将新版本包、别名、SHA256SUMS 与生成的更新信息一并发布到 Ferryyer/Ferryyer;本次没有修改线上更新入口。 + +验证:28 项 Node 测试(包含 ZIP 实包及损坏结构检查),10 项安装/自启动 CPU 测试,JXA JavaScript 语法和安装脚本语法检查。新二进制校验器接受现有 1.1.2/1.1.3 及新 1.1.4 实包;错误路径、本地/中央记录冲突、重复条目、符号链接、截断和过大条目被拒绝。Linux 环境未执行 Mac AppKit 与真实安装验收。 diff --git a/downloads/GPU-Queue-Widget-1.1.4-macOS.zip b/downloads/GPU-Queue-Widget-1.1.4-macOS.zip new file mode 100644 index 0000000000000000000000000000000000000000..928c50ca6a641cc5069e7de958fed5300d749391 GIT binary patch literal 29073 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 I!@qX_15(Y23jhEB literal 0 HcmV?d00001 diff --git a/downloads/SHA256SUMS b/downloads/SHA256SUMS index 03a52f2..ab2c1fd 100644 --- a/downloads/SHA256SUMS +++ b/downloads/SHA256SUMS @@ -1,3 +1,4 @@ 77b1652f46675cf7f0b57616411df8d66124fbd2da499a270e1ad246c7902bff GPU-Queue-Widget-1.1.2-macOS.zip 25d8a432737623a950a65115b1bb6b0f72bbfbbd86c783842a93a245a2ea4be1 GPU-Queue-Widget-1.1.3-macOS.zip 25d8a432737623a950a65115b1bb6b0f72bbfbbd86c783842a93a245a2ea4be1 GPU-Queue-Widget-macOS.zip +7f1e6d089b2c087ebebb2ab68a2b796fbfdbee3a43ba97eedd2fe401e3a49b70 GPU-Queue-Widget-1.1.4-macOS.zip diff --git a/launchers/macos/widget/Updater.js b/launchers/macos/widget/Updater.js index b3be4e9..fa85b6c 100644 --- a/launchers/macos/widget/Updater.js +++ b/launchers/macos/widget/Updater.js @@ -38,13 +38,6 @@ function command(path, args, done) { } else done(null, out); }); } -function zipListing(option, done) { - // Finder does not reliably provide a UTF-8 locale. Do not inherit options - // that could change the listing format used by the package validator. - command('/usr/bin/env', ['LC_ALL=en_US.UTF-8','LANG=en_US.UTF-8', - 'ZIPINFO=','ZIPINFOOPT=','UNZIP=','UNZIPOPT=', - '/usr/bin/unzip','-Z',option,archive], done); -} function curl(url, output, maximum, done) { var args = ['--fail','--silent','--show-error','--location','--proto','=https','--proto-redir','=https', '--connect-timeout','10','--max-time',output ? '150' : '20','--max-filesize',String(maximum)]; @@ -67,18 +60,15 @@ function operation(phase, release, done) { else done(null); }); } else if (phase === 'extracting') { - zipListing('-1',function (error,out) { + try { + var zip = $.NSData.dataWithContentsOfFile(archive); + if (zip.isNil()) throw new Error('无法读取安装包'); + GPUQUpdates.archiveBase64(text(zip.base64EncodedStringWithOptions(0))); + } catch (error) { done(error.message); return; } + command('/usr/bin/ditto',['-x','-k',archive,unpacked],function (error) { if (error) { done(error); return; } - try { GPUQUpdates.archiveEntries(out); } catch (e) { done(e.message); return; } - zipListing('-l',function (error,listing) { - if (error) { done(error); return; } - try { GPUQUpdates.archiveMetadata(listing); } catch (e) { done(e.message); return; } - command('/usr/bin/ditto',['-x','-k',archive,unpacked],function (error) { - if (error) { done(error); return; } - if (readFile(unpacked + '/GPU任务台桌面小组件/VERSION') !== release.version) done('安装包版本与更新信息不一致。'); - else done(null); - }); - }); + if (readFile(unpacked + '/GPU任务台桌面小组件/VERSION') !== release.version) done('安装包版本与更新信息不一致。'); + else done(null); }); } else if (phase === 'preparing') { command('/bin/bash',[unpacked + '/GPU任务台桌面小组件/安装桌面小组件.command','--update',workDir],done); diff --git a/launchers/macos/widget/release.json b/launchers/macos/widget/release.json index 451d381..b6102c1 100644 --- a/launchers/macos/widget/release.json +++ b/launchers/macos/widget/release.json @@ -1,8 +1,8 @@ { - "version": "1.1.3", + "version": "1.1.4", "notes": [ - "改善桌面小组件字体可读性,连接状态与辅助说明使用更清晰的字号。", - "固定暖色窗口与原生控件为一致的浅色外观,避免系统深色模式下显示不协调;更新说明使用明确的文字与背景色。", - "保留 SSH 配置、模式和自动启动偏好;网页排版修复需同步更新服务器源码。" + "修复更新时“安装包文件清单不符”:直接校验 ZIP 中央目录及本地文件头,不再解析 macOS unzip 的文本输出。", + "继续校验下载大小和 SHA-256,严格限制文件路径、重复项、普通文件类型及展开大小。", + "1.1.2 更新器遇到此错误时,请完整解压本版并双击安装器覆盖安装,保留已有 SSH 配置和偏好。" ] } diff --git a/launchers/macos/widget/update-model.js b/launchers/macos/widget/update-model.js index 52da776..113c2f8 100644 --- a/launchers/macos/widget/update-model.js +++ b/launchers/macos/widget/update-model.js @@ -52,6 +52,73 @@ var GPUQUpdates = (function () { }); if (total > 16 * 1024 * 1024) throw new Error('安装包展开后过大'); } + // Validate our release ZIP directly. macOS zipinfo's display output is not + // a machine-readable archive manifest (locale/build/options can affect it). + // Our packager writes UTF-8 regular files, deflate, no descriptors or ZIP64. + function archiveBytes(bytes) { + function fail() { throw new Error('安装包 ZIP 结构或文件清单无效'); } + function u16(at) { + if (at < 0 || at + 2 > bytes.length) fail(); + return bytes[at] + bytes[at + 1] * 256; + } + function u32(at) { return u16(at) + u16(at + 2) * 65536; } + function name(at, length) { + if (at < 0 || at + length > bytes.length || length > 1024) fail(); + var escaped = ''; + for (var i = at; i < at + length; i++) escaped += '%' + ('0' + bytes[i].toString(16)).slice(-2); + try { return decodeURIComponent(escaped); } catch (error) { fail(); } + } + if (bytes.length < 22 || bytes.length > 20 * 1024 * 1024) fail(); + var end = bytes.length - 22; + if (u32(end) !== 0x06054b50 || u16(end + 4) || u16(end + 6) || + u16(end + 8) !== files.length || u16(end + 10) !== files.length || u16(end + 20)) fail(); + var centralSize = u32(end + 12), central = u32(end + 16); + if (central + centralSize !== end) fail(); + var cursor = central, total = 0, paths = [], ranges = []; + for (var index = 0; index < files.length; index++) { + if (cursor + 46 > end || u32(cursor) !== 0x02014b50) fail(); + var flags = u16(cursor + 8), method = u16(cursor + 10); + var compressed = u32(cursor + 20), size = u32(cursor + 24); + var length = u16(cursor + 28), extra = u16(cursor + 30), comment = u16(cursor + 32); + var offset = u32(cursor + 42), mode = Math.floor(u32(cursor + 38) / 65536); + if (bytes[cursor + 5] !== 3 || flags !== 0x800 || method !== 8 || + u16(cursor + 34) || (mode & 0xf000) !== 0x8000 || size > 4 * 1024 * 1024 || + cursor + 46 + length + extra + comment > end) fail(); + total += size; + if (total > 16 * 1024 * 1024) fail(); + var path = name(cursor + 46, length); + if (offset + 30 > central || u32(offset) !== 0x04034b50 || + u16(offset + 6) !== flags || u16(offset + 8) !== method || + u32(offset + 14) !== u32(cursor + 16) || + u32(offset + 18) !== compressed || u32(offset + 22) !== size || + u16(offset + 26) !== length || name(offset + 30, length) !== path) fail(); + var dataEnd = offset + 30 + length + u16(offset + 28) + compressed; + if (dataEnd > central) fail(); + paths.push(path); ranges.push([offset, dataEnd]); + cursor += 46 + length + extra + comment; + } + if (cursor !== end) fail(); + // No hidden/orphan local entries, prepended payloads or overlapping data. + ranges.sort(function (a, b) { return a[0] - b[0]; }); + var next = 0; + ranges.forEach(function (range) { if (range[0] !== next) fail(); next = range[1]; }); + if (next !== central) fail(); + archiveEntries(paths.join('\n')); + } + function archiveBase64(value) { + var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', bytes = []; + if (typeof value !== 'string' || value.length > 28 * 1024 * 1024 || + value.length % 4 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) throw new Error('安装包数据无效'); + for (var i = 0; i < value.length; i += 4) { + var a = alphabet.indexOf(value.charAt(i)), b = alphabet.indexOf(value.charAt(i + 1)); + var c = alphabet.indexOf(value.charAt(i + 2)), d = alphabet.indexOf(value.charAt(i + 3)); + if (a < 0 || b < 0) throw new Error('安装包数据无效'); + bytes.push(a * 4 + Math.floor(b / 16)); + if (c >= 0) bytes.push((b % 16) * 16 + Math.floor(c / 4)); + if (d >= 0) bytes.push((c % 4) * 64 + d); + } + archiveBytes(bytes); + } function Controller(io, current) { version(current); this.io = io; this.current = current; this.phase = 'idle'; this.release = null; this.note = ''; this.epoch = 0; this.deadline = 0; @@ -113,6 +180,6 @@ var GPUQUpdates = (function () { } if (this.busy() && this.phase !== 'checking' && this.io.now() > this.deadline) this.fail('更新超时,请重新打开小组件后重试。'); }; - return {feed:feed, version:version, compare:compare, manifest:manifest, archiveEntries:archiveEntries, archiveMetadata:archiveMetadata, Controller:Controller}; + return {feed:feed, version:version, compare:compare, manifest:manifest, archiveEntries:archiveEntries, archiveMetadata:archiveMetadata, archiveBytes:archiveBytes, archiveBase64:archiveBase64, Controller:Controller}; }()); if (typeof module !== 'undefined') module.exports = GPUQUpdates; diff --git a/tests/widget-update.test.cjs b/tests/widget-update.test.cjs index 4962351..e35e9b7 100644 --- a/tests/widget-update.test.cjs +++ b/tests/widget-update.test.cjs @@ -88,3 +88,29 @@ test('the actual release ZIP has only allowed paths, regular files and bounded s assert.throws(()=>U.archiveMetadata(metadata.replace(/(\s+unx\s+)\d+/,'$1'+'99999999'))); } finally {fs.rmSync(temp,{recursive:true,force:true});} }); + +test('binary ZIP validation is independent of zipinfo text and rejects conflicting local headers',()=>{ + const temp=fs.mkdtempSync(path.join(os.tmpdir(),'gpuq-zip-')); + try { + cp.execFileSync('python3',[path.join(__dirname,'../scripts/package-widget.py'),'--output-dir',temp]); + const bytes=fs.readFileSync(path.join(temp,'GPU任务台-桌面小组件-macOS.zip')); + U.archiveBytes(bytes);U.archiveBase64(bytes.toString('base64')); + // Validate the immutable versions users are trying to update from/to as well. + for(const v of ['1.1.2','1.1.3'])U.archiveBytes(fs.readFileSync(path.join(__dirname,'../downloads/GPU-Queue-Widget-'+v+'-macOS.zip'))); + const end=bytes.length-22,central=bytes.readUInt32LE(end+16); + function corrupt(offset,value,size=4){const copy=Buffer.from(bytes);if(size===2)copy.writeUInt16LE(value,offset);else copy.writeUInt32LE(value,offset);return copy;} + for(const bad of [bytes.subarray(0,-1),Buffer.concat([bytes,Buffer.from([0])]), + corrupt(end+10,10,2),corrupt(end+16,0),corrupt(central+42,1), + corrupt(central+38,0xa1ff0000),corrupt(central+24,99999999), + corrupt(6,0x801,2),corrupt(18,0),corrupt(30,0x2e2e2f2f)])assert.throws(()=>U.archiveBytes(bad)); + // The second/third (enable/disable) file names have equal byte lengths. + const duplicate=Buffer.from(bytes),headers=[];let cursor=central; + for(let i=0;i<9;i++){headers.push(cursor);cursor+=46+bytes.readUInt16LE(cursor+28)+bytes.readUInt16LE(cursor+30)+bytes.readUInt16LE(cursor+32);} + const source=headers[1],target=headers[2],length=bytes.readUInt16LE(source+28),local=bytes.readUInt32LE(target+42); + assert.equal(length,bytes.readUInt16LE(target+28)); + bytes.copy(duplicate,target+46,source+46,source+46+length); + bytes.copy(duplicate,local+30,source+46,source+46+length); + assert.throws(()=>U.archiveBytes(duplicate)); + for(const value of ['!', 'AAA', '=AAA', 'AAAA===='])assert.throws(()=>U.archiveBase64(value)); + } finally {fs.rmSync(temp,{recursive:true,force:true});} +});