HarmonyOS诊断包导出引用与文件一致性校验【共创稿事节】

本地诊断功能写完字段分级和脱敏规则以后,文件交付环节仍然容易出现“预览的是一份数据,实际导出的却是另一份”的问题。应用页面可能展示了已经处理过的邮箱,后台写文件时却再次读取原始事件;也可能先生成清单,随后修改事件内容,导致清单记录的摘要与文件不再一致。为避免两个读取入口各自维护一套规则,可以让导出流程只消费上一阶段得到的受控事件对象,将字段检查、规范化、写入和回读安排为明确的连续步骤。

仍以诊断事件EV-104为主线:业务类型NAV_TIMEOUT,耗时5320毫秒,页面键/pages/map,遮盖邮箱user***@example.com。这个对象应有四个可交付字段,原始凭据在上一阶段已被剔除。本篇把它写成EV-104.json,再创建引用清单manifest.json。清单中的相对文件名、字节长度和SHA-256必须对应真实存放的文件字节,而不是根据界面里的字符长度或内存对象猜测。遇到异常时先保留可读的旧清单,避免把半成品交给下一个页面。

诊断包从受控字段到文件引用的处理流程

明确导出契约及字节口径

事件编号负责定位,摘要负责比较内容,两者不能互换。为了让对照结果稳定,导出模块先检查白名单:contactEmail、elapsedMs、eventCode、pageKey四个字段必须存在,且不允许多出其它字段。对象属性进入文件之前,按照字段名排序,再用UTF-8编码生成字节。文件长度取编码后的Buffer.length;对含中文或其他多字节字符的内容,JavaScript字符串长度与文件字节数可能不同,因此长度要从真正要写入的缓冲区获得。

清单还需要一个格式版本,方便后续迁移。本文约定schemaVersion=1,引用路径只允许采用固定命名规则events/EV-104.json。这种引用是应用包内的相对路径,不是任意用户输入路径。数据出站前检查相对路径可以避免目录穿越,读回时还需要确认实际文件确实位于导出根目录内。路径校验与内容摘要承担不同职责:一个解决“读哪一个文件”,另一个解决“读取的字节是否相同”。

第一段代码生成受控内容和可重复计算的摘要。它使用Node.js内置模块,三条断言检查键集合、秘密字段排除和UTF-8字节口径。具体业务值与前两篇一致,因此下一阶段可用同一份契约做回归比较。

const assert = require('node:assert/strict');
const {createHash} = require('node:crypto');
const keys = ['contactEmail', 'elapsedMs', 'eventCode', 'pageKey'];
function encodeEvent(input) {
  const actual = Object.keys(input).sort();
  if (JSON.stringify(actual) !== JSON.stringify(keys)) {
    throw new Error('EXPORT_FIELDS_MISMATCH');
  }
  const data = Object.fromEntries(keys.map(key => [key, input[key]]));
  const bytes = Buffer.from(JSON.stringify(data), 'utf8');
  return {bytes, sha256: createHash('sha256').update(bytes).digest('hex')};
}
const safe = {eventCode:'NAV_TIMEOUT', elapsedMs:5320,
  pageKey:'/pages/map', contactEmail:'user***@example.com'};
const output = encodeEvent(safe);
assert.equal(output.sha256.length, 64);
assert.equal(JSON.parse(output.bytes.toString()).contactEmail,
  'user***@example.com');
assert.throws(() => encodeEvent({...safe, authToken:'x'}), /EXPORT_FIELDS/);
console.log('fields=4, utf8Bytes=' + output.bytes.length);

从页面调用这一函数时,safe应来自已脱敏的数据层,而不是通过视图控件收集显示文字。展示时隐藏某一行并不会改变持久化对象,导出契约却会拒绝多余键。字段升级也要同步修订版本号和校验用例:假如将来决定删除pageKey,不能只删除页面上的栏目,而仍按旧规则写入。格式版本变更后,接收方应能明确得知字段集合及编码约定发生了什么变化。

临时文件、发布顺序与回读

直接写入目标文件可能中途失败,留下一个可被其他功能看到的截断文件。较安全的单目录流程是先写入临时文件,确保写入返回并关闭文件句柄,再回读计算摘要;只有内容和字节数满足契约,才将临时文件移动到正式名称。manifest.json应在事件文件完成后再发布,其引用在创建时就指向已经存在的文件。这里采用同一文件系统内的重命名作为切换动作,实际项目还应根据存储介质和失败恢复要求评估持久落盘顺序。

第二段代码在操作系统临时目录中创建一个独立工作区,以便直接运行断言。实际HarmonyOS工程应把对应的文件操作适配到应用自己的可写目录和Core File Kit接口,而不是引入Node.js的fs作为ArkTS系统API。代码中的fsyncSync用于将文件句柄内容交给文件系统同步处理,单次调用并不能替代对整个存储介质、权限与异常恢复路径的设计。

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const {createHash} = require('node:crypto');
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'diag-export-'));
const eventsDir = path.join(root, 'events');
fs.mkdirSync(eventsDir);
const safe = {contactEmail:'user***@example.com', elapsedMs:5320,
  eventCode:'NAV_TIMEOUT', pageKey:'/pages/map'};
