# fsys.res：资源枚举、加载与保存

```aardio
import fsys.res;
```

主要用于枚举、加载和保存 aardio EXE 中的内嵌文件及库资源：`enum/loadRes/saveRes`。默认类型为字符串 `"RES"`，枚举内嵌库可指定 `"LIB"`。

也支持其它 EXE/DLL 的 PE 资源；`names/types/languages/list` 提供普通数组查询。

[使用 fsys.res.menu 探查菜单资源](menu.html)

## 快速上手

```aardio
import fsys.res;

// 枚举当前 EXE 的内嵌文件；回调不需要返回值。
var ok,err,code = fsys.res.enum(function(module,resType,resName){
    var data = fsys.res.loadRes(resName,resType,module);
    print(resName,#data);
});
if(!ok) return null,err,code;

// 枚举当前 EXE 的内嵌库。
fsys.res.enum(function(module,resType,resName){
    print(resType,resName);
},"LIB");
```

通常读取工程内嵌文件可直接使用 `string.load("/res/file.txt")`，它能同时处理开发期硬盘文件和发布后的内嵌资源。`enum` 用于需要列出或批量处理资源的情况；默认模块在开发期是 aardio EXE，发布后是自己的 EXE。

| 函数 | 返回值 |
| --- | --- |
| `enum(proc, resType="RES", module)` | 对每个名称调用 `proc(module,resType,resName)`；成功返回 `true` |
| `types(module)` | 全部类型的纯数组：数字 ID 或文本类型名 |
| `names(resType="RES", module)` | 指定类型下的资源名称纯数组：数字 ID 或文本名称 |
| `languages(resName, resType="RES", module)` | 真实存在的 LANGID 数值纯数组，可能包含 `0` |
| `list(resType, module)` | `{type;name;lang;size}` 项目的纯数组，每个语言版本一项 |
| `loadRes(resName, resType="RES", module, lang)` | 二进制字符串，保留所有字节，包括 NUL |
| `loadMenu(resName, module, lang)` | `{version;helpId;items}` 菜单数据表 |
| `decodeMenu(data)` | 不访问文件，直接解码 MENU/MENUEX 字节串 |

- `module` 可以是路径或 `load` 返回的有效指针；省略时使用当前 EXE 的 `_HINSTANCE`。
- 传路径时自动以 `LOAD_LIBRARY_AS_DATAFILE` 加载并释放，不执行文件代码；借用的句柄不释放。
- 查询结果按数值在前、文本在后，各自升序排列；`list` 再按语言升序排列，适合比较和序列化。
- 查询不存在的类型、名称或语言集合返回 `[]`。加载失败、参数错误等仍返回 `null, err, code`，不要用 `: []` 隐藏错误。
- `loadRes` 的空资源返回 `""`，缺失资源返回 `null, err, code`。空字符串在 aardio 中为真值。
- 查询函数返回数组，直接放在 `for ... in` 中时应加括号；更推荐先接收返回值并检查错误，再遍历。

## 名称与类型：数字不等于文本

Windows 的数值资源 ID 是 16 位无符号整数，可取 `0..65535`。数值 `7` 与文本 `"7"` 保持区分。原生 API 的 `"#7"` 数值名称约定仍按 Windows 语义处理。

`"MENU"`、`"ICON"` 等字符串不是数字类型的隐式别名，可能是真实的自定义类型名。Windows API 常量不作运行时声明，代码使用 `4/*_RT_MENU*/`、`3/*_RT_ICON*/` 或直接写数值。库中的常量条目仅供编辑器自动完成，将名称替换成数值，不依赖运行时变量。

```aardio
import fsys.res;
var file = "C:\Windows\System32\notepad.exe";
var ids,err,code = fsys.res.names(4/*_RT_MENU*/,file);
if(!ids) return null,err,code;
for i,id in ids {
    print(id); // number 或 string，没有原生资源指针
}
```

常用类型值：菜单 `4`、对话框 `5`、字符串表 `6`、快捷键表 `9`、原始数据 `10`、图标组 `14`、版本 `16`、HTML `23`、清单 `24`。类型库直接使用字符串 `"TYPELIB"`。

调用原生 API 时，`makeIntResource(value)` 负责转成原生参数；`getName(value)` 反向转换。普通查询无需使用它们。`getName` 将空原生指针解释为资源 ID `0`，将低位指针（包含 `65535`）转成整数，将有效的 UTF-16 名称指针转成 UTF-8。不要给 `getName` 传任意内存地址。

## 精确语言选择

```aardio
import fsys.res;
var file = "C:\path\resources.dll";
var langs,err,code = fsys.res.languages(100,10/*_RT_RCDATA*/,file);
if(!langs) return null,err,code;
for i,lang in langs {
    var bytes,err,code = fsys.res.loadRes(100,10/*_RT_RCDATA*/,file,lang);
    if(bytes===null) return null,err,code;
    print(lang,#bytes);
}
```

