fix(runtime): remove obsolete PDF browser compatibility patch
This commit is contained in:
@@ -40,8 +40,6 @@ Linux 文档预览通过构建期补丁将 Office-to-PDF 转换入口接到系
|
||||
|
||||
每次转换使用私有临时目录、独立 profile、最高宏安全级别和独立进程组;保留默认 60 秒超时,取消时终止进程组。检查输入大小、OOXML 条目数、解压总量和 CRC、输出大小及完整性。子进程采用最小环境变量;校验器地址空间上限 512 MiB,LibreOffice 为 2 GiB,单文件写入上限为 max(maxOutputBytes, 16 MiB),最终 PDF 仍严格检查 maxOutputBytes。进程资源限制不是进程树总量或临时目录总量限制,部署时仍须容器内存、CPU、PID 和临时磁盘限制。
|
||||
|
||||
PDF 浏览器兼容补丁:image/install-pdf-compat.mjs 在构建期为 DSH / documentpreview 0.2.1-rc.2 的 PDF.js 主线程及内嵌 worker 注入缺失的 Map.getOrInsert、Map.getOrInsertComputed 和 Math.sumPrecise,不覆盖原生方法。版本不匹配、worker 入口变化或重复安装时直接失败。精确求和使用 BigInt 累加,避免嵌入字体因 Chrome 143 缺少 API 而回退。回归测试命令:node --test image/pdf-compat.test.mjs。已用 Chrome for Testing 143.0.7499.192(移动视口、触摸模拟)验证镜像内真实 PDF 加载/渲染代码:补丁前复现 methodPromises 报错,补丁后 14 页样本加载、第 1→2→1 页渲染与文本提取成功,控制台无错误或警告;不等同 Android 真机或完整业务 UI 测试。现有部署须重新构建并更新镜像才会获得补丁。
|
||||
|
||||
安全边界:原生 LibreOffice 与 DSH 使用同一 UID,独立 profile 不是文件系统或网络沙箱;不应将此功能视为隔离恶意文档的服务。容器内敏感文件及网络访问仍需部署侧隔离,尤其是处理不可信外部文档时。
|
||||
|
||||
字体由系统解析;显式 fontDirectories/fontFallbacks 配置会被拒绝,应移除并改用系统字体配置。kit 的 maxFontFiles/maxFontFileBytes/maxLoadedFontBytes 不再约束系统字体。missingFonts 为空仅表示不提供缺失字体诊断,不代表没有字体替换。
|
||||
|
||||
@@ -34,8 +34,6 @@ RUN node /usr/local/lib/dsh-mgr/install-misans.mjs
|
||||
COPY MiSans-NOTICE.txt /usr/local/share/fonts/misans/NOTICE.txt
|
||||
COPY install-office-preview.mjs native-office.mjs validate-office.py office-worker.py /usr/local/lib/dsh-mgr/
|
||||
RUN node /usr/local/lib/dsh-mgr/install-office-preview.mjs
|
||||
COPY install-pdf-compat.mjs pdf-map-compat.js /usr/local/lib/dsh-mgr/
|
||||
RUN node /usr/local/lib/dsh-mgr/install-pdf-compat.mjs
|
||||
COPY preview-download /usr/local/lib/dsh-mgr/preview-download
|
||||
COPY install-preview-download.mjs /usr/local/lib/dsh-mgr/install-preview-download.mjs
|
||||
RUN node /usr/local/lib/dsh-mgr/install-preview-download.mjs
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
export const workerTarget = 'new Blob([_dsh_pdf_worker_default,';
|
||||
export const marker = '// dsh-pdf-map-compat-v1';
|
||||
export const compat = readFileSync(new URL('./pdf-map-compat.js', import.meta.url), 'utf8');
|
||||
|
||||
export function patchPdf(source, hostVersion, packageVersion) {
|
||||
if (hostVersion !== '0.2.1-rc.2' || packageVersion !== '0.2.1-rc.2') throw new Error('unsupported PDF preview version');
|
||||
if (source.includes(marker)) throw new Error('PDF compatibility patch already installed');
|
||||
if (source.split(workerTarget).length !== 2 || !source.includes('this.#methodPromises.getOrInsertComputed(')) {
|
||||
throw new Error('unexpected PDF.js bundle: expected main-thread upsert and exactly one worker Blob');
|
||||
}
|
||||
return marker + '\n' + compat + '\n' + source.replace(workerTarget, 'new Blob([' + JSON.stringify(compat) + ', _dsh_pdf_worker_default,');
|
||||
}
|
||||
|
||||
export function install(root = '/usr/local/lib/node_modules/@deepseek-ai/dsh') {
|
||||
const dir = join(root, 'node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview');
|
||||
const version = path => JSON.parse(readFileSync(join(path, 'package.json'), 'utf8')).version;
|
||||
const file = join(dir, 'lib/client.pdf.js');
|
||||
writeFileSync(file, patchPdf(readFileSync(file, 'utf8'), version(root), version(dir)));
|
||||
console.log('Installed PDF Map compatibility patch (main thread + worker)');
|
||||
}
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) install(process.argv[2]);
|
||||
@@ -1,47 +0,0 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { runInNewContext } from 'node:vm';
|
||||
import { compat, patchPdf, workerTarget, marker } from './install-pdf-compat.mjs';
|
||||
|
||||
function run(code) {
|
||||
return runInNewContext('delete Math.sumPrecise; delete Map.prototype.getOrInsert; delete Map.prototype.getOrInsertComputed;\n' + compat + '\n' + code);
|
||||
}
|
||||
test('precise summation handles cancellation, rounding, overflow, subnormals and special values', () => {
|
||||
assert.equal(run('Math.sumPrecise([1e20, 1, -1e20])'), 1);
|
||||
assert.equal(run('Math.sumPrecise([Number.MAX_VALUE, Number.MAX_VALUE, -Number.MAX_VALUE])'), Number.MAX_VALUE);
|
||||
assert.equal(run('Math.sumPrecise([Number.MAX_VALUE, Number.MAX_VALUE])'), Infinity);
|
||||
assert.equal(run('Math.sumPrecise([Number.MIN_VALUE, Number.MIN_VALUE])'), 2 * Number.MIN_VALUE);
|
||||
assert.equal(run('Math.sumPrecise([1, 2 ** -53])'), 1);
|
||||
assert.equal(run('Math.sumPrecise([1, 2 ** -53, 2 ** -54])'), 1 + Number.EPSILON);
|
||||
assert.ok(Object.is(run('Math.sumPrecise([])'), -0));
|
||||
assert.ok(Object.is(run('Math.sumPrecise([-0])'), -0));
|
||||
assert.ok(Object.is(run('Math.sumPrecise([-1, 1])'), 0));
|
||||
assert.ok(Number.isNaN(run('Math.sumPrecise([Infinity, -Infinity])')));
|
||||
assert.throws(() => run('Math.sumPrecise([Infinity, "1"])'), /numbers/);
|
||||
});
|
||||
test('computed insertion caches undefined and promises without recomputation', () => {
|
||||
assert.equal(run('const m = new Map(); let n = 0; m.getOrInsertComputed("x", () => { n++; }); m.getOrInsertComputed("x", () => { n++; }); n'), 1);
|
||||
assert.equal(run('const m = new Map(); const p = Promise.resolve(1); m.getOrInsertComputed("x", () => p) === m.getOrInsertComputed("x", () => 2)'), true);
|
||||
});
|
||||
test('callback validation, errors, receiver, key normalization and reentrancy', () => {
|
||||
assert.throws(() => run('new Map([[1, 2]]).getOrInsertComputed(1, null)'), /callable/);
|
||||
assert.throws(() => run('Map.prototype.getOrInsertComputed.call({}, 1, () => 2)'), /incompatible receiver/);
|
||||
assert.equal(run('new Map().getOrInsertComputed(-0, k => Object.is(k, 0))'), true);
|
||||
assert.equal(run('const m = new Map(); m.getOrInsertComputed(1, k => { m.set(k, 2); return 3; }); m.get(1)'), 3);
|
||||
assert.equal(run('const m = new Map(); try { m.getOrInsertComputed(1, () => { throw Error(); }); } catch {} m.size'), 0);
|
||||
assert.equal(run('new Map([[NaN, 4]]).getOrInsertComputed(NaN, () => 5)'), 4);
|
||||
});
|
||||
test('plain insertion and non-enumerable, repeat-safe methods', () => {
|
||||
assert.equal(run('const m = new Map([[1, undefined]]); m.getOrInsert(1, 2) === undefined && m.getOrInsert(2, 3) === 3'), true);
|
||||
assert.equal(run('Object.getOwnPropertyDescriptor(Map.prototype, "getOrInsertComputed").enumerable'), false);
|
||||
assert.equal(runInNewContext('Map.prototype.getOrInsertComputed = () => 42;\n' + compat + '\nnew Map().getOrInsertComputed()'), 42);
|
||||
});
|
||||
test('patch guards and worker injection', () => {
|
||||
const source = 'this.#methodPromises.getOrInsertComputed(name, f); ' + workerTarget + ' tail]);';
|
||||
const patched = patchPdf(source, '0.2.1-rc.2', '0.2.1-rc.2');
|
||||
assert.ok(patched.startsWith(marker + '\n' + compat));
|
||||
assert.ok(patched.includes('new Blob([' + JSON.stringify(compat) + ', _dsh_pdf_worker_default,'));
|
||||
assert.throws(() => patchPdf(patched, '0.2.1-rc.2', '0.2.1-rc.2'), /already/);
|
||||
assert.throws(() => patchPdf(source, 'other', '0.2.1-rc.2'), /version/);
|
||||
for (const bad of ['', source + workerTarget]) assert.throws(() => patchPdf(bad, '0.2.1-rc.2', '0.2.1-rc.2'), /unexpected/);
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
// PDF.js 6 uses Map upsert APIs absent from Chrome 143 / older WebViews.
|
||||
// Run in both the document and its separate worker realm. Never replace natives.
|
||||
(() => {
|
||||
'use strict';
|
||||
const { has, get, set } = Map.prototype;
|
||||
// Accumulate finite doubles exactly in units of the smallest subnormal.
|
||||
// Round only once, avoiding both cancellation loss and intermediate overflow.
|
||||
if (typeof Math.sumPrecise !== 'function') {
|
||||
Object.defineProperty(Math, 'sumPrecise', {
|
||||
configurable: true, writable: true,
|
||||
value: function sumPrecise(values) {
|
||||
const bits = new DataView(new ArrayBuffer(8));
|
||||
let total = 0n, negativeZero = true, positiveInfinity = false, negativeInfinity = false, nan = false;
|
||||
for (const value of values) {
|
||||
if (typeof value !== 'number') throw new TypeError('sumPrecise requires numbers');
|
||||
if (!Object.is(value, -0)) negativeZero = false;
|
||||
if (!Number.isFinite(value)) {
|
||||
if (Number.isNaN(value)) nan = true;
|
||||
else if (value > 0) positiveInfinity = true;
|
||||
else negativeInfinity = true;
|
||||
continue;
|
||||
}
|
||||
bits.setFloat64(0, value);
|
||||
const high = bits.getUint32(0), low = bits.getUint32(4);
|
||||
const exponent = (high >>> 20) & 0x7ff;
|
||||
let mantissa = (BigInt(high & 0xfffff) << 32n) | BigInt(low);
|
||||
if (exponent) mantissa = (mantissa | (1n << 52n)) << BigInt(exponent - 1);
|
||||
total += high >>> 31 ? -mantissa : mantissa;
|
||||
}
|
||||
if (nan || (positiveInfinity && negativeInfinity)) return NaN;
|
||||
if (positiveInfinity) return Infinity;
|
||||
if (negativeInfinity) return -Infinity;
|
||||
if (total === 0n) return negativeZero ? -0 : 0;
|
||||
const sign = total < 0n ? -1 : 1;
|
||||
if (total < 0n) total = -total;
|
||||
const shift = Math.max(0, total.toString(2).length - 53);
|
||||
let rounded = total >> BigInt(shift);
|
||||
if (shift) {
|
||||
const remainder = total - (rounded << BigInt(shift));
|
||||
const half = 1n << BigInt(shift - 1);
|
||||
if (remainder > half || (remainder === half && (rounded & 1n))) rounded++;
|
||||
}
|
||||
return sign * Number(rounded) * 2 ** (shift - 1074);
|
||||
},
|
||||
});
|
||||
}
|
||||
if (typeof Map.prototype.getOrInsert !== 'function') {
|
||||
Object.defineProperty(Map.prototype, 'getOrInsert', {
|
||||
configurable: true, writable: true,
|
||||
value: function getOrInsert(key, value) {
|
||||
if (has.call(this, key)) return get.call(this, key);
|
||||
set.call(this, key, value);
|
||||
return value;
|
||||
},
|
||||
});
|
||||
}
|
||||
if (typeof Map.prototype.getOrInsertComputed !== 'function') {
|
||||
Object.defineProperty(Map.prototype, 'getOrInsertComputed', {
|
||||
configurable: true, writable: true,
|
||||
value: function getOrInsertComputed(key, callback) {
|
||||
const present = has.call(this, key); // Also validates the Map receiver.
|
||||
if (typeof callback !== 'function') throw new TypeError('callback must be callable');
|
||||
if (present) return get.call(this, key);
|
||||
if (key === 0) key = 0; // Canonicalize -0 before calling the callback.
|
||||
const value = callback(key);
|
||||
set.call(this, key, value); // Callback may have inserted the same key.
|
||||
return value;
|
||||
},
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user