3DGS+Node.js crypto:模型交付包SHA-256清单【鸿蒙心迹】
3DGS 场景做完重建,工程上往往还会多出一道不起眼的交付工序:把结果目录从开发机交给内容库、其他终端或审核同事。文件能复制过去,不代表内容就完整;同名文件被旧版本覆盖,缩略图更新了而主体没更新,接收端看到的都可能是一份“结构齐全却混搭”的包。只靠文件名与大小判断,很难及时发现这种变化。
这一篇把检查范围限定到开发机上的离线交付工具,不用假装 Node.js 脚本是 HarmonyOS 端侧 SDK。示例项目叫 GaussPack,任务 GS-084,三个虚构交付资产是 scene.asset、preview.png、poses.json。它们仅代表演示归档结构,不宣称是华为 Spatial Recon Kit 的固定输出格式。目标是用 SHA-256 为每份文件生成内容摘要,并在发布入口阻止不一致的包。

一、先确认这不是一篇重建算法文章
华为的空间计算能力包括 3DGS 端侧重建与渲染相关能力,但模型的生成、相机位姿求解以及最终导出形式,必须以当前 SDK 指南和项目实际输出为准。本文不去假设模型一定是 .ply,也不编造某个重建 C API 的参数。相反,我们把“已经拿到一批待交付文件”作为输入前提,处理一个独立又能复现的工程环节:打包前判断文件是否被改写。
上一类交付检查容易停在“路径存在”这一层。路径可以证明文件有入口,却不能保证内容没被交换。SHA-256 哈希将文件内容映射为固定长度摘要,同名文件只要字节改变,摘要通常就不同。它适合检测意外损坏、版本混用和未经批准的替换,但不负责证明文件来自可信作者;攻击者如果可以同时改写文件和清单,单纯哈希仍然挡不住。这也是后面要强调“可信清单”的原因。
设计数据提前统一:GS-084 的清单记录三份文件,演示复核阶段有两份标记 PASS,一份 scene.asset 标记 HASH_MISMATCH,总体状态为 BLOCKED。这里的 2/3 是预设异常用例的期望输出,不是华为端侧实测通过率。开发截图和手机页面都使用这套字段。
二、分块计算摘要,别一次读完整模型
第一个具体实现问题是大文件内存。3DGS 资产可能比较大,使用 readFile() 一次性读完整包再哈希,会随着模型体积提高内存峰值。Node.js 自带 node:crypto 的 createHash('sha256'),配合 node:fs 的 createReadStream(),可以边读边更新哈希,最后输出十六进制摘要。它属于主机侧工具链能力,不依赖 HarmonyOS 编译环境。
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
export async function hashFile(filePath) {
const hash = createHash('sha256');
for await (const chunk of createReadStream(filePath)) {
hash.update(chunk);
}
return hash.digest('hex');
}
每次计算都会创建新的 Hash 对象,不能复用已经调用 digest() 的对象继续处理其他文件。流读取过程中的权限错误、文件被删除或设备 I/O 失败都会以异常形式传出;调用者应将其标成 READ_ERROR,而不是吞掉后写一条空摘要。真正的产线还要防止“读到一半文件被生产者覆盖”的竞态,通常用只读快照或固定交付目录,并在生成清单后冻结文件内容。
还要让清单真的从当前文件生成,而不是人工填写大小和摘要。第二段代码使用一个固定白名单,先读取每个文件的大小和哈希,再把任务号与结果写进 JSON。这是开发机端工具脚本;接收方只消费清单,不应自行发明一个“官方3DGS导出字段”。
import { stat, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { hashFile } from './hash-file.mjs';
export async function buildManifest(root) {
const names = ['scene.asset', 'preview.png', 'poses.json'];
const files = [];
for (const name of names) {
const full = join(root, name);
const info = await stat(full);
if (!info.isFile()) throw new Error(`NOT_FILE: ${name}`);
files.push({ name, bytes: info.size, sha256: await hashFile(full) });
}
const manifest = { taskId: 'GS-084', files };
await writeFile(join(root, 'manifest.json'),
JSON.stringify(manifest, null, 2), 'utf8');
return manifest;
}
names 是此演示包的明确约定,不是对 Spatial Recon Kit 输出结构的推断。构建前需要保证生产者已完成全部写入;如果把清单生成放在仍可修改的目录内,后续验证很可能失败。这不是校验器的误报,而是资产仍处于可变状态的信号。
这里不附一串看似真实的摘要常量,因为虚构哈希会削弱验证意义。实际运行时,应该从当前文件逐字节生成值,再把完整的64位十六进制摘要写入清单。界面上只显示校验结论和文件名,详细摘要保存在供机器比较的数据文件里。
三、先校验路径与大小,再比较内容摘要
仅有哈希函数还不能直接拿外部清单的路径去读磁盘。清单如果携带 ../,可能读到交付目录之外;软链接也能把看似正常的相对路径指向别处。当前实现先拒绝绝对路径和上级目录,再对实际路径做 realpath() 规范化,并要求真实文件仍位于约定根目录下。文件大小不符时提前失败,可以少做一次大文件哈希,但不能用大小相同代替内容一致。
import { realpath, stat } from 'node:fs/promises';
import { resolve, sep, isAbsolute } from 'node:path';
import { hashFile } from './hash-file.mjs';
export async function verifyOne(root, entry) {
const name = entry.name;
if (typeof name !== 'string' || isAbsolute(name) ||
name.includes('\\') || name.split('/').includes('..')) {
return 'BAD_PATH';
}
try {
const base = await realpath(root);
const full = await realpath(resolve(base, name));
if (!full.startsWith(base + sep)) return 'BAD_PATH';
const info = await stat(full);
if (!info.isFile() || info.size !== entry.bytes) {
return 'SIZE_MISMATCH';
}
return (await hashFile(full)) === entry.sha256
? 'PASS' : 'HASH_MISMATCH';
} catch (_) {
return 'READ_ERROR';
}
}
这段代码解决的是常见误用,不是一个完整的防恶意输入沙箱。调用前还需要校验 entry.bytes 为合法非负整数、entry.sha256 是64位十六进制字符串,并限制清单条数、总大小与允许的文件类型。针对高强度并发替换的场景,仅做 realpath() 后重新打开文件仍存在时间窗,应通过不可变快照、目录权限或文件句柄策略继续加固。
另一个细节是相对路径分隔符。这里故意把清单格式规范为使用 / 的相对路径,并拒绝反斜杠,便于开发机和构建流水线使用相同的字段。若交付目录允许嵌套子目录,应同时处理路径大小写规则与符号链接策略;如果只是三个固定资产,直接白名单反而更简单。
四、所有文件通过后才允许发布指针
第三步处理“最后一秒被误发”。逐项校验结果必须先汇总为一份完成状态,不能在检查第一份文件通过时就把整个任务标为成功。演示逻辑对 GS-084 逐项给出状态;只要出现一个 HASH_MISMATCH、READ_ERROR 或 BAD_PATH,就输出 BLOCKED,不写发布指针。这个门禁是业务策略,不是系统自动提供的 3DGS API。
import { writeFile, rename } from 'node:fs/promises';
import { join } from 'node:path';
import { verifyOne } from './verify-one.mjs';
export async function checkAndPublish(root, manifest) {
const results = [];
for (const entry of manifest.files) {
const status = await verifyOne(root, entry);
results.push({ name: entry.name, status });
}
const passed = results.filter(item => item.status === 'PASS').length;
const ready = passed === manifest.files.length && passed > 0;
console.info(`[GaussPack] ${manifest.taskId} verify=${passed}/${results.length}`);
if (!ready) {
console.warn(`[GaussPack] ${manifest.taskId} publish=BLOCKED`);
return { ready: false, results };
}
const temp = join(root, '.publish-ready.tmp');
await writeFile(temp, JSON.stringify({ taskId: manifest.taskId }), { flag: 'wx' });
await rename(temp, join(root, 'publish-ready.json'));
return { ready: true, results };
}
临时文件与目标文件同目录,rename() 在同一文件系统内适合用于减少读到半份指针的风险;它并不能让“复制整个模型目录”变成事务。flag: 'wx' 拒绝覆盖遗留临时文件,便于暴露上一轮未清理的异常,但需要配套失败清理与重试机制。即使发布指针写入成功,下游仍应再次核对清单,避免模型在交付后被另一个环节替换。
还要考虑任务并发。两个任务不能共享 .publish-ready.tmp;实际项目应使用任务号命名或独立输出目录,这段短代码为了突出门禁逻辑省去了任务隔离。清单必须从可信构建流水线来,必要时对清单签名;SHA-256 本身不提供身份认证。

五、把失败说明做成接收方看得懂的界面
对接收方来说,“哈希不一致”四个字不够。界面需要保留任务号、期望文件数、通过数、失败文件和处理动作。本文示意结果是 GS-084:scene.asset 为 HASH_MISMATCH,preview.png 与 poses.json 为 PASS,所以显示 2/3 和 BLOCKED。接收方看到的是“不允许继续发布”,而不是“文件大概可用”。

如果清单不存在,显示 MANIFEST_MISSING;如果清单解析出错,显示 SCHEMA_INVALID;如果只是图片缩略图暂时没传完,也不应擅自忽略缺失文件。是否允许非关键资源缺失,应由上层发布规则事先声明,而不是校验程序临时猜测。错误页给用户的操作建议也应具体:重新从可信构建产物导出、重试传输、重新签发清单,避免“多点几次就好了”的模糊提示。
为了检查示例代码而不是只填写预设结论,本轮还在主机 Node.js v22.16.0 上构造了三份非3DGS的测试文件:先调用 buildManifest() 记录原始摘要,再将 scene.asset 的 V1 文本改为等字节长度的 V2,随后执行 checkAndPublish()。这次本地样例确实返回 verify=2/3,scene.asset=HASH_MISMATCH,publish=BLOCKED;这些是离线脚本对人工样例的执行结果,不是对华为重建模型格式、性能或 SDK 的实机验证。
DevEco 风格截图中右侧的 HarmonyOS 页面,是为了说明将主机校验摘要传给应用后可以怎样展示。它不代表 Node.js 在模拟器里运行,也不代表空间重建 SDK 已集成。底部 HiLog 文字同样属于预设调试示意,未被当作真实设备日志。若要实现实际联动,必须另外设计安全的结果输入格式、来源鉴别和应用端数据校验。
六、验收更关心失败能不能被拦住
建议至少覆盖四组测试:未修改的固定样例全部通过;只变动 scene.asset 的一个字节后出现 HASH_MISMATCH;删除 poses.json 后返回 READ_ERROR;将清单文件名改为 ../secret 后返回 BAD_PATH。这些都是工程验收用例描述,并非本轮已经在真实 3DGS 模型上跑出的测试报告。对比日志时,最好同时记录构建任务号、清单版本和具体文件名,避免把不同批次的结果合并。
内容摘要校验也并非万能。若攻击者既控制文件又控制清单,重新计算摘要就能让两者匹配;若在校验通过后还有可写目录,结果可能随后失效;若文件来自未授权导出,哈希再正确也不能解决版权或权限问题。高可信交付需要不可变产物、签名清单、可追溯流水线和接收端再验证共同作用。
本文得到的结论很窄,但实用:让3DGS结果从“存在文件”推进到“能够证明待交付字节与可信清单一致”。GaussPack 所展示的 GS-084 / 2/3 / BLOCKED 是预设的篡改拦截场景,不是官方工具的成功率。只有把实际模型产物接入、在真实的开发与交付环境复测后,才能据此给具体项目下可交付的结论。
官方参考:华为 HarmonyOS 空间计算/3DGS 能力资料及 Node.js 官方 crypto、fs API。
- https://developer.huawei.com/consumer/cn/features/spatialization
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/spatial-recon-c-spatial-recon-pipeline
- https://nodejs.org/api/crypto.html
- https://nodejs.org/api/fs.html
更多推荐





所有评论(0)