# textkit.utf16：UTF-16LE 文件编辑

`textkit.utf16` **操作文件，不操作传入的内存文本**。适用于已确认使用 UTF-16LE（`codepage=1200`）的 Windows 工具 Unicode 日志、文本输出和配置文件。Windows 中“Unicode”并不总是指 UTF-16LE；请先确认来源，不要仅凭文件名或扩展名推断编码。

- **磁盘：** 固定 UTF-16LE；支持带 `FF FE` BOM 或无 BOM。
- **内存：** 参数、读取结果、补丁、预览与诊断都是 UTF-8。
- **接口：** 与 `textkit.ansi` 相同，只有 `read/patch/edit/save(args)` 四个文件入口；每次必须指定 `path`。
- **隔离：** 不改变 Autos 的 UTF-8 工具、`textkit.ansi` 或 `textkit.patch/editor`。在 Autos 中按需通过 `execute_code` 调用，并 `return` 结果表。
- 内存文本直接使用 `textkit`、`textkit.patch`、`textkit.editor`，无需本库。

## 编码与 BOM 规则

1. 新建文件默认写入 UTF-16LE BOM（`FF FE`），便于 Windows 工具识别。新建空文本只有 2 字节 BOM。
2. 读取不把文件开头的 BOM 作为正文，不影响第一行匹配和行号。
3. 编辑或覆盖已有文件，保留原来的有/无 BOM 状态；零字节原文件按无 BOM 空文本处理，不擅自增加 BOM。
4. `bom` 是读取及成功写操作结果中的布尔字段，**不是可设置参数**。不提供 `codepage`、`encoding`、`bom` 或 `compile` 参数。
5. 只支持 UTF-16LE，不猜测编码、不自动切换 UTF-16BE/UTF-32，也不修复乱码。带 `FE FF` BOM 的 UTF-16BE 文件明确拒绝。
6. 拒绝奇数字节、不配对的代理码元及正文中的 U+0000，避免转换器用替换字符悄悄修复原文。正常 UTF-16 零字节不受影响；emoji、扩展汉字和合法的 U+FFFD 均可使用。
7. 无 BOM 文件不能写入以 U+FEFF/U+FFFE 开头的正文，否则会与 BOM 混淆；这种修改会被拒绝，不会静默丢字或改变端序。有 BOM 文件允许正文以这些字符开头，读取只去掉最外层文件 BOM。

本库不是编码转换或二进制文件修复工具。覆盖前也会验证原文件，不能用 `save` 绕过错误编码保护。

## 四个文件入口

所有入口都接收单个参数表，成功和失败都返回结果表，首先检查 `ok`。

| 入口 | 用途 | 参数 |
|---|---|---|
| `read` | 有界 UTF-8 视图，供 AI 查看、定位 | `path, startLine, lineCount, maxBytes, lineNumbers` |
| `patch` | 精确 SEARCH/REPLACE 补丁 | `path, patches, backup, dryRun, previewMaxBytes` |
| `edit` | 行、锚点、区间及批量字面编辑 | `path` 加 `edit_text_file` 的编辑字段，不接受 `compile` |
| `save` | 新建或明确全文覆盖，不追加 | `path, content, backup, dryRun` |

未知或不适用的编辑参数会被拒绝；不编译或执行文件。

## 读取 Unicode 日志

```aardio
import textkit.utf16;
return textkit.utf16.read({
    path = "/logs/unicode.log";
    startLine = 1;
    lineCount = 80
});
```

默认 `startLine=1`、`lineCount=120`、`maxBytes=20000`、`lineNumbers=true`。

- `data` 是带行号的 UTF-8 展示文本；复制 SEARCH 或旧片段时去掉行号。
- `lineNumbers=false` 去掉行号，但行之间仍用 LF 展示，可能被裁剪。**这不是无损全文加载，不能把结果直接用于 `save` 覆盖原文件。**
- `startLine/endLine` 是实际展示的行区间；没有展示行时 `endLine=startLine-1`。
- `truncated` 表示还有未展示的后续内容，或展示行被裁剪；`clipped` 专指字节预算裁剪。
- `maxBytes` 按返回的 UTF-8 字节数计算，包括行号和展示换行；不会切开 UTF-8 字符，`maxBytes=0` 可仅取元数据。
- `originalBytes` 是含 BOM 的磁盘字节数，`textBytes` 是实际返回的展示文本字节数，`bom` 表示原文件有无 BOM。
- CRLF、LF、CR 都按换行计数；空文本有第 1 行，末尾换行后还有一个空逻辑行，与 `editor.replaceLines` 一致。起始行超范围返回 `line_out_of_range`。

**分页只限制输出，内部仍完整读取和解码文件。** 本库不是流式大日志读取器或 tail 工具。持续追加的日志可能读到半个码元或半个代理对，此时返回 `invalid_text`，不会自动补字节或忽略尾部。

## 精确补丁

```aardio
import textkit.utf16;
var patches = "<<<<<<< SEARCH
[信息] 开始处理
=======
[信息] 开始构建
>>>>>>> REPLACE";

return textkit.utf16.patch({
    path = "/logs/unicode.log";
    patches = patches;
    dryRun = true
});
```

