John-MustangGT/ford-workshop-manual-tools
GitHub: John-MustangGT/ford-workshop-manual-tools
福特维修手册Arc文件提取器,简化维修手册使用。
Stars: 0 | Forks: 0
# 福特维修手册 Arc 提取器
用于从福特维修手册 `.arc` 容器文件中提取和本地服务内容,无需使用福特专有软件的工具。处理 `BAY POD` 和 `POD BAY` 容器格式,并解压缩所有 IDICOMP 压缩条目。
## 重要
**此存储库不包含手册数据。** 您必须提供自己的福特维修手册 CD-ROM 复制件。这些工具是在针对 **2012-2014 福特维修手册**(主要是 S197Mustang)开发和测试的,并且应该适用于同一光盘覆盖的其他车辆和年份。其他版本的维修手册可能使用相同的格式且无需修改即可工作,但尚未经过测试。
福特维修手册软件和所有服务内容均为福特汽车公司所有。
## 使用方法
```
python3 extract_baypod.py [ ...] [-o ] [-v] [-l] [-e ]
```
**参数:**
| 标志 | 描述 |
|------|-------------|
| `input` | 一个或多个 `.arc` 文件,或包含 `.arc` 文件的目录 |
| `-o DIR` | 输出目录(默认:`extracted`) |
| `-v` | 详细信息:打印每个提取的文件名 |
| `-l` | 列出内容而不提取 |
| `-e EXT` | 仅提取具有此扩展名的文件(可重复,例如 `-e .htm -e .svg`) |
**示例:**
```
# 从 arc 文件夹中提取所有内容
python3 extract_baypod.py content/useni4 -o extracted
# 列出单个 arc 的内容
python3 extract_baypod.py content/useni4/SEB.arc -l
# 仅提取 HTML 和 SVG 文件
python3 extract_baypod.py content/useni4 -o extracted -e .htm -e .HTM -e .svg
```
每个 `.arc` 文件都提取到以 arc 命名的单独子目录中(例如 `extracted/SEB/`)。
## LLM 训练导出
如果您想要一个按节级的训练语料库而不是 Obsidian 保险库,请使用:
```
python3 -m pip install 'markitdown[pdf]'
python3 build_training_data.py --arc SEB
```
这将遍历提取的节目录,使用 MarkItDown 转换文档文件,并在 `training_export//` 下写入 JSONL 语料库和清单。默认包含 HTML、PDF、WCF、XML 和 TXT 文件;使用 `--extensions` 来缩小或扩大集合。
## RAG 知识导出
对于针对 `Qwen:latest` 调优的 Open WebUI 知识导入,请使用分块 RAG 导出器:
```
python3 build_rag_knowledge.py --arc SEB
```
这将写入 `rag_export//chunks/` 下的重叠 markdown 块,并在已知时包含配对的布线 arc。默认块大小为 `900` 个单词,重叠 `120` 个单词,这可以将维修程序和诊断上下文保持在一起,同时不会使检索单元太大。如果需要,请使用 `--target-words` 和 `--overlap-words` 来重新调整块大小。
## 格式参考
### BAY POD 容器(`.arc`)
魔数:`BAY POD\x02`(8 字节)
```
Offset Size Description
------ ---- -----------
0 8 Magic: "BAY POD\x02"
8 1 Padding (0x00)
9 3 Number of directory entries (3-byte LE)
12 1 Padding (0x00)
13 3 Filename table size in bytes (3-byte LE)
16 N*16 Directory entries (16 bytes each)
16+N*16 1 Null separator byte
... ? Filename table (concatenated filenames, no separators)
... ? Data section (IDICOMP-wrapped file data)
```
**目录条目(16 字节):**
```
Byte(s) Description
------- -----------
0 Padding (0x00)
1–3 Offset into filename table (3-byte LE)
4 Padding (0x00)
5 Filename length (1 byte)
6–8 Padding (0x00 0x00 0x00)
9–12 Absolute data offset in file (4-byte LE)
13–14 Size of IDICOMP-wrapped data block (2-byte LE)
15 Padding (0x00)
```
### POD BAY 容器(`.arc`)
魔数:`POD BAY\x01`(8 字节)
用于召回/TSB 文档。具有与 BAY POD 相同的标题布局,但不含可用的文件名表——文件名是从数据中嵌入的工作单元 XML 块中推断出来的。数据部分是 IDICOMP 包装的块序列,端到端存储,没有目录;提取器扫描 `\x01IDICOMP\x01` 魔数以定位每个块。
工作单元块包含 `r98s20.htm `,它给出了主要输出文件名。其他块被分配从存档基本名和检测到的内容类型派生的名称。
## IDICOMP 格式
arc 中的每个数据块(无论是在 BAY POD 还是 POD BAY 中)都包裹在一个 IDICOMP 标题中。`\x01IDICOMP\x01` 魔数出现在数据部分,绝对偏移量由目录条目给出。
**IDICOMP 块布局:**
```
Offset Size Description
------ ---- -----------
0 9 Magic: "\x01IDICOMP\x01"
9 2 Compressed length of first chunk (2-byte LE)
11 ? First compressed chunk (see below)
... ... Additional chunks (see Multi-chunk blocks)
```
有效载荷从字节 9 立即开始。没有单独的“类型标志”字段——最初记录为这样的内容实际上是第一个压缩块的前两个字节(第一个 LZSS 控制字)。
**原始(未压缩)文件:**
对于二进制文件(JPEG、GIF、PDF 等),偏移量 9-10 的字节包含实际文件数据的第一个两个字节,而不是有效的 LZSS 长度。解压缩器通过第一个块产生越界回参考(`IndexError`)来检测这一点;在这种情况下,从偏移量 11 开始的原始字节被直接返回。
在偏移量 11 处常见的签名:
- `FF D8` → JPEG(从字节 11 存储原始数据)
- `47 49` (`GI…`) → GIF(从字节 11 存储原始数据)
- `25 50` (`%P…`) → PDF(从字节 11 存储原始数据)
- `3B 20` (`;` ` `) → WCF 文本(从字节 11 存储原始数据)
## IDICOMP 多块块
大文件作为单个 IDICOMP 块内独立压缩的块序列存储。每个块解压缩到最多 **16,384 字节**(0x4000)。在 9 字节魔数之后的格式:
```
[2-byte LE chunk_len] [chunk_len bytes of LZSS data]
[2-byte LE chunk_len] [chunk_len bytes of LZSS data]
...
[0x00 0x00] ← terminator (zero-length chunk)
```
解压缩器将所有块的输出连接起来以重建完整文件。单块文件(≤16 KB 解压缩)正好有一个块后跟终止符。
## IDICOMP 解压缩算法
从文件偏移量 `0x35fe9` 的 `TSBASP.dll` 中逆向工程。通过调试符号识别的源项目为 `D:\develop\common\idicomm2\DB.CPP`。
该算法是一个 **位导向 LZSS 变体**,具有 16 位控制字。
### 控制字处理
```
mask = 0
ctrl = 0
src = 0 (index into compressed input)
out = [] (output buffer)
loop:
mask >>= 1
if mask == 0:
ctrl = read_u16_le() # reload control word
mask = 0x8000
if (ctrl & mask) == 0:
out.append(read_byte()) # literal
else:
decode_encoded_op() # back-reference or run-fill
```
控制字是 16 位小端值。位按 MSB-first(掩码从 `0x8000` 开始并右移)测试。一个 `0` 位表示下一个字节是字面量;一个 `1` 位表示接下来是一个编码操作。
**第一个控制字也是每个块的前两个字节**(第一个块的 IDICOMP 块的字节 11-12,或后续块的第一个两个字节)。
### 编码操作
当遇到一个 `1` 位时,读取一个字节 `b`:
- `t = (b >> 4) & 0xF` — 高位,操作类型
- `m = b & 0xF` — 低位,参数
| 类型 (`t`) | 描述 |
|------------|-------------|
| 0 | **短运行填充**:读取 `fill_byte`;发出 `(m + 3)` 个副本 |
| 1 | **长运行填充**:读取 `nb`,然后 `fill_byte`;发出 `(m + (nb << 4) + 19)` 个副本 |
| 2 | **回参考,显式长度**:读取 `nb`,然后 `cl`;从 `out[current - dist]` 复制 `(cl + 16)` 个字节,其中 `dist = m + (nb << 4) + 3` |
| 3–15 | **回参考,隐式长度**:读取 `nb`;从 `out[current - dist]` 复制 `t` 个字节,其中 `dist = m + (nb << 4) + 3` |
回参考使用对已解码输出的滑动窗口。重叠的副本(其中 `dist < copy_length`)按字节逐个处理,以正确实现通过回参考的运行长度编码。
### Python 实现
```
def _decompress_idicomp(data):
src = 0
n = len(data)
out = bytearray()
mask = 0
ctrl = 0
while src < n:
mask = (mask >> 1) & 0xFFFF
if mask == 0:
if src + 2 > n:
break
ctrl = data[src] | (data[src + 1] << 8)
src += 2
mask = 0x8000
if (ctrl & mask) == 0:
if src >= n:
break
out.append(data[src])
src += 1
else:
if src >= n:
break
b = data[src]; src += 1
t = (b >> 4) & 0xF
m = b & 0xF
if t == 0: # short run-fill
ln = m + 3
fb = data[src]; src += 1
out.extend([fb] * ln)
elif t == 1: # long run-fill
nb = data[src]; src += 1
ln = m + (nb << 4) + 19
fb = data[src]; src += 1
out.extend([fb] * ln)
elif t == 2: # back-ref, explicit copy length
nb = data[src]; src += 1
dist = m + (nb << 4) + 3
cl = data[src] + 0x10; src += 1
p = len(out) - dist
for i in range(cl):
out.append(out[p + i])
else: # back-ref, copy length = type nibble
nb = data[src]; src += 1
dist = m + (nb << 4) + 3
p = len(out) - dist
for i in range(t):
out.append(out[p + i])
return bytes(out)
```
## 语料库说明(福特维修手册)
从完整安装中提取的典型内容:
| 扩展名 | 数量 | 描述 |
|-----------|-------|-------------|
| `.HTM`/`.htm` | ~36,000 | 服务程序 HTML 页面 |
| `.SVG` | ~11,000 | 布线图矢量图形 |
| `.JPG` | ~80,000 | 程序步骤照片 |
| `.GIF` | ~15,000 | 图表和图标 |
| `.XML`/`.xml` | ~18,000 | 结构化数据/零件信息 |
| `.PDF` | ~5,000 | 补充和规范表 |
| `.wcf` | ~5,600 | 工作单元控制文件(元数据) |
命名 arc 文件对应于车辆系列和年份(例如 `SEB.arc` = S197Mustang,`SE2.arc` = 布线图,`VIE.arc` = 老式参考图像)。
## 本地查看器服务器
启动本地手册查看器:
```
python3 serve.py
```
默认 URL:`http://localhost:8080/`
直接 S197 入口:
```
http://localhost:8080/SEB/SEBALPHAINDEX.HTM
```
服务器还模拟福特提取的 HTML 使用的福特路由:
- `/renderers/2colframeset.asp`
- `/tpsasps/2colframeset.asp`
- `/renderers/wiringsvg/ep_main.asp`
## 第 1 阶段离线移动应用(PWA)
存储库现在包括位于 `webapp/` 的基于浏览器的离线应用外壳。
### 1) 构建离线目录
从提取的 arc 目录生成 `webapp/offline_catalog.json`。
```
# 示例:仅 S197 工作坊 + 线路
python3 build_offline_catalog.py --only SEB EEB
# 或索引所有提取的 arc
python3 build_offline_catalog.py
```
如果您愿意,可以跳过此单独步骤,并通过 `extract_baypod.py --cache-catalog ...` 直接从提取生成相同的目录。
### 2) 启动服务器
```
python3 serve.py
```
### 3) 打开离线应用
```
http://localhost:8080/offline_app.html
```
### 4) 在离线前缓存书籍
在应用中:
1. 选择一个 arc(例如 `SEB`)
2. 点击 **下载 Arc 以供离线使用**
3. 为其他 arc(例如 `EEB`)重复操作
4. 在在线时打开一些重要页面,以便缓存动态渲染器 URL
### 5) 在设备上安装
- **iPad(Safari)**:共享 -> 添加到主屏幕
- **Android(Chrome)**:安装提示或浏览器菜单 -> 安装应用
### 注意
- 这是一个第 1 阶段 PWA 实现,旨在与您的提取文件进行本地/离线使用。
- iOS 可能会在压力下驱逐缓存的存储;在依赖断开连接的使用之前,请验证关键页面。
## Obsidian 导出管道
使用以下方法将一个提取的维修手册 arc(例如 `SEB`)转换为 Obsidian 准备好的捆绑包:
- 每个`.HTM`页面的 Markdown 笔记
- Obsidian wikilinks 用于内部页面导航
- 复制的 arc 资产(JPG/GIF/PDF/SVG/PNG)
- 可选的布线图资产从相关的 EE* arc 中复制(如果引用)
- 生成的节索引和字母索引笔记
### 1) 安装依赖项
推荐(本地虚拟环境):
```
python3 -m venv .venv
.venv/bin/python -m pip install beautifulsoup4 markdownify
```
替代(系统 Python):
```
python3 -m pip install beautifulsoup4 markdownify
```
### 2) 构建 Obsidian 捆绑包(SEB 示例)
```
.venv/bin/python build_obsidian_section.py --arc SEB
```
使用显式布线 arc 覆盖:
```
python3 build_obsidian_section.py --arc SEB --wiring-arc EEB
```
从布线 arc 复制所有资产(可选;默认为仅引用):
```
python3 build_obsidian_section.py --arc SEB --wiring-arc EEB --copy-all-wiring-assets
```
导出后压缩保险库,以便轻松云/手机传输:
```
python3 build_obsidian_section.py --arc SEB --zip
```
这将生成 `obsidian_export/SEB_YYYYMMDD_HHMMSS.zip`,与保险库文件夹并列。
默认输出路径:
```
obsidian_export/SEB/
```
顶级捆绑包内容:
- `index.md`(根导航笔记)
- `pages/`(所有转换的程序)
- `assets/`(复制的引用图像/文档)
- `indexes/Section Index.md`
- `indexes/Alphabetical Index.md`
- `_meta/manifest.json`
### 3) 导入到 Obsidian
将 `obsidian_export/SEB/` 打开为保险库(或将其内容复制到现有保险库中)。
标签:逆向工具