pngouin/android-abx
GitHub: pngouin/android-abx
一个基于 Rust 的 Android Binary XML 解析器与编码器,用于读写设备端系统配置文件并经真实 AOSP 双向验证。
Stars: 0 | Forks: 0
# Android-ABX
[](https://github.com/pngouin/android-abx/actions/workflows/ci.yml)
[](https://crates.io/crates/android-abx)
[](https://docs.rs/android-abx)
一个用于 Android Binary XML (ABX) 的 Rust 解析器和编码器——这是 Android 的 `BinaryXmlSerializer`/`BinaryXmlPullParser` 用于设备端系统配置文件(`packages.xml`、`settings_*.xml`、`users/*.xml` 等)的二进制格式。
包含两个共享同一事件模型的解码解析器,一个反向工作的编码器(将 `Event` 或纯 XML 文本转换回 ABX 字节),可选的 `serde` 反序列化功能,以及一个与真实 AOSP 源码和真实编码文件进行双向校验的传输格式(wire format)——而不仅仅是基于本 crate 自身的测试。
```
[dependencies]
android-abx = "0.1"
# 或者,对于 serde 支持:
android-abx = { version = "0.1", features = ["serialize"] }
# 或者,要将 XML 文本编码为 ABX 字节:
android-abx = { version = "0.1", features = ["xml"] }
```
crates.io 包名为 `android-abx`;Rust 导入路径保持为 `abx`(通过 `Cargo.toml` 中的 `[lib] name` 设置),因此以下所有代码都是 `use abx::...`,而不是 `use android_abx::...`。
## 不是 AXML
如果你想解析从 APK 中提取出来的 `AndroidManifest.xml` 或 `res/**/*.xml`,那么你选错 crate 了——那是 **AXML**,一种完全不同的、基于块(chunk-based)的二进制格式,除了共享“Android 二进制 XML”这个别称之外,它与 ABX 毫无关联:
| | **ABX**(本 crate) | **AXML** |
|---|---|---|
| 用途 | 平台配置:`packages.xml`、`users/*.xml`、`settings_*.xml` | *APK 内部*的已编译资源:`AndroidManifest.xml`、`res/**/*.xml` |
| 生成者 | `BinaryXmlSerializer`,在正在运行的系统上写入文件时生成 | `aapt`/`aapt2`,在 APK *构建*时生成 |
| 传输结构 | 扁平 token 流:魔数 `ABX\0`,随后是每个 `XmlPullParser` 事件对应的一个字节,每个字节后可选地跟着一个带类型的值 | 基于块(`ResChunk_header`):一个字符串池块,一个资源映射块,然后是一个元素节点的扁平数组 |
| 属性值 | 一组封闭的原语类型——string、int、long、float、double、bool、bytes——没有“资源”的概念 | 可以是字面量,*也可以是*指向资源表的引用(`@string/foo`),在加载时解析 |
| 典型工具 | `xml2abx`/`abx2xml` | `aapt2 dump xmltree`、`apktool`、`androguard` |
`abx` 基于 `nom` 的 token 流解析器无法直接套用到 AXML 上——那需要基于块/长度前缀的解析以及一个感知资源表的解析器。
## 快速开始
遍历原始事件:
```
let data = std::fs::read("packages.abx")?;
let mut p = abx::AbxParser::new(&data)?;
while let Some(ev) = p.next_event()? {
println!("{ev:?}");
}
```
或者从任何 reader(文件、socket、pipe)进行流式读取,而无需将整个文档加载到内存中:
```
let mut p = abx::open_file("packages.abx")?;
let xml = p.to_xml()?;
```
启用 `serialize` 后,可直接反序列化为你自己的类型。如果文档的根节点*就是*你想要的记录:
```
#[derive(serde::Deserialize)]
struct Settings {
enabled: bool,
count: i32,
}
let settings: Settings = abx::from_file("settings.abx")?;
// or abx::from_slice(&bytes) / abx::from_reader(reader)
```
对于包含许多重复记录的包装器文档——例如 AOSP 的 `packages.xml` 结构,一个根节点包含许多 `` 子节点:
```
#[derive(serde::Deserialize)]
struct Pkg {
name: String,
version: Option,
}
let mut p = abx::open_file("packages.abx")?;
for pkg in p.deserialize_iter::("pkg") {
println!("{:?}", pkg?);
}
```
结构体字段通过名称映射到属性和子元素(对于不是有效 Rust 标识符的名称,使用 `#[serde(rename = "...")]`),重命名为 `$text` 的字段会捕获元素自身的直接文本内容,而 `Vec` 字段会收集重复的同名子节点。有关完整且可运行的程序,请参阅 `examples/`(`abx2xml`、`xml2abx`、`serde_pkgs`、`serde_root`),并请参阅 `src/de/mod.rs` 的模块文档以获取完整的映射规则及其与 `quick-xml` 的关系。
启用 `xml` 后,可将纯 XML 文本直接编码为 ABX 字节:
```
let bytes = abx::xml_to_abx(&xml_string)?;
std::fs::write("packages.abx", bytes)?;
```
属性值总是被编码为纯字符串——这与真实的 AOSP 自身的 `attribute()` 方法一致,该方法也绝不会从文本中推断类型;只有调用者选择的 API(`attributeInt`、`attributeBoolean` 等)才能决定类型。要编码带类型的值,请直接构建 `Event` 流并将其交给底层编码器:
```
let events = vec![
abx::Event::StartDocument,
abx::Event::StartTag {
name: "settings".into(),
attributes: vec![abx::Attribute { name: "count".into(), value: abx::AttributeValue::Int(42) }],
},
abx::Event::EndTag { name: "settings".into() },
abx::Event::EndDocument,
];
let bytes = abx::events_to_abx(&events)?;
```
## 设计
两个解析器实现共享相同的 `Event`/`Attribute`/`AttributeValue` 类型和 XML 渲染逻辑,因此它们的输出始终是一致的:
- **`AbxParser`** —— 零内存分配,直接在内存中的 `&[u8]` 上操作。
- **`AbxStreamParser`** —— 通过内部环形缓冲区从任何 `impl std::io::Read` 读取,适用于太大(或过于实时)而无法预先缓冲的文件、socket 或 pipe。
两者都提供了相同的便捷接口:`to_xml`/`write_xml`、`find_attribute`/`find_all_attributes`、`attributes_of`/`all_attributes_of`、`into_map`,以及(启用 `serialize` 后的)`deserialize_next`/`deserialize_all`,外加用于在 `AbxStreamParser` 上实现真正惰性流式处理的 `deserialize_iter`。
编码器只有一个 `AbxWriter`,而不是两个——写入操作没有环形缓冲区/重新填充的复杂性需要拆分,因此 `Vec`(内存中)和文件/socket(流式传输)共享相同的类型。其内部化的字符串池仅涵盖标签/属性的*名称*,而不包含值,这与真实的 AOSP 的通用 `attribute()` 相匹配(请参阅下文的“与真实 AOSP 进行了校验”)。
标签和属性名称(`Event::StartTag`/`EndTag` 的 `name`,`Attribute::name`)是 `InternedStr`(`smol_str::SmolStr`),而不是 `String`。传输格式会对这些内容进行内部化——文档中每个元素都会重复出现同样有限的一组名称——而且它们通常都很短,因此 `SmolStr` 会将任何长度不超过 23 字节的内容内联存储:克隆重复的名称只是一个栈拷贝,完全没有堆内存分配。在日常使用中,`InternedStr` 的行为就像一个只读的 `String`——它实现了 `Deref`,可以直接与 `str`/`&str`/`String` 进行相等性比较(`name == "pkg"` 照常工作)——它只是不能被原地修改,因为缓冲区可能是共享的。与基于 `Rc` 的替代方案不同,它也是 `Send + Sync` 的。
## 与真实 AOSP 进行了校验
传输格式直接与真实的 AOSP 进行了校验,而不仅仅是基于本 crate 自身的测试——而且是双向校验,是通过实际编译和运行 AOSP 的真实源码,而不是仅仅阅读它。
**解码:**传输常量已与 AOSP 自身的源码(`BinaryXmlSerializer.java`/`FastDataOutput.java`)以及真实的 `.abx` 文件进行了核对,而不仅仅是基于本 crate 自身合成的测试数据块。这种区分很重要:该 crate 的早期版本中,每个数据类型的半字节(nibble)都比真实协议差了一个位置,但由于测试构建器和解析器共享了同一个错误的假设,测试套件依然通过了。这个问题只有通过解码真实文件才浮出水面——在修复之前,真实的 `.abx` 解码出来只有光秃秃的 XML 声明,主体被静默丢弃,且没有任何错误。`tests/fixtures/` 中的真实测试夹具(fixtures)——现在由真实的 AOSP `BinaryXmlSerializer` 直接生成(参见下文的“重新运行 AOSP 校验”),最初是由独立的 `xml2abx` 工具生成的——通过 `tests/aosp_fixture_tests.rs`/`tests/aosp_fixture_serde_tests.rs` 在每次测试运行时进行检查,从而弥补了这一差距。
**编码:**`AbxWriter` 的传输编码直接基于 `BinaryXmlSerializer.java` 的源码,而不是通过反转解码器推断出来的——这捕捉到了简单的反转操作可能会遗漏的两个细节(`StartDocument`/`EndDocument` 携带了一个解码器从不检查的类型半字节;`StartTag`/`EndTag` 使用的是 `TYPE_STRING_INTERNED`,而不是 `TYPE_STRING`)。
**随后进行了进一步验证:**真实、未经修改的 AOSP `BinaryXmlSerializer`/`BinaryXmlPullParser` 被直接编译并运行。一份覆盖了所有 `AttributeValue` 变体、所有携带文本的 `Event` 变体以及重复名称内部化(interning)的文档,在与 `events_to_abx` 的双向转换中实现了逐字节的一致。该运行还发现了两个真实的 bug:
- **存在于 AOSP 自身的解析器中,而非本 crate**:`BinaryXmlPullParser` 无法正确读取 `BinaryXmlSerializer` 自身可能生成的 `TYPE_NULL` 文本 token(例如 `text(null)`)——它会无条件地读取长度前缀的 payload,而不先检查类型半字节,从而导致反序列化错位并静默截断文档的剩余部分。`AbxWriter` 现在总是为携带文本的事件发出 `TYPE_STRING`,这是唯一被验证为对真实解析器安全的格式。
- **存在于本 crate 自身的渲染中**:`AttributeValue::as_str()` 会将十六进制类型(`IntHex`/`LongHex`)的值渲染为原始的二进制补码(two's-complement)十六进制,仅对确切为 `u32::MAX`/`u64::MAX` 的值特殊处理为 `-1`。真实的 AOSP 的 `Integer.toString(v, 16)` 会将*任何*负数输入视为有符号数(`0xCAFEBABE` 被渲染为 `"-35014542"`,而不是 `"cafebabe"`)——现已修复以保持一致。受影响的仅是渲染出的文本形式;传输的字节和解码后的值一直都是正确的。
此外还确认:当超出 65,535 条目的上限时,真实的 AOSP 的内部化池不会报错,它只是静默停止缓存新名称,而已经内部化的所有内容都将继续正常工作——`AbxWriter` 现在与此行为保持一致,而不是返回一个硬错误。
### 重新运行 AOSP 校验
`tests/fixtures/aosp_verify/` 会重新运行上述所有操作:编译并运行真实的 AOSP 源码,在 `tests/fixtures/` 中写入每一个 `.abx` 测试夹具(由 `tests/aosp_fixture_tests.rs`/`tests/aosp_fixture_serde_tests.rs` 解码),并重新验证上述发现,并为每一项打印 PASS/FAIL。真实的 AOSP 源码及其唯一依赖(`xmlpull`)都已 vendored 到 `vendor/` 目录下(各自保留其原始许可证——参见 `vendor/NOTICE.md`),因此此构建完全可以离线进行:
```
cd tests/fixtures/aosp_verify
# 使用 podman 或 docker:检查作为构建的一部分运行,因此构建失败
# 意味着检查失败
podman build -t abx-aosp-verify -f Containerfile . # or: docker build ...
podman create --name abx-aosp-verify-tmp abx-aosp-verify
podman cp abx-aosp-verify-tmp:/work/aosp_verify.abx .
podman rm abx-aosp-verify-tmp
# 或者直接使用 PATH 上的 JDK (javac),无需容器:
./build-and-run.sh aosp_verify.abx
```
`refresh-vendored-sources.sh` 会从当前的 AOSP `main` 分支重新获取 `vendor/aosp/`(仅供维护者运行,提交前请审查 diff)——这对于捕获上游行为变更非常有用。
## 已知限制
- **`$text` 会丢弃交错的实体/空格。**`#[serde(rename = "$text")]` 便捷字段仅累积 `Event::Text`,会静默跳过在中间穿插的 `Event::EntityReference`/`Event::IgnorableWhitespace` 内容——因此 `Use "quotes" safely` 出来会变成 `"Use quotes safely"`,实体被丢弃而不是被解码。事件级别的 API(`next_event`/`to_xml`)可以处理所有这三种事件类型并能精确进行往返(round-trip)转换;只有 `$text` 这个快捷方式存在此缺陷。
- **不支持修改版的 UTF-8。**AOSP 的 `writeUTF` 通过 Java 的修改版 UTF-8(NUL 编码为 `0xC0 0x80`;星形平面/emoji 字符编码为 CESU-8 代理对)对字符串进行编码,而不是纯粹的 UTF-8。本 crate 使用 `std::str::from_utf8` 进行解码,该函数会拒绝或错误解码这些字节序列。这对于典型的配置内容(ASCII/BMP,没有嵌入的 NUL)来说无关紧要——只有极其特殊的输入才会成为真实的缺陷。
## 基准测试
```
cargo bench --bench parsing
cargo bench --bench deserialize --features serialize
cargo bench --bench encoding
cargo bench --bench xml_encoding --features xml
```
基于 `criterion`:在解码方面对比 `AbxParser` 与 `AbxStreamParser`,在编码方面对比 `AbxWriter`/`events_to_abx` 与 `xml_to_abx`,并在几种不同规模下进行测试。运行后,HTML 报告位于 `target/criterion/report/index.html`。以下是针对 10,000 个元素(约 600 KB;非提交的基准,请在本地重新运行,而非盲目信任这些数据)的一次本地运行结果:
| 基准 | 时间 |
|---|---|
| `parse_events/AbxParser` | 2.71ms |
| `parse_events/AbxStreamParser` | 3.04ms |
| `to_xml/AbxParser` | 2.24ms |
| `deserialize_all/AbxParser` | 2.33ms |
| `deserialize_iter` (流式传输) | 2.74ms |
| `events_to_abx/AbxWriter` | 341µs |
| `xml_to_abx` | 2.56ms |
`AbxParser` 的速度大约比 `AbxStreamParser` 快 1.1–1.2 倍,这是环形缓冲区在零拷贝切片上进行簿记的不可避免的预期开销。serde 层相对于原始事件遍历的开销可以忽略不计——对于流式解析器来说,它实际上比将每个原始 `Event` 收集到 `Vec` 中*更快*,因为 `deserialize_iter` 每个元素只保留较小的反序列化结构,而不是保留每个事件拥有的所有字符串。`xml_to_abx` 的耗时主要受 `quick-xml` 的分词器主导,而不是本 crate 自身的编码——在相同规模下,单是 `events_to_abx` 就比完整的 XML 文本管道快约 7.5 倍。
通过这些基准测试,我们进行了三次优化:
- **解码**:跳过了 serde 层中针对每个元素的、对于扁平元素未使用的 `HashSet` 分配(`deserialize_all` 快了约 15–19%),将数值/布尔属性值直接写入 XML 输出缓冲区,而不是通过中间分配(`to_xml` 快了约 11%),并将内部化名称池切换为 `smol_str::SmolStr`(`parse_events` 快了约 43%,`to_xml` 与最初基于 `String` 的池相比快了约 40%——这是最大的一次提升,也是 `InternedStr` 存在的原因)。
- **编码**:`InternedPool` 的 `HashMap` 对于每个元素都要在一个仅包含少数几个 key 的 map 上进行 5 次哈希查找——真实的文档会重复一小组有界的名称词汇,因此对这极短的列表进行线性扫描比哈希查找更快(`events_to_abx` 快了约 48%)。但是,线性扫描在*唯一*名称的数量上是 O(n²) 的——一个包含 65,535 个不同名称的测试耗时从几毫秒飙升到了 49 秒。现已通过混合方式修复:在唯一名称少于 32 个时使用线性扫描,超过时使用 `HashMap`——这比最初的方案快了约 41%,而不是冒险去追求那更激进且不稳定的 ~48%。
- **流式解码**:`AbxStreamParser` 的环形缓冲区在*每次*检查缓冲区容量的调用时都会进行压缩(将未消费的字节滑动到前面),而不仅仅是在确实需要从 reader 重新填充时才进行——因此,即使根本不需要读取任何内容,几乎每个事件都要为整个未消费的缓冲区尾部进行一次 memmove。通过增加一个“是否确实即将要重新填充?”的检查来作为压缩的前提条件,使得 `AbxStreamParser` 上的 `parse_events`/`to_xml`/`deserialize_all` 耗时下降了 26–34%,并将其与 `AbxParser` 的差距从 ~1.5–1.8 倍缩小到了上文提到的 ~1.1–1.2 倍。
## Feature flags
- `serialize` —— `serde::Deserialize` 支持:`from_slice`/`from_reader`/`from_file`,`deserialize_next`/`deserialize_all`/`deserialize_iter`。
- `xml` —— `xml_to_abx`,将纯 XML 文本编码为 ABX 字节(引入了 `quick-xml`)。底层的 `AbxWriter`/`events_to_abx` 不需要额外的依赖,且始终可用。
## 开发
格式化/代码检查(lint)和提交信息通过 [pre-commit](https://pre-commit.com/)(配置位于 `.pre-commit-config.yaml`)进行检查:
`cargo fmt`、`cargo clippy --all-targets --all-features -- -D warnings`,以及对提交信息的 [Conventional Commits](https://www.conventionalcommits.org/) 检查。`.git/hooks/` 不受 git 追踪,因此在克隆仓库后:
```
pip install pre-commit
pre-commit install --hook-type pre-commit --hook-type commit-msg
```
`pre-commit run --all-files` 可以在不提交的情况下按需运行所有检查。
## 关于此项目
本 crate 还曾被用作智能体 AI 编程助手的真实测试用例——用于评估此类工具如何在多次会话中处理一个非同寻常的 Rust 项目:实现二进制格式解析器/编码器,将其与真实的上游(AOSP)源码进行交叉核对,以及进行日常的持续维护。
## 许可证
MIT。
标签:ABX, Android, DSL, Rust, 可视化界面, 序列化, 文件格式, 网络流量审计, 解析器, 通知系统