const bytes = Buffer.from(JSON.stringify(safe), 'utf8');
const hash = b => createHash('sha256').update(b).digest('hex');
const tmp = path.join(eventsDir, 'EV-104.json.pending');
const target = path.join(eventsDir, 'EV-104.json');
const fd = fs.openSync(tmp, 'wx');
try { fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); }
finally { fs.closeSync(fd); }
assert.equal(hash(fs.readFileSync(tmp)), hash(bytes));
fs.renameSync(tmp, target);
const manifest = {schemaVersion:1, files:[{
  path:'events/EV-104.json', size:bytes.length, sha256:hash(bytes)
}]};
fs.writeFileSync(path.join(root, 'manifest.json'),
  JSON.stringify(manifest), {flag:'wx'});
assert.equal(fs.readFileSync(target).length, manifest.files[0].size);
assert.equal(fs.existsSync(tmp), false);
console.log('exportFile=EV-104.json, references=1');
fs.rmSync(root, {recursive:true, force:true});

导出模块还应考虑并发请求。若用户连续点击两次导出,同名文件可能被两条任务同时创建。工程里可以给每次导出分配独立目录,或者为写入阶段设置串行锁,待状态完成后再开放交付入口。面对磁盘空间不足、权限变化、应用生命周期切换等问题,界面应该显示“写入未完成”“文件待核对”或“可以交付”等明确状态,而不是只凭按钮触发就更新为成功。临时文件清理应有单独的恢复入口,因为进程中断可能发生在关闭句柄之后、正式命名之前。

开发环境中的文件构建、回读检查及应用预览

引用验证必须从文件重新读取

manifest.json只是声明,不能用它里面的摘要去和同一个清单的副本比较。真正的检查要从清单读取相对路径,打开相应事件文件,重新计算SHA-256并核对字节长度。任何一项不一致,都应阻止后续分享动作。也要拒绝清单重复引用同一路径、绝对路径和向上穿越;否则即使摘要正确,路径解析也可能访问非预期文件。对于允许用户导入的诊断包,仍应设置文件数量和总字节预算,防止构造异常内容导致过量内存与存储消耗。

第三段代码提供了独立可运行的核对函数,包含路径白名单、重复路径拒绝与文件被修改后的不一致断言。为了让逻辑清晰,目录内只有单个受控事件文件;多文件版本应逐项应用相同规则,并在全部通过以后才把清单状态标为可交付。文件名的EV-104与真实诊断事件关联,但不承担用户身份标识的职责。

const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const {createHash, timingSafeEqual} = require('node:crypto');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'diag-check-'));
fs.mkdirSync(path.join(dir, 'events'));
const rel = 'events/EV-104.json';
const file = path.join(dir, rel);
fs.writeFileSync(file, '{"eventCode":"NAV_TIMEOUT"}', 'utf8');
const digest = bytes => createHash('sha256').update(bytes).digest();
const entry = {path:rel, size:fs.statSync(file).size,
  sha256:digest(fs.readFileSync(file)).toString('hex')};
function check(root, item) {
  if (!/^events\/EV-[0-9]+\.json$/.test(item.path)) return false;
  const full = path.join(root, item.path);
  if (!fs.existsSync(full) || fs.lstatSync(full).isSymbolicLink()) return false;
  const bytes = fs.readFileSync(full);
  const expected = Buffer.from(item.sha256, 'hex');
  const actual = digest(bytes);
  return bytes.length === item.size && expected.length === actual.length &&
    timingSafeEqual(expected, actual);
}
assert.equal(check(dir, entry), true);
fs.appendFileSync(file, ' ');
assert.equal(check(dir, entry), false);
assert.equal(check(dir, {...entry, path:'../private.txt'}), false);
console.log('reference tamper checks passed');
fs.rmSync(dir, {recursive:true, force:true});

若同一目录还有其他业务资料,使用更细的专属导出子目录可缩小路径检查范围;遇到符号链接、文件替换或竞争条件时,则应在打开描述符以后再核对文件身份,并避免检查与读取之间出现不可控切换。示例中短小的路径约束是对单种文件名的约定,不应直接放大为允许任意外部文件的通用策略。清单一旦交付,就应与文件内容共同冻结;修改任一文件都需要重新生成引用记录。

HarmonyOS文件能力与交付边界

HarmonyOS应用可以使用上下文提供的应用文件目录保存自己的诊断内容。文件读取、写入和句柄释放应采用Core File Kit中适配当前SDK的接口,并针对ENOENT、无权限、空间耗尽以及文件已经被移走的情形分别处理。仅在应用自己的可写目录管理数据,和将用户文件输出到公共目录,是两个不同的授权场景。公共目录操作或跨应用文件共享需要按照实际交付方式选择Picker、URI及相应授权流程;不能把一个私有绝对路径直接当作其他应用可读取的共享地址。

华为开发者关于用户文件的文档说明,对用户文件的访问需要相应授权或由用户进行选择;文件共享API也明确采用URI与权限策略。本文的Node.js脚本用于固定导出协议和验证步骤,在HarmonyOS工程中要由ArkTS层对接文件能力,保留结构检查、读取后摘要比较与异常状态机,而不是把脚本中的磁盘路径原样写入设备。相关资料包括用户文件概述、文件共享接口与Preferences接口。Preferences适合少量状态键值,不适合代替诊断文件的完整字节存储。

诊断包文件引用与摘要状态的移动端界面

最后一个容易被遗漏的步骤是清理导出失败后遗留的临时文件,以及解除不再使用的文件句柄和页面引用。应用退出时先停止新的写入,再关闭队列、释放资源;已经发布的清单只能指向完整文件。由受控对象开始,经规范化、暂存、回读到清单发布,导出链路才能做到每一份可交付引用都对应可核对的真实字节。下一篇将以这份清单为入口,进一步处理文件到期、清理保留和恢复检查。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