- 不传 `lang`：保留 Windows 默认资源语言选择/回退行为。
- 显式传 `lang`，包括 `0`：必须存在这个 LANGID，**绝不回退**到其它语言。
- 即使直接调用 `FindResourceExW` 也可能发生语言回退；库会先核对实际语言列表，避免“请求日语却读到了中性/英语资源”。
- 不存在的显式语言返回错误码 `1815`；资源类型或名称本身不存在时保留相应错误码。

`LANGID` 是整数，不是 `"zh-CN"` 等字符串。常见值为 `0`（中性）、`1033`（英语/美国）、`2052`（中文/中国）。

## enum 回调

调用方式为 `enum(proc,resType="RES",module)`，回调参数顺序为 `module,resType,resName`。

```aardio
import fsys.res;

fsys.res.enum(function(module,resType,resName){
    var data = fsys.res.loadRes(resName,resType,module);
    print(resName,#data);
    // 不返回值即可继续；如果只要第一个资源，明确 return false。
},"RES","C:\path\app.exe");
```

- `"RES"`、`"LIB"` 等自定义资源类型和名称为 UTF-8 **string**；数字资源类型/名称直接为 **number**。不用转换指针。
- 回调不需要返回值；只有返回值全等于 `false` 才停止后续回调，`null`、`0`、`true` 都继续。
- 与 `names` 共用一次名称收集和排序，再逐项调用普通 aardio 回调；停止不影响已经完成的名称收集。
- 正常完成、提前停止或没有匹配资源均返回 `true`；加载/参数/系统错误返回 `null,err,code`。
- 回调期间模块有效，可直接传给 `loadRes`，**不要在回调中释放它**。传路径的模块在 `enum` 返回或回调抛错后释放，不能保存该句柄留作以后使用；传入已有模块时仍由调用者负责释放。

## 手工加载模块

`load(path, flags)` 默认标志 `2`，只映射数据、不执行代码。若显式指定其它标志，则按 `LoadLibraryExW` 语义执行，特别是 `0` 可执行 DLL 入口。

只对自己从 `load` 获得的模块调用 `free`。`free(null)` 安全忽略，但它不能识别已释放的非空句柄，不要重复释放或释放 `_HINSTANCE`。

## 更新资源

```aardio
import fsys.res;
var updater,err,code = fsys.res.open("C:\path\copy.dll");
if(!updater) return null,err,code;

var ok,err,code = updater.update(10/*_RT_RCDATA*/,100,'abc\0def',2052);
if(!ok){
    updater.close(true); // 丢弃暂存修改
    return null,err,code;
}
return updater.close(); // 提交
```

- `open(path, true)` 会在提交时移除全部旧资源，属于显式的破坏性操作；通常省略第二参数。
- `update(type, name, data, lang=0)` 暂存修改；data 可以是字符串或 buffer，数据会按确切字节长度写入。
- **删除资源必须明确传 `null`**：`updater.update(10/*_RT_RCDATA*/,100,null,2052)`。
- Win32 更新 API 不支持写入零长度数据。空字符串返回错误，不会隐式删除；读取已有的空资源仍受支持。
- `close(false)`（默认）提交；`close(true)` 丢弃；重复关闭安全忽略，关闭后不能再更新。
- 原生更新成功返回 `true`，失败返回 `null, err, code`。
- 库不会代替调用者创建备份或实现文件事务。更新前应确保目标是允许修改的副本；不要修改签名系统文件或正在使用的程序，修改资源会使数字签名失效。

## 当前 EXE 的 aardio 内嵌文件

aardio 内嵌目录使用文本类型 `"RES"` 和大写虚拟路径名称，因此 `names/loadRes` 的默认类型为 `"RES"`。

`saveRes(srcDir="/", target="/")` 和 `saveAppData(resDir="/", target)` 用于导出当前程序内嵌资源目录，发布期实现直接使用 `names/loadRes`。一般读取工程文件无需使用本库，直接 `string.load("/res/file.txt")` 就能兼容开发期硬盘文件与发布后的 EXE 内嵌资源。

## 常见错误码

| code | 含义 |
| --- | --- |
| 2 / 3 | 文件或路径不存在 |
| 6 | 更新器已关闭/无效句柄 |
| 13 | 损坏或不支持的菜单模板 |
| 87 | 参数类型、范围或 NUL 错误 |
| 1812 | PE 没有资源数据 |
| 1813 | 资源类型不存在 |
| 1814 | 资源名称不存在 |
| 1815 | 指定语言资源不存在 |

没有资源的 PE 可能完全没有资源目录，也可能保留空目录；Windows 会相应返回 `1812` 或 `1813`，库保留实际错误码。

检查第一个返回值或 `===null` 即可，通常不需要 `try/catch`；不要在释放模块后才取 `lasterr()`，函数已经返回操作时的错误文本和错误码。