确认预览后去掉 `dryRun` 或设为 `false` 再提交。补丁中的中文等内容是普通 UTF-8，不要手工转成 UTF-16，也不要包含文件 BOM。

语义与 `patch_text_file` 一致：

- 只传独占行的 `<<<<<<< SEARCH / ======= / >>>>>>> REPLACE` 块，不传路径行。
- 所有块匹配同一份修改前正文，后块不能搜索前块输出。
- SEARCH 非空、恰好匹配一处，匹配区间不能重叠；任一块失败整单不写。
- 字面、区分大小写和空白，仅 CRLF/LF/CR 差异等价；旧片段原样复制，不 trim/dedent，不做模糊回退。
- 插入时保留锚点并增加内容；删除时 REPLACE 留空。正文含补丁标记行时使用 `edit`。

## 结构化编辑

```aardio
import textkit.utf16;
return textkit.utf16.edit({
    path = "/logs/unicode.log";
    operation = "replace";
    oldText = "旧提示";
    newText = "新提示";
    expectedCount = 2;
    dryRun = true
});
```

| operation | 定位字段 | 写入字段 |
|---|---|---|
| `replace` | `oldText` | `newText` |
| `insertBefore` / `insertAfter` | `anchor` | `newText` |
| `replaceBetween` | `beginAnchor, endAnchor` | `newText` |
| `deleteBetween` | `beginAnchor, endAnchor` | 不传 `newText` |
| `replaceLines` | `startLine`，可选 `endLine, expectedOldText` | `newText` |

- 默认要求唯一；`expectedCount=N` 是准确次数断言，不是替换上限或第 N 个。`replaceAll=true` 替换全部非重叠命中，仍可通过 `expectedCount` 断言。零命中失败。
- `newText` 必须显式提供；空字符串表示删除，插入不自动加换行。
- 区间默认保留锚点，`includeAnchors=true` 才一起替换。`anchor` 可代替 `beginAnchor`，不要同时传冲突值；嵌套或缺尾会拒绝。
- `replaceLines` 是从 1 开始、两端包含的整行范围，空字符串删除整行。新文本缺少末尾 EOL 时保留原区间末尾 EOL；`expectedOldText` 可以带或不带该 EOL。
- `preserveEol` 默认 `true`，只忽略换行差异，新内容适配局部 EOL；`false` 禁用换行等价及适配，但不取消整行替换的 EOL 衔接规则。
- 只传当前操作需要的字段，不填充无关参数。

## 新建或全文覆盖

```aardio
import textkit.utf16;
return textkit.utf16.save({
    path = "/logs/unicode.log";
    content = '[信息] 开始处理\r\n[信息] 处理完成 😀\r\n'
});
```

`content` 为 UTF-8 字符串，落盘才转 UTF-16LE。新文件带 BOM，已有文件保留 BOM 状态；空字符串表示空文本。既不追加，也不统一换行、不自动创建父目录。控制字符在 aardio 中用单引号，例如 `'\r\n'`。

已有文件的局部修改优先 `patch/edit`；`read` 展示结果不能当作 `save` 的完整内容来源。

## 返回结果与文件保护

写接口常见字段：

- `ok`：操作是否成功；失败查看 `code/error/hint`。
- `written`：是否实际提交；dryRun 或无变化时为 `false`。
- `changed`：拟写入字节与原文件是否不同；新建空文本也为 `true`。
- `dryRun`：是否仅验证和预览。
- `bom`：拟写入文件是否带 BOM；无变化时为原状态。
- `originalBytes/resultBytes`：含 BOM 的磁盘 UTF-16LE 字节数，不是内存 UTF-8 大小。
- `backupPath`：实际保留的原始字节备份。
- `count/changedCount/locations/preview`：`patch/edit` 引擎返回的匹配数、文本改动数、位置和预览。

`previewMaxBytes` 默认 1024，范围 0～4096，0 禁止文本回显。预算按 UTF-8 计算，仅限制回显，不限制实际编辑数量。诊断候选只供定位，不能授权模糊写入。

文件提交规则沿用 `textkit.ansi`：

1. `backup` 默认 `true`，备份名为 `<path>.utf16.bak.<时间戳>.<随机后缀>`，备份不经过转码。
2. `dryRun` 无备份或暂存；无字节变化不写入。`patch/edit` 无文本变化时直接保留原字节。
3. 同目录独占创建暂存文件，写入、刷新、字节校验后提交，不先截断目标。
4. 提交前复查原始字节，有变化则取消。**这不是跨进程锁**，复查与替换之间仍有短竞争窗口，不承诺多文件事务或断电持久性。
5. 已有文件通过 `ReplaceFileW` 替换，新文件移动提交且不覆盖并发创建的目标。`backup=false` 也先保留恢复原件，成功后才删除；未删除的备份仍返回 `backupPath`。
6. 特殊提交故障可能返回 `recoveryRequired/backupPath/stagedPath`；此时停止编辑，先检查和恢复，不要盲目重试。

备份恢复使用 `io.copy(backupPath,targetPath)` 原样复制。**不要让 Autos 的 UTF-8 文本编辑或回滚工具处理 UTF-16 文件及其备份。** 恢复前应确认目标没有需要保留的新修改。

编辑日志时建议先停止日志写入，或创建稳定副本；本库不会锁住日志生产进程，也不会把它改成日志清理服务。
