135 lines
4.7 KiB
JavaScript
135 lines
4.7 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* PoC H1: Windows fs.renameSync 跨目录原子性验证
|
||
|
|
*
|
||
|
|
* 验证 4 种场景:
|
||
|
|
* A. 同分区同目录 rename (期望: 原子)
|
||
|
|
* B. 同分区跨目录 rename (期望: 原子)
|
||
|
|
* C. 跨分区 rename (期望: Node 自动 copy+unlink, 非原子)
|
||
|
|
* D. 目标文件存在时 rename (期望: 覆盖, Windows >=10 支持)
|
||
|
|
*
|
||
|
|
* 红队 red-team-logic A1 [CRITICAL]: 跨卷 rename 非原子 → 本测试量化验证
|
||
|
|
* 产出: ai-delivery-pipeline/_poc-sandbox/h1-report.json
|
||
|
|
*/
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
const fs = require('fs');
|
||
|
|
const os = require('os');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
const SANDBOX = path.join(__dirname, '..', '..', 'ai-delivery-pipeline', '_poc-sandbox');
|
||
|
|
const REPORT = path.join(SANDBOX, 'h1-report.json');
|
||
|
|
|
||
|
|
function mk(subdir) {
|
||
|
|
const d = path.join(SANDBOX, subdir);
|
||
|
|
fs.mkdirSync(d, { recursive: true });
|
||
|
|
return d;
|
||
|
|
}
|
||
|
|
|
||
|
|
function scenarioA_sameDir() {
|
||
|
|
const d = mk('h1-a');
|
||
|
|
const a = path.join(d, 'src.txt');
|
||
|
|
const b = path.join(d, 'dst.txt');
|
||
|
|
fs.writeFileSync(a, 'A-content', 'utf8');
|
||
|
|
const t0 = process.hrtime.bigint();
|
||
|
|
fs.renameSync(a, b);
|
||
|
|
const dt = Number(process.hrtime.bigint() - t0) / 1e6;
|
||
|
|
const ok = !fs.existsSync(a) && fs.readFileSync(b, 'utf8') === 'A-content';
|
||
|
|
return { name: 'A-sameDir', ok, ms: dt.toFixed(3) };
|
||
|
|
}
|
||
|
|
|
||
|
|
function scenarioB_crossDir() {
|
||
|
|
const d1 = mk('h1-b/src');
|
||
|
|
const d2 = mk('h1-b/dst');
|
||
|
|
const a = path.join(d1, 'foo.txt');
|
||
|
|
const b = path.join(d2, 'foo.txt');
|
||
|
|
fs.writeFileSync(a, 'B-content', 'utf8');
|
||
|
|
const t0 = process.hrtime.bigint();
|
||
|
|
fs.renameSync(a, b);
|
||
|
|
const dt = Number(process.hrtime.bigint() - t0) / 1e6;
|
||
|
|
const ok = !fs.existsSync(a) && fs.readFileSync(b, 'utf8') === 'B-content';
|
||
|
|
return { name: 'B-crossDir-sameVolume', ok, ms: dt.toFixed(3) };
|
||
|
|
}
|
||
|
|
|
||
|
|
function scenarioC_crossVolume() {
|
||
|
|
// 探测系统是否有第二卷; 无则跳过
|
||
|
|
const drives = [];
|
||
|
|
for (const d of ['D:', 'E:', 'F:']) {
|
||
|
|
try { fs.accessSync(d + '\\', fs.constants.W_OK); drives.push(d); } catch {}
|
||
|
|
}
|
||
|
|
if (drives.length === 0) {
|
||
|
|
return { name: 'C-crossVolume', ok: null, skip: '未检测到第二可写卷' };
|
||
|
|
}
|
||
|
|
const otherVol = drives[0];
|
||
|
|
const tmpOther = path.join(otherVol + '\\', 'bookworm-poc-' + Date.now());
|
||
|
|
try {
|
||
|
|
fs.mkdirSync(tmpOther, { recursive: true });
|
||
|
|
const a = path.join(SANDBOX, 'h1-c-src.txt');
|
||
|
|
const b = path.join(tmpOther, 'dst.txt');
|
||
|
|
fs.writeFileSync(a, 'C-content', 'utf8');
|
||
|
|
const t0 = process.hrtime.bigint();
|
||
|
|
fs.renameSync(a, b);
|
||
|
|
const dt = Number(process.hrtime.bigint() - t0) / 1e6;
|
||
|
|
const ok = !fs.existsSync(a) && fs.readFileSync(b, 'utf8') === 'C-content';
|
||
|
|
fs.unlinkSync(b);
|
||
|
|
fs.rmdirSync(tmpOther);
|
||
|
|
return { name: 'C-crossVolume', ok, ms: dt.toFixed(3), targetVol: otherVol };
|
||
|
|
} catch (e) {
|
||
|
|
return { name: 'C-crossVolume', ok: false, error: e.message, targetVol: otherVol };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function scenarioD_overwrite() {
|
||
|
|
const d = mk('h1-d');
|
||
|
|
const a = path.join(d, 'src.txt');
|
||
|
|
const b = path.join(d, 'dst.txt');
|
||
|
|
fs.writeFileSync(a, 'D-new', 'utf8');
|
||
|
|
fs.writeFileSync(b, 'D-old', 'utf8');
|
||
|
|
try {
|
||
|
|
fs.renameSync(a, b);
|
||
|
|
const content = fs.readFileSync(b, 'utf8');
|
||
|
|
const ok = !fs.existsSync(a) && content === 'D-new';
|
||
|
|
return { name: 'D-overwrite', ok, note: 'overwrote existing' };
|
||
|
|
} catch (e) {
|
||
|
|
return { name: 'D-overwrite', ok: false, error: e.code + ': ' + e.message };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function main() {
|
||
|
|
fs.mkdirSync(SANDBOX, { recursive: true });
|
||
|
|
const results = [
|
||
|
|
scenarioA_sameDir(),
|
||
|
|
scenarioB_crossDir(),
|
||
|
|
scenarioC_crossVolume(),
|
||
|
|
scenarioD_overwrite(),
|
||
|
|
];
|
||
|
|
const report = {
|
||
|
|
hypothesis: 'H1 · Windows fs.renameSync 跨目录原子性',
|
||
|
|
platform: os.platform() + ' ' + os.release(),
|
||
|
|
nodeVersion: process.version,
|
||
|
|
timestamp: new Date().toISOString(),
|
||
|
|
results,
|
||
|
|
verdict: (() => {
|
||
|
|
const a = results[0].ok === true;
|
||
|
|
const b = results[1].ok === true;
|
||
|
|
const d = results[3].ok === true;
|
||
|
|
const c = results[2];
|
||
|
|
if (a && b && d) {
|
||
|
|
const cState = c.ok === null ? '无法测试(无第二卷)' :
|
||
|
|
c.ok === true ? '跨卷也可 rename(Node 自动 copy)' :
|
||
|
|
'跨卷 rename 失败: ' + (c.error || 'unknown');
|
||
|
|
return { pass: true, summary: '同卷原子 ✅, 覆盖 ✅, ' + cState };
|
||
|
|
}
|
||
|
|
return { pass: false, summary: 'A/B/D 有失败场景, 不可用于生产' };
|
||
|
|
})(),
|
||
|
|
recommendation: '',
|
||
|
|
};
|
||
|
|
report.recommendation = report.verdict.pass
|
||
|
|
? '✅ 采用同卷 rename 作为 staging→delivery 主路径; 跨卷场景须 fallback 到 copy+fsync+unlink(非原子, 需上层补偿)'
|
||
|
|
: '❌ 方案不可行, 需重新设计';
|
||
|
|
fs.writeFileSync(REPORT, JSON.stringify(report, null, 2), 'utf8');
|
||
|
|
console.log(JSON.stringify(report, null, 2));
|
||
|
|
}
|
||
|
|
|
||
|
|
main();
|