Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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”。如果旧更新器报“安装包文件路径无效”,先退出旧面板和更新窗口,再覆盖安装一次。
Expand Down
14 changes: 8 additions & 6 deletions SOURCE-SHA256.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions docs/widget-update-1.1.4.md
Original file line number Diff line number Diff line change
@@ -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 与真实安装验收。
Binary file added downloads/GPU-Queue-Widget-1.1.4-macOS.zip
Binary file not shown.
1 change: 1 addition & 0 deletions downloads/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -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
26 changes: 8 additions & 18 deletions launchers/macos/widget/Updater.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions launchers/macos/widget/release.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"version": "1.1.3",
"version": "1.1.4",
"notes": [
"改善桌面小组件字体可读性,连接状态与辅助说明使用更清晰的字号。",
"固定暖色窗口与原生控件为一致的浅色外观,避免系统深色模式下显示不协调;更新说明使用明确的文字与背景色。",
"保留 SSH 配置、模式和自动启动偏好;网页排版修复需同步更新服务器源码。"
"修复更新时“安装包文件清单不符”:直接校验 ZIP 中央目录及本地文件头,不再解析 macOS unzip 的文本输出。",
"继续校验下载大小和 SHA-256,严格限制文件路径、重复项、普通文件类型及展开大小。",
"1.1.2 更新器遇到此错误时,请完整解压本版并双击安装器覆盖安装,保留已有 SSH 配置和偏好。"
]
}
69 changes: 68 additions & 1 deletion launchers/macos/widget/update-model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
26 changes: 26 additions & 0 deletions tests/widget-update.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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});}
});