空间重建项目里最容易被忽略的环节,有时不在算法推理,而在模型真正进入渲染工程之前。一个3DGS素材批次可能同时带着图像帧、掩码、相机描述和临时导出文件。处理者拿到清单后,如果只是join(root, asset.path)再写入磁盘,清单里一个看起来不起眼的../就可能让输出跑出项目的暂存目录。
这篇只解决素材接收入库,不声称调用华为3DGS重建内核。华为官方空间计算资料说明3DGS端侧重建与渲染属于独立能力链路;开发者仍需为外围资产管理负责。本文把接收层命名为SceneInbox,任务编号SI-416,用 Node.js 的path与fs/promises做批次验证。Demo清单共6条:5条符合相对路径约束,1条因越界被拦截,整个批次停在REVIEW。这些是本地构造的输入,不是真实建模器输出。
一、问题不只是“过滤两个点”
先把输入说具体。示例批次里有frames/0001.jpg、frames/0002.jpg、frames/0003.jpg、masks/0001.png、metadata/camera.json五个候选文件,再混入一条../leak/private.key。五条合法路径只说明目录语法符合约定,不代表内容已经通过安全、格式或模型完整性检查;第六条则应在任何文件写入之前被拒绝。
有些实现只测试路径字符串里是否包含..,这会把合法文件名里的两个点也当作逃逸。还有些实现只对路径做normalize,结果忽略了绝对路径和盘符形式。更稳妥的方法是先约定清单只接受POSIX风格的相对路径,再逐段检查,最后用解析后的绝对路径重新计算它相对工作根目录的位置。
这里的规范故意比较严格:不接受反斜杠、不接受空目录段、不接受.和..作为完整段、不接受绝对路径、不接受空字节。这样做会拒绝部分可以通过其他方式安全正规化的输入,但适合机器生成的重建资源清单,因为合法生产流程没有必要发出这些含糊写法。
二、路径裁决是独立的纯函数
第一段代码解决“清单是否有权选择磁盘落点”的问题。它不依赖HarmonyOS端侧SDK;运行位置是Node.js开发脚本或受控工具服务。path.resolve负责得出目标绝对路径,path.relative负责确认目标仍在根目录内。两层校验不能互相替代。
importpathfrom'node:path';exportfunctionsafeTarget(root,relativeName){if(typeofrelativeName!=='string'||!relativeName||relativeName.includes('\0')||relativeName.includes('\\')){thrownewError('INVALID_PATH');}if(path.posix.isAbsolute(relativeName)||/^[A-Za-z]:/.test(relativeName)){thrownewError('ABSOLUTE_PATH');}constsegments=relativeName.split('/');if(segments.some(s=>!s||s==='.'||s==='..')){thrownewError('TRAVERSAL_SEGMENT');}constabsoluteRoot=path.resolve(root);consttarget=path.resolve(absoluteRoot,...segments);constback=path.relative(absoluteRoot,target);if(!back||back==='..'||back.startsWith(`..${path.sep}`)||path.isAbsolute(back)){thrownewError('OUTSIDE_ROOT');}returntarget;}这里刻意把../leak/private.key拦在段检查阶段,错误码记为TRAVERSAL_SEGMENT;即使某些输入经过正规化仍落在根目录内,也不建议默许它们。在跨平台构建机上,反斜杠还会触发不同的路径解释,要求统一清单格式通常比到处修补分隔符更易审计。
但这段代码只解决词法层面的路径越界。如果暂存根目录或中间目录可以被其他进程替换成符号链接,解析后的字符串仍可能被文件系统重定向。正式导入器需要把暂存区放在私有、不可被不可信用户写入的位置,并用lstat、安全打开策略等额外手段处理符号链接与竞态。不要把一次path.relative判断包装成全面安全审计。
三、六条输入必须先出一份整体判定
业务上还有一个不易察觉的问题:逐条校验、逐条写入,会让前五条成功文件提前出现在下游目录里;等发现第六条越界,模型加载器可能已经看到了半套素材。因此SceneInbox采用批次判定:先在内存中完成所有路径裁决,只有整体通过才进入写入阶段。
第二段代码负责生成审计报告。path字段是相对路径,bytes是本地示例负载;accepted和rejected是列表数量而非“算法成功率”。工程正式接入时还需要检查类型、大小、数量上限和业务清单版本,尤其要防止单批次资源过多造成内存压力。
constassets=[{path:'frames/0001.jpg',bytes:Buffer.from('frame-1')},{path:'frames/0002.jpg',bytes:Buffer.from('frame-2')},{path:'frames/0003.jpg',bytes:Buffer.from('frame-3')},{path:'masks/0001.png',bytes:Buffer.from('mask-1')},{path:'metadata/camera.json',bytes:Buffer.from('{}')},{path:'../leak/private.key',bytes:Buffer.from('blocked')}];constroot='/private/scene-inbox/SI-416';// 示例逻辑根目录constallowed=[];constrejected=[];for(constitemofassets){try{safeTarget(root,item.path);allowed.push(item);}catch(error){rejected.push({path:item.path,reason:String(error.message)});}}constreport={taskId:'SI-416',scanned:assets.length,accepted:allowed.length,rejected:rejected.length,gate:rejected.length?'REVIEW':'READY'};console.info('[SceneInbox] SI-416 scanned=6 accepted=5 rejected=1');预计的报告是scanned=6、accepted=5、rejected=1、gate=REVIEW。注意这里的accepted更准确叫“路径预审通过”,并不是五个3DGS资源已经写入工程。把“通过路径规则”“进入暂存目录”“完整交付给渲染器”区分成三个业务阶段,发生异常时才知道究竟哪个状态能回滚。
REVIEW下不应该自动进入3DGS模型装载阶段。界面可以列出被拒绝的路径、错误码和建议检查项,但不应在页面上展示“渲染成功”或“重建完成”。这也是我坚持在配图中把批次状态写成审核中、而不是通过的原因。
四、没有整批通过,就没有对外可见目录
第三段代码解决“前几条已经写进去,最后一条才失败”的一致性问题。演示用mkdtemp建立随机私有暂存目录,并以wx标志写入,避免静默覆盖已有文件。若预审出现任何拒绝项,直接返回SKIPPED,不会向下游发布目录提交一套半成品。只有整批合格,才在同一文件系统里尝试rename完成交付。
import{mkdtemp,mkdir,writeFile,rename,rm}from'node:fs/promises';importpathfrom'node:path';asyncfunctioncommitBatch(baseDir,items,report){if(report.gate!=='READY'){return{...report,committed:0,staging:'SKIPPED'};}awaitmkdir(baseDir,{recursive:true});constwork=awaitmkdtemp(path.join(baseDir,'.stage-'));try{for(constitemofitems){consttarget=safeTarget(work,item.path);awaitmkdir(path.dirname(target),{recursive:true});awaitwriteFile(target,item.bytes,{flag:'wx'});}awaitrename(work,path.join(baseDir,report.taskId));return{...report,committed:items.length,staging:'PROMOTED'};}catch(error){awaitrm(work,{recursive:true,force:true});throwerror;}}对于本批次,函数应返回committed=0、staging=SKIPPED。它没有把路径预审通过的五个条目实际放进生产目录。这个“不提交”的行为在业务上比单纯展示一条红色错误更重要,因为下游只观察正式目录时,不会读到一半的数据。
边界也要明确:rename的原子性依赖同文件系统和目标目录状态;代码没有实现并发同名批次锁、磁盘空间预估、符号链接全面防护或断电持久性确认。产品方案应把这些列成上线验收项,而不是只靠一段示例脚本承诺数据绝对安全。
五、示意界面要反映“拒绝发布”而非“拒绝处理全部输入”
SceneInbox 的示意操作时间为12:40:21,先展示scan=6,随后展示accepted=5 rejected=1,最后落到gate=REVIEW。这几行是预设的演示日志;真实设备上是否运行 Node.js、在哪个工具进程执行,都需要项目方案自己确定。HarmonyOS应用侧可以消费已经审核好的报告,也可以通过可信服务接口请求导入结果,但本篇没有编造一个名为“3DGS自动拦截”的系统API。
手机示意图中显示任务SI-416、六条检查、五条通过、一条拒绝以及../leak/private.key。下方明确标记committed=0与REVIEW,提醒读者一条可疑路径足以暂停整批发布;其他五条只是候选资源,并非已经生成模型。
建议在真正项目里补三类验证:构造包含绝对路径和路径分隔符混用的对抗性清单;模拟磁盘写入失败确保残留暂存目录被清理;在读取侧只扫描最终发布目录,不扫描.stage-*。还要用真实重建工具输出验证清单与模型数据格式,尤其是相机参数文件的结构和帧编号是否匹配。离线脚本检查通过,不等于设备端推理能力已经适配。
六、这条流程实际保护了什么
路径白名单让文件“只能写在约定区域”有了可解释的依据;整批提交门禁让“六条里一条不安全”不会变成一个模棱两可的中间版本;明确状态字段则为后续扩展模型预览、失败重试和操作审计提供了边界。它们解决的是3DGS周边工程的数据交付可靠性,而不是点云或高斯重建算法的精度。
本地可以验证的范围是 Node.js 路径函数对这些构造字符串的裁决和脚本级输出;尚未验证真实建模、图形渲染或不同HarmonyOS设备的目录结构。若未来引入压缩包解压、云端上传或跨进程文件交付,还必须增加归档成员名、符号链接、文件数量与容量上限等检查。安全边界应该随着数据链路扩展,而不是随着文章标题变大。
资料依据:华为空间计算与3DGS能力介绍;Node.js 官方node:path与node:fsAPI 说明。此文中的界面、资源清单与日志为构造演示数据,不是已上线的3DGS重建结果。