
鸿蒙 PC Markdown 编辑器跨应用分享权限验证摘要Markdown 编辑器里的“分享”并不是把一个按钮接到系统接口就算完成。对本地优先的桌面编辑器来说真正需要证明的是一条跨越编辑缓冲区、应用沙箱、系统能力匹配和接收应用权限边界的完整数据链用户尚未保存的正文必须进入快照快照不能暴露原工作区文件发送端只能授予必要的临时读取能力另一个独立进程必须能按声明的 MIME 类型获得 URI、完整读取字节并严格解码失败时编辑器还要保留当前正文、文档身份和脏状态。本文记录 OhMarkdown 在 HarmonyOS 6.1.1、API 24、MateBook Pro 2in1 模拟器上完成的一次受控跨应用验证。仓库地址为 https://gitcode.com/VON-/codex_md_oh分享接收测试应用与证据进入提交d9c8384。发送端输入未保存标记3G_08_MARKDOWN_SHARE_20260721独立包名com.example.ohmarkdown.sharereceiver的接收端最终显示29 字符 / 29 字节正文逐字符一致。这项结果证明当前模拟器上的sendData text/markdown 临时 URI主链可工作但它不等价于第三方产品兼容性也不能替代鸿蒙 PC 真机验证。一、为什么必须从“系统面板出现”继续向后验证系统分享面板出现只能说明发送端发起了一个系统请求。它不能证明系统找到正确接收方不能证明 MIME 过滤正确不能证明 URI 权限已经转交更不能证明接收进程读到的是编辑器当前缓冲区。此前模拟器没有兼容 Markdown 接收 AbilityOhMarkdown 发起分享后系统最终返回No matching ability is found。这个结果验证了失败恢复却没有覆盖成功路径。成功路径至少包含六个可独立观察的事实。第一编辑器读取的是 CodeMirror 当前值而非最后一次落盘值。第二发送端在自己的缓存目录创建独立快照并完成truncate、完整写入和fsync。第三Want 的 action、type 和 stream URI 同时满足系统能力匹配要求。第四系统给目标 Ability 临时只读授权而不是让接收方永久访问发送端沙箱。第五接收端通过 Core File Kit 读取全部字节并使用严格 UTF-8 解码。第六跨应用结束后发送端仍维持未保存状态不把分享误当保存。如果只截一张系统分享页面就会把上述六条压缩成一个无法审计的视觉印象。此次验证专门增加了独立测试应用使发送端和接收端拥有不同 bundle、不同 Ability 生命周期和不同应用沙箱。这样才能把“应用内自读缓存”提升为真正的跨应用权限验证。二、受控接收器与产品代码必须隔离接收器位于仓库tools/markdown-share-receiver/它不是 OhMarkdown 的 entry 模块也不会随产品 HAP 打包。测试工程使用独立 bundlecom.example.ohmarkdown.sharereceiver只支持2in1其目标是给模拟器提供一个行为明确、可复现、可审计的接收方。根目录脚本scripts/build-share-receiver.sh负责使用同一 DevEco Studio SDK 独立构建测试 HAP。这种隔离有三个价值。其一接收成功不能借助 OhMarkdown 自身的内存对象正文必须经过系统跨进程 URI。其二测试能力不会扩大产品权限清单正式应用仍不需要 INTERNET也不会因为测试而声明接收外部分享。其三接收端可以主动采用更严格的读取限制从而暴露发送协议中的 MIME、URI 或编码错误。接收器的模块声明只匹配明确的发送动作和 Markdown 类型{ name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ets, exported: true, skills: [ { actions: [ ohos.want.action.sendData ], uris: [ { scheme: file, type: text/markdown }, { scheme: datashare, type: text/markdown } ] } ] }这里没有使用宽泛的*/*。如果接收器接受任意类型发送端即使错误地标记为纯文本或二进制也可能“碰巧成功”测试就失去了协议约束。file与datashare两种 scheme 则用于兼容 HarmonyOS 系统分发时可能提供的授权 URI 形式接收端不会自行推导原始路径。三、未保存正文才是分享一致性的关键语料对编辑器而言最危险的分享错误不是调用失败而是调用成功却分享旧内容。用户打开一个磁盘文件继续输入但没有保存此时内存正文、磁盘正文和恢复快照可能分别处于不同修订号。若分享服务重新从文件 URI 读取就会静默发送旧版本界面却没有任何异常。因此设备验证没有使用已经保存的样例文件而是在Untitled.md中输入唯一标记3G_08_MARKDOWN_SHARE_20260721保持标签脏标记再从 OhMarkdown 的导出设置中触发“分享 Markdown”。这个字符串全为 ASCII字符数与 UTF-8 字节数都应是 29便于第一轮快速核对链路是否发生截断、额外换行或编码替换。后续真机矩阵还应增加中文、组合字符、emoji、CRLF 和接近大小上限的语料但唯一 ASCII 标记更适合定位首次跨进程接通问题。发送端的正确事实来源是当前编辑会话constmarkdown:stringthis.getCurrentDocumentContent();constsharedUri:stringawaitthis.exportService.createMarkdownShareSnapshot(this.context,this.currentDocumentName,markdown);awaitthis.context.startAbility({action:ohos.want.action.sendData,type:text/markdown,uri:sharedUri,parameters:{ability.params.stream:sharedUri},flags:wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION});这段示意对应当前仓库的真实实现边界内容来自编辑缓冲区分享对象是缓存快照Want 同时携带 Markdown MIME 和 stream URI并只授予读取能力。分享不会修改文档的原 URI、保存基线或脏状态。即使目标应用启动失败用户也可以继续编辑和再次选择目标。四、为什么不能直接分享工作区原文件直接把工作区原文件 URI 交给目标应用看似省掉一次写入却会混淆两个授权域。用户授予 OhMarkdown 的工作区访问权不代表用户同意任何接收应用获得同样的长期访问。原文件还可能包含旧版本因为当前内容尚未保存。更糟的是某些目标应用可能持有 URI 较长时间导致编辑器难以说明权限生命周期。独立缓存快照把分享语义变为“本次用户动作产生的一份只读内容”。它有明确文件名、明确正文和受控清理策略。当前实现只清理严格匹配share-*.md的普通文件跳过目录、符号链接和无关缓存最近最多保留 32 份并清理超过 24 小时的旧快照。当前正要交给系统的 URI 始终保留清理失败只记录为尽力而为不反向破坏本次分享。这仍不是绝对保密机制。任何获得临时 URI 的接收方都可以在授权有效期间复制正文这正是用户主动分享的含义。安全目标不是阻止目标读取而是把读取限制在用户明确选择的快照避免额外暴露工作区结构和其他文档。五、接收端只相信 Want 中的受限输入接收 Ability 可能通过首次启动的onCreate收到 Want也可能在已经存在的进程中通过onNewWant收到后续分享。如果只处理首次启动第二次分享可能继续显示旧正文如果每次启动都清空界面但异步读取结果发生乱序又可能让早先请求覆盖后来请求。当前测试应用规模很小先同时覆盖两个生命周期入口后续多分享并发测试可以再增加代际编号。URI 的读取只接受want.uri或系统约定的ability.params.stream字符串privategetShareUri(want:Want):string{if(want.uriwant.uri.length0){returnwant.uri;}conststreamwant.parameters?.[ability.params.stream];returntypeofstreamstring?stream:;}privateasyncreceiveMarkdown(want:Want):Promisevoid{constsharedUri:stringthis.getShareUri(want);if(want.type!text/markdown||sharedUri.length0){AppStorage.setOrCreate(receiverStatus,未收到有效 Markdown 分享);return;}// 仅在类型与 URI 同时成立时继续读取。}接收端不会把 parameters 中的任意对象转成路径也不会根据显示文件名拼接沙箱地址。严格比较text/markdown可以及时暴露发送端协议退化。生产级通用接收器可能还要支持text/plain等兼容输入但这个受控工具的目的不是提高匹配率而是证明 OhMarkdown 按约定发送了准确类型。六、临时 URI 读取必须经过 Core File Kit跨应用 URI 不是普通 POSIX 路径。接收端不应裁剪file://前缀后自行拼路径也不应假设发送端缓存目录在自己的文件系统命名空间可见。正确方式是把系统分发的 URI 直接交给 Core File Kit由系统同时校验临时授权和目标进程身份。读取实现使用READ_ONLY | NOFOLLOW在打开后先获取 stat并拒绝超过 4 MiB 的验证输入privateasyncreadUtf8Content(sharedUri:string):PromiseReceivedMarkdown{constfileawaitfileIo.open(sharedUri,fileIo.OpenMode.READ_ONLY|fileIo.OpenMode.NOFOLLOW);try{conststatawaitfileIo.stat(file.fd);if(stat.size0||stat.size4*1024*1024){thrownewError(分享文件超过 4 MiB 验证上限);}constbytesnewUint8Array(stat.size);lettotalBytesRead:number0;while(totalBytesReadstat.size){constrequestedBytesMath.min(64*1024,stat.size-totalBytesRead);constchunknewArrayBuffer(requestedBytes);constbytesReadawaitfileIo.read(file.fd,chunk,{length:requestedBytes});if(bytesRead0){thrownewError(分享文件读取未完成);}bytes.set(newUint8Array(chunk,0,bytesRead),totalBytesRead);totalBytesReadbytesRead;}constdecoderutil.TextDecoder.create(utf-8,{fatal:true,ignoreBOM:false});return{content:decoder.decodeToString(bytes),bytes:totalBytesRead};}finally{awaitfileIo.close(file);}}NOFOLLOW避免测试接收器无意跟随符号链接4 MiB 是验证工具自身的防御上限不是 OhMarkdown 产品文档上限64 KiB 分块循环避免把一次read误认为必然返回完整文件。fatal: true让非法 UTF-8 明确失败不用替换字符掩盖字节错误。finally 关闭句柄保证成功、短读、解码失败和界面异常都不泄漏文件描述符。七、字符数与字节数必须同时显示只比较 UI 文本“看起来一样”不够稳健。JavaScript/ArkTS 字符串长度使用 UTF-16 代码单元而文件大小来自 UTF-8 字节中文和 emoji 会让两者不同。接收器同时展示content.length与实际读取字节数因此后续语料可以发现编码边界问题。本轮标记由 29 个 ASCII 字符组成期望值恰好是29 字符 / 29 字节。截图中接收器显示文件share-Untitled.md、成功状态、精确计数和完整标记设备 hilog 只输出Markdown received: 29 characters, 29 bytes。日志故意不包含正文、完整 URI 或路径这样证据既可判断读取结果又不会把用户内容复制到系统日志。下一轮中文语料可以选择鸿蒙PC分享验证此时字符数与字节数会显著不同。验收应比较原始 UTF-8 哈希或明确的字节数组而不是错误地要求两个计数相等。本文记录 29/29 只是本次受控输入的事实不是协议的一般性质。八、模拟器操作链与证据闭环验证使用正在运行的 MateBook Pro 2in1 模拟器物理显示为 3120×2080。首先独立构建并安装测试接收 HAP再安装当前 OhMarkdown Debug HAP。确认bm dump中两个 bundle 分离后通过桌面界面打开 OhMarkdown在未命名文档输入唯一标记保持标签未保存状态进入设置中的导出区域并点击“分享 Markdown”。系统根据sendData、text/markdown与 URI scheme 匹配到测试接收器启动独立窗口。接收器通过临时授权 URI 读取缓存快照显示成功状态。屏幕中同时能看到 OhMarkdown、独立接收窗口和文件管理器说明结果不是 Web 页面内模拟的弹层。证据文件保存在docs/test/ohmarkdown/2026-07-20-g3-08-export-share/evidence/emulator-cross-app-share-receiver.png原图为 3120×2080 PNG。接收测试 HAP 大小为 137710 字节SHA-256 为02f26ba578a15fbe00dd7c1a6890d71bc728dea18f34ff44e115f1b7f46c20e5。它通过以下脚本复现./scripts/build-share-receiver.sh# 产物仅用于测试不进入正式包tools/markdown-share-receiver/entry/build/default/outputs/default/entry-default-unsigned.hap随后执行主仓库统一门禁Playwright44/44通过精确 10 MiB Web 保护模式回归在当前环境记录 80 ms生产单 HTML、Debug HAP 和 ArkTS UnitTestBuild 均成功。这组回归用于确认新增测试工程和证据没有改变正式产品构建不把接收工具误并入应用依赖。九、测试接收器自身也要遵守最小权限测试工具常被当成临时代码反而容易引入比产品更宽的权限。这个接收器不申请 INTERNET不读取剪贴板不访问账号不扫描公共目录也不声明后台任务。它只在系统显式启动 Ability 时读取一个 Want 提供的 URI。正文仅保存在当前界面的 AppStorage 中hilog 只记录公开计数。接收器的exported: true是系统跨应用启动所必需的攻击面因此必须和精确 skills、MIME 校验、大小上限、严格解码共同使用。如果它接受任意 action、任意 URI 并在日志打印正文模拟器验证虽然方便却会建立错误的安全示范。受控工具的价值就在于用很小的代码把授权边界写清楚。HAP 是未签名 Debug 测试产物只用于当前模拟器。仓库通过嵌套.gitignore排除.hvigor/、entry/build/、oh_modules/和本地配置提交只包含可审查源码、构建脚本与真实截图。正式构建不会引用该工程。十、成功读取不等于兼容所有目标应用这次接收成功的含义非常具体当前 HarmonyOS API 24 模拟器能够把 OhMarkdown 创建的 Markdown 快照 URI 和临时读权限交给另一个独立 bundle接收方能完整读出 29 字节。这已经关闭“模拟器完全没有兼容接收方无法观察成功路径”的局部证据缺口。它不能证明三个更大的结论。第一第三方笔记、邮件、聊天或云盘应用可能只声明text/plain、只读取PARAMS_STREAM数组或对文件名、URI scheme 有额外约束。第二真机系统的分享选择器、目标排序、跨应用权限生命周期和多窗口行为可能与模拟器不同。第三接收方可能异步延迟读取如果发送端过早删除当前快照会出现启动成功但稍后读取失败。因此 G3-08 仍然是“条件保留”。受控跨应用分享已经通过真正的第三方接收应用和鸿蒙 PC 真机仍是 RC 门槛。竞争优势记分卡也不能因为这次测试从 2 分直接升到 4 分。证据边界清楚比把一次成功包装成全面兼容更重要。十一、分享缓存生命周期与临时授权的配合发送端当前最多保留最近 32 份或 24 小时内的分享快照。这一策略控制的是本地敏感内容留存不直接定义系统授予目标的 URI 权限持续时间。两者必须分开理解文件存在不代表目标仍有权读取目标有短暂授权也不代表文件可以在交付前删除。当前策略的关键点是清理时永远跳过本次将要分享的 URI。完成fsync后再启动系统 Ability避免目标读到半写文件。其他旧快照按修改时间和数量清理mtime 在 HarmonyOS 模拟器上可能返回秒级服务已把秒与毫秒统一后通过连续 36 份设备测试最终保留 32 份。受控接收器的即时读取证明当前时序可用但还应设计延迟目标接收 Ability 启动后等待 1 秒、10 秒和进程重启再读取观察系统授权与缓存清理的组合行为。该测试必须在真机和至少一个真实第三方目标上执行才能决定 24 小时是否只是隐私上限还是还需增加“系统返回后延迟清理”的兼容策略。十二、失败模型比成功截图更有工程价值跨应用分享可能在五个阶段失败能力匹配失败、目标启动失败、URI 授权失败、文件读取失败、UTF-8 解码失败。发送端能直接观察的通常只有前两项后面三项发生在目标进程中。没有受控接收器时错误很容易被归因于“模拟器问题”无法知道协议到底走到哪一步。接收器把后三类错误转成界面状态并在不暴露正文的前提下记录错误说明。短读会得到“分享文件读取未完成”超过 4 MiB 会得到明确上限错误非法 UTF-8 会由 fatal 解码抛出错误后正文区域清空。这样测试不会残留上一次成功内容让观察者误认为本次也成功。发送端仍需保持自己的恢复语义失败不能清空编辑器不能移除脏标记不能把缓存 URI写入文档会话也不能把“分享失败”记成“保存失败”。此前无匹配 Ability 的模拟器路径已经验证应用可继续编辑本轮成功路径补齐了对称证据。十三、后续语料矩阵单一 ASCII 标记适合接通链路不足以覆盖 Markdown 文档真实复杂度。下一组自动化和真机用例应至少包含以下语料UTF-8 BOM 与无 BOM中文标题和中文文件名emoji 与代理对LF、CRLF 和混合换行空文档末尾有无换行包含 NUL 或非法 UTF-8 的拒绝路径1 MiB 与接近 4 MiB 的分块读取包含图片相对链接但不携带图片的标准 Markdown同一目标连续接收两次以覆盖onNewWant。每项都应记录发送缓冲区 UTF-8 SHA-256、接收字节 SHA-256、字符数、字节数、目标 bundle、系统版本与时间。对文本保真而言哈希比截图更强截图用于证明真实系统窗口和用户路径哈希用于证明字节级一致。两者互补不能相互替代。还应加入拒绝语料text/plain不应被这个严格测试接收器匹配无 URI 的 Want 应显示无效分享超限文件应拒绝符号链接 URI 应因NOFOLLOW失败目标取消或被强杀后发送端正文不变。只有把失败路径也纳入分享才是可靠能力而不只是一次演示。十四、与 PDF 闸门的关系G3-08 同时包含 HTML、PDF、PNG 和分享但四类输出的系统依赖不同。HTML 与 PNG 已经通过系统选择器写入并分别由独立浏览器和图片查看器打开Markdown 现在又完成受控跨应用接收PDF 仍停留在系统打印预览因为当前模拟器显示“无可用打印机”无法生成实际 PDF 文件。分享接收器不会帮助关闭 PDF 闸门。应用复用 ArkWeb 打印适配器与 HarmonyOS 系统打印服务必须在提供“保存为 PDF”或可用打印目标的环境生成真实产物再由独立阅读器打开核对分页、字体、公式、图表、代码块、主题与危险内容净化。两条系统链应分别报告不因分享成功而合并判定。这也是阶段状态保持“研发条件通过、公开发布资格未通过”的原因。没有真机时可以继续 G4 的硬件无关开发但不能删除 PDF、真实第三方接收方和真机矩阵这些 RC 硬门槛。十五、可复现的最小验证清单复现本次结果时先构建 OhMarkdown 与接收器两个 HAP确认 bundle 不同安装后清空旧任务启动 OhMarkdown输入一个从未落盘的唯一标记并记录预期 UTF-8 字节发起text/markdown分享在独立接收器核对文件名、字符数、字节数与正文回到 OhMarkdown 核对脏标记和编辑能力最后执行主仓库统一测试。验收记录至少包括下面这些字段发送应用com.example.ohmarkdown 接收应用com.example.ohmarkdown.sharereceiver 动作ohos.want.action.sendData MIMEtext/markdown 输入3G_08_MARKDOWN_SHARE_20260721 预期29 字符 / 29 字节 实际29 字符 / 29 字节 平台HarmonyOS 6.1.1 / API 24 / MateBook Pro 2in1 模拟器 结论受控跨应用读取通过真机与真实第三方目标待验任何一项缺失都应降低结论强度。例如只有接收端截图而没有发送端未保存状态就不能证明缓冲区一致性只有 hilog 而没有独立 bundle 信息就不能排除应用内调用只有模拟器成功而没有环境标记就不能写成鸿蒙 PC 真机兼容。十六、工程结论本轮最大的进展不是增加一个测试窗口而是把 G3-08 的系统分享从“发送端调用与失败恢复已验证”推进到“模拟器受控跨应用权限链已验证”。独立 bundle 按精确 MIME 被系统匹配通过临时 URI 读取 OhMarkdown 尚未保存的 29 字节快照严格 UTF-8 解码后正文一致测试应用不联网、不读取原工作区、不记录正文并对文件大小、符号链接和短读设置防线。与此同时证据没有被夸大。这个接收器是仓库内可审计的工程测试工具不是真实第三方软件模拟器不是鸿蒙 PC 真机即时读取不能替代延迟读取生命周期分享成功不能替代系统 PDF 产物。因此当前可以确认方案和权限链成立可以继续后续开发却仍需在 RC 前完成真实目标应用、两档真机、复杂语料和实际 PDF 的独立验收。对本地优先 Markdown 编辑器而言这种结论边界本身就是可靠性的一部分每次只声称证据真正覆盖的范围让代码、设备、截图、哈希和阶段门槛能够互相对照。这样开发速度不会被暂缺真机无限冻结产品质量也不会因为“模拟器看起来能用”而提前透支。