64 lines
3.2 KiB
JavaScript
64 lines
3.2 KiB
JavaScript
import assert from 'node:assert/strict'
|
||
import { createHash } from 'node:crypto'
|
||
import { readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
||
import path from 'node:path'
|
||
|
||
const [workspace, evidence] = process.argv.slice(2)
|
||
if (!workspace || !evidence) throw new Error('请指定项目根目录和本次验证目录')
|
||
const digest = data => createHash('sha256').update(data).digest('hex')
|
||
const read = filename => readFileSync(filename, 'utf8')
|
||
const names = ['config.json', 'smtp.json', 'email.json', 'ssl.json', 'plugins.json']
|
||
|
||
// 只忽略字符串外的 JSON 空白,数字精度、中文、转义及字段顺序都必须保持。
|
||
function compact(raw) {
|
||
JSON.parse(raw)
|
||
let quoted = false
|
||
let escaped = false
|
||
let result = ''
|
||
for (const character of raw) {
|
||
if (quoted) {
|
||
result += character
|
||
if (escaped) escaped = false
|
||
else if (character === '\\') escaped = true
|
||
else if (character === '"') quoted = false
|
||
} else if (character === '"') {
|
||
quoted = true
|
||
result += character
|
||
} else if (!' \t\r\n'.includes(character)) result += character
|
||
}
|
||
return result
|
||
}
|
||
|
||
const report = { checkedAt: new Date().toISOString(), files: [] }
|
||
for (const [relative, backup] of [['config', 'root-config'], ['yunx-test/config', 'runtime-config']]) {
|
||
const directory = path.join(workspace, relative)
|
||
assert.deepEqual(readdirSync(directory).sort(), [...names].sort(), '配置目录出现额外文件')
|
||
const oldRoot = JSON.parse(read(path.join(evidence, backup, 'config.json')))
|
||
const newRoot = JSON.parse(read(path.join(directory, 'config.json')))
|
||
for (const name of names) {
|
||
const oldRaw = read(path.join(evidence, backup, name))
|
||
const newRaw = read(path.join(directory, name))
|
||
assert(!newRaw.startsWith('\ufeff'), '配置不能含 BOM')
|
||
assert(newRaw.includes('\n "'), '配置未按两空格缩进分行')
|
||
let expected = compact(oldRaw)
|
||
if (name === 'config.json') {
|
||
for (const [section, reference] of Object.entries(oldRoot.sections)) {
|
||
expected = expected.replace(`"sha256":"${reference.sha256}"`, `"sha256":"${newRoot.sections[section].sha256}"`)
|
||
}
|
||
} else {
|
||
const section = name.slice(0, -5)
|
||
assert.equal(newRoot.sections[section].sha256, digest(Buffer.from(newRaw)), '新文件摘要不匹配')
|
||
}
|
||
// 不输出配置原文或断言两侧内容,避免凭据进入测试日志。
|
||
assert(expected === compact(newRaw), `${relative}/${name} 内容发生变化`)
|
||
report.files.push({ file: `${relative}/${name}`, lines: newRaw.split('\n').length, valuesUnchanged: true })
|
||
}
|
||
}
|
||
const before = JSON.parse(read(path.join(evidence, 'before.json')).replace(/^\ufeff/, ''))
|
||
const audit = readFileSync(path.join(workspace, 'yunx-test/log/config-audit-v2.jsonl'))
|
||
report.auditUnchanged = digest(audit).toUpperCase() === before.auditSHA256
|
||
assert(report.auditUnchanged, '审计原始文件发生变化')
|
||
writeFileSync(path.join(evidence, 'verification.json'), JSON.stringify(report, null, 2) + '\n')
|
||
console.log('验证通过:10 个 JSON 仅变更空白和对应摘要;中文、密文、数字、版本及审计原字节保持不变。')
|
||
for (const file of report.files) console.log(`${file.file}:${file.lines} 行`)
|