路径与文件系统
经 bridge 通道提供 Electron app.getPath 等价 + userData 内最小 fs 能力。前端读写持久化数据无需消费方自建桥接,WindowTem 自动绑槽,子类零改。
零业务依赖——fs 用 STL <filesystem>/<fstream> + base64,不碰消费方的 HFile/HJson,互不冲突。
设计取舍
| 维度 | 选择 |
|---|---|
| 范围 | 白名单最小集:fs 操作强制限 userData 子树,前端不可越界写系统区 |
| userData | %APPDATA%\<exe 名>(Electron 标准,多用户隔离),首调自动建 |
| getPath | userData / 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:
- 相对路径相对 userData 解析
weakly_canonical+lexically_normal归一(消..、符号链接、UNC)- 归一后必须落在 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.hpp | exePath/exeDir/appName/knownFolder + getPath(name) + userData ensureDir |
detail/safe_path.hpp | resolveAndCheck 白名单校验(纯函数) |
detail/fs.hpp | read/write/exists/mkdir/listDir/remove(namespace fsx) |
detail/base64.hpp | base64 编解码 |
window_tem.hpp | bindPublicSlots() 自动绑 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 子树(白名单安全边界) |