Skip to content

路径与文件系统

经 bridge 通道提供 Electron app.getPath 等价 + userData 内最小 fs 能力。前端读写持久化数据无需消费方自建桥接,WindowTem 自动绑槽,子类零改。

零业务依赖——fs 用 STL <filesystem>/<fstream> + base64,不碰消费方的 HFile/HJson,互不冲突。

设计取舍

维度选择
范围白名单最小集:fs 操作强制限 userData 子树,前端不可越界写系统区
userData%APPDATA%\<exe 名>(Electron 标准,多用户隔离),首调自动建
getPathuserData / appData / home / temp / exe 五个
fs 操作readFile / writeFile / exists / mkdir / listDir / remove 六个
编码readFile 返 base64(支持二进制),writeFile 入参 base64
上限readFile 16 MB,超返 {error:"file too large"}(大文件走 showSaveDialog

getPath(同步语义)

前端 bridge.call 统一返 Promise(IPC 异步),getPath C++ 端无 IO 秒返,仍需 await

js
const ud  = await bridge.call('getPath', 'userData');  // %APPDATA%\<exe名>
const app = await bridge.call('getPath', 'appData');   // %APPDATA%
const home= await bridge.call('getPath', 'home');      // %USERPROFILE%
const tmp = await bridge.call('getPath', 'temp');      // %TEMP%\
const exe = await bridge.call('getPath', 'exe');       // exe 绝对路径

非法 name 返 null

fs(Promise 语义)

js
// 写读往返(base64)
await bridge.call('fs.mkdir', 'cache/thumb');
await bridge.call('fs.writeFile', 'cache/config.json', btoa(JSON.stringify({a:1})));
const r = await bridge.call('fs.readFile', 'cache/config.json');
// r = { content: 'eyJhIjoxfQ==', size: 7 }
const data = JSON.parse(atob(r.content));   // {a:1}

// 查询
await bridge.call('fs.exists', 'cache/config.json');        // {exists:true}
JSON.stringify(await bridge.call('fs.listDir', 'cache'));
// {"entries":[{"name":"config.json","isDir":false,"size":7},{"name":"thumb","isDir":true,"size":0}]}

// 删除(幂等)
await bridge.call('fs.remove', 'cache/config.json');       // {ok:true}
await bridge.call('fs.remove', 'cache/config.json');       // {ok:true}(不存在=ok)

path 参数

  • 相对路径(推荐):相对 userData 解析。'cache/config.json'<userData>/cache/config.json
  • 绝对路径:须落在 userData 子树内,否则拒。

安全(白名单强制)

所有 fs 操作经 safe_path::resolveAndCheck

  1. 相对路径相对 userData 解析
  2. weakly_canonical + lexically_normal 归一(消 ..、符号链接、UNC)
  3. 归一后必须落在 userData 子树内(逐段比较,防 userDataX 前缀欺骗)
越界尝试返回
'../../etc/passwd'{error:"out of userData"}
'C:/Windows/x'{error:"out of userData"}
符号链接逃逸 userData{error:"out of userData"}
exists 越界{exists:false}(不泄露是否存在)

错误模型

操作成功失败
readFile{content, size}{error:"not found"|"file too large"|"read failed"}
writeFile{ok:true}{error:"invalid base64"|"write failed"}
exists{exists:bool}不抛(越界返 false)
mkdir{ok:true}(幂等){error:"mkdir failed"}
listDir{entries:[{name,isDir,size}]}{error:"not a dir"}
remove{ok:true}(幂等){error:"dir not empty"|"remove failed"}

幂等性

  • mkdir:目录已存在 → {ok:true}(等同成功)
  • remove:文件/空目录不存在 → {ok:true}(目标态达成)

实现层

文件职责
detail/paths.hppexePath/exeDir/appName/knownFolder + getPath(name) + userData ensureDir
detail/safe_path.hppresolveAndCheck 白名单校验(纯函数)
detail/fs.hppread/write/exists/mkdir/listDir/remove(namespace fsx
detail/base64.hppbase64 编解码
window_tem.hppbindPublicSlots() 自动绑 7 槽(getPath + fs.*

槽在 bindPublicSlots(ctor/assignWindow 自动绑),与子类 initBridge(业务槽)分离 → name 不冲突,子类零改继承。

与 Electron 对比

Electron本框架
app.getPath('userData')await bridge.call('getPath', 'userData')
fs.promises.readFile(p)await bridge.call('fs.readFile', p)(base64)
fs.promises.writeFile(p, buf)await bridge.call('fs.writeFile', p, btoa(buf))
任意路径限 userData 子树(白名单安全边界)

下一步