mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cf064e145d | |||
| 3b0c97a93d | |||
| 2d4f08f48b | |||
| a753304dff | |||
| 9480e2ffa4 | |||
| 140d0ddd8b | |||
| e0b114cf71 | |||
| 78449f686d | |||
| a2fb807803 | |||
| 6de5cb783a | |||
| 12d6f7f78c | |||
| 3c0a77df21 | |||
| d1b6bce227 | |||
| 10e0748d18 | |||
| 9cf63c7036 | |||
| 291b3c887b | |||
| 5d7eb13fbc | |||
| 05cd74719e | |||
| 7e0d5584d9 | |||
| b351d47f28 | |||
| 421b0a5207 | |||
| cad08a6a2f | |||
| 0a32dfaec3 | |||
| b03b2ade6a | |||
| c198646c7c | |||
| d815a78e9a | |||
| 99c5800f96 | |||
| c801fbb069 | |||
| 0b250086a3 | |||
| f6c6c8ab40 | |||
| 5c353a5f42 | |||
| ad932ed445 | |||
| d04d5451dc | |||
| 1d5db9f731 | |||
| 409b9298a5 | |||
| 516238d907 | |||
| 39d5fbf76d | |||
| 20f5bdacc7 | |||
| 5b8eefa867 | |||
| b2610fa0e9 | |||
| 8a5550a43c | |||
| e16f4c7fa7 | |||
| 983e6c40a5 | |||
| 8729854727 | |||
| 0d1beb7734 | |||
| 2cd0d69238 | |||
| c0e65264e5 | |||
| 37b15a2b6a | |||
| e4994fbe98 | |||
| cf1bacab30 | |||
| 4832121160 | |||
| c614e61744 | |||
| 9ebce1666c | |||
| fb34198ffc | |||
| 4a5eab4ba0 | |||
| 1e1818ad8f | |||
| 3f25ad7c76 | |||
| 37e28eb040 | |||
| ce87757fd1 | |||
| 10c425dede | |||
| 8a0d147993 | |||
| 062aa21485 | |||
| 7ffeb5e5cb | |||
| a9a1aa55e9 | |||
| 0ca2872111 | |||
| a403c26b54 | |||
| 4afa1d1ba6 |
@@ -42,7 +42,7 @@ jobs:
|
||||
android_api: '31'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
|
||||
with:
|
||||
key: ${{ matrix.artifact.name }}
|
||||
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
run: cp LICENSE README.md target/output/
|
||||
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
|
||||
path: |
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: cargo-deny
|
||||
on:
|
||||
push:
|
||||
@@ -11,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@v1
|
||||
uses: EmbarkStudios/cargo-deny-action@e2f4ede4a4e60ea15ff31bc0647485d80c66cfba # v2.0.4
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Github Release
|
||||
on:
|
||||
push:
|
||||
@@ -25,10 +24,10 @@ jobs:
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
|
||||
@@ -7,6 +7,40 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.11.0
|
||||
|
||||
* Fix crash when ignoring warning about `--magisk-preinit-device` not being specified ([PR #394])
|
||||
* When using `--ignore-magisk-warnings`, assume that unsupported Magisk versions newer than the latest supported version are capable of all features ([Issue #393], [PR #395])
|
||||
* Update bzip2-rs and switch to the Rust backend ([PR #397], [PR #402])
|
||||
* Minor code cleanup for custom integer range type ([PR #398])
|
||||
* Improve errors to make them less ambiguous about what went wrong ([PR #401])
|
||||
* Fix bug where a vendor v4 boot image that was truncated in the bootconfig padding section would be accepted as valid ([PR #401])
|
||||
* Avoid performing many small I/O operations when reading and writing cpio archives ([PR #403])
|
||||
* Update dependencies ([PR #404])
|
||||
|
||||
### Version 3.10.0
|
||||
|
||||
* Switch to using zerocopy library for all binary file format parsers ([PR #384])
|
||||
* Update to latest AOSP protobuf schema for the `payload.bin` metadata file format ([PR #385])
|
||||
* Update dependencies and pin Github Actions actions to specific commits ([PR #386], [PR #392])
|
||||
* Improve error messages from file format parsers ([PR #390])
|
||||
* Add support for Magisk 28100 ([PR #391])
|
||||
|
||||
### Version 3.9.0
|
||||
|
||||
* Update all dependencies ([PR #368], [PR #377])
|
||||
* Add advanced option to skip replacing the OTA certificate in the recovery image ([Issue #366], [PR #367], [PR #371])
|
||||
* Improve error message when an incompatible RSA key is used for AVB signing ([Issue #366], [PR #369])
|
||||
* Fix clippy warnings ([PR #370])
|
||||
* Allow `avbroot ota verify` to verify OTAs that lack `META-INF/com/android/metadata.pb` ([Issue #366], [PR #373])
|
||||
* Allow `avbroot ota verify` to verify OTAs where the payload signature does not set `unpadded_signature_size` ([Issue #366], [PR #374])
|
||||
* Allow `avbroot sparse` to parse sparse images with unknown fields (matches AOSP implementation) ([PR #376])
|
||||
|
||||
### Version 3.8.0
|
||||
|
||||
* Add `avbroot avb digest` subcommand for computing the special vbmeta digest ([PR #363])
|
||||
* Update all dependencies ([PR #364])
|
||||
|
||||
### Version 3.7.1
|
||||
|
||||
* Add support for Magisk 28000 ([PR #362])
|
||||
@@ -260,6 +294,8 @@ Behind-the-scenes changes:
|
||||
[Issue #328]: https://github.com/chenxiaolong/avbroot/issues/328
|
||||
[Issue #332]: https://github.com/chenxiaolong/avbroot/issues/332
|
||||
[Issue #356]: https://github.com/chenxiaolong/avbroot/issues/356
|
||||
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
|
||||
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
|
||||
[PR #130]: https://github.com/chenxiaolong/avbroot/pull/130
|
||||
[PR #132]: https://github.com/chenxiaolong/avbroot/pull/132
|
||||
[PR #133]: https://github.com/chenxiaolong/avbroot/pull/133
|
||||
@@ -369,3 +405,28 @@ Behind-the-scenes changes:
|
||||
[PR #355]: https://github.com/chenxiaolong/avbroot/pull/355
|
||||
[PR #357]: https://github.com/chenxiaolong/avbroot/pull/357
|
||||
[PR #362]: https://github.com/chenxiaolong/avbroot/pull/362
|
||||
[PR #363]: https://github.com/chenxiaolong/avbroot/pull/363
|
||||
[PR #364]: https://github.com/chenxiaolong/avbroot/pull/364
|
||||
[PR #367]: https://github.com/chenxiaolong/avbroot/pull/367
|
||||
[PR #368]: https://github.com/chenxiaolong/avbroot/pull/368
|
||||
[PR #369]: https://github.com/chenxiaolong/avbroot/pull/369
|
||||
[PR #370]: https://github.com/chenxiaolong/avbroot/pull/370
|
||||
[PR #371]: https://github.com/chenxiaolong/avbroot/pull/371
|
||||
[PR #373]: https://github.com/chenxiaolong/avbroot/pull/373
|
||||
[PR #374]: https://github.com/chenxiaolong/avbroot/pull/374
|
||||
[PR #376]: https://github.com/chenxiaolong/avbroot/pull/376
|
||||
[PR #377]: https://github.com/chenxiaolong/avbroot/pull/377
|
||||
[PR #384]: https://github.com/chenxiaolong/avbroot/pull/384
|
||||
[PR #385]: https://github.com/chenxiaolong/avbroot/pull/385
|
||||
[PR #386]: https://github.com/chenxiaolong/avbroot/pull/386
|
||||
[PR #390]: https://github.com/chenxiaolong/avbroot/pull/390
|
||||
[PR #391]: https://github.com/chenxiaolong/avbroot/pull/391
|
||||
[PR #392]: https://github.com/chenxiaolong/avbroot/pull/392
|
||||
[PR #394]: https://github.com/chenxiaolong/avbroot/pull/394
|
||||
[PR #395]: https://github.com/chenxiaolong/avbroot/pull/395
|
||||
[PR #397]: https://github.com/chenxiaolong/avbroot/pull/397
|
||||
[PR #398]: https://github.com/chenxiaolong/avbroot/pull/398
|
||||
[PR #401]: https://github.com/chenxiaolong/avbroot/pull/401
|
||||
[PR #402]: https://github.com/chenxiaolong/avbroot/pull/402
|
||||
[PR #403]: https://github.com/chenxiaolong/avbroot/pull/403
|
||||
[PR #404]: https://github.com/chenxiaolong/avbroot/pull/404
|
||||
|
||||
Generated
+254
-273
File diff suppressed because it is too large
Load Diff
+9
-1
@@ -4,7 +4,15 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.7.1"
|
||||
version = "3.11.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
cast_lossless = "deny"
|
||||
missing_fields_in_debug = "warn"
|
||||
redundant_clone = "deny"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
|
||||
@@ -66,6 +66,16 @@ If `-p` is omitted, the signatures and hashes are checked only for validity, not
|
||||
|
||||
By default, this command will not write to any file and fails if an image is corrupt or invalid. To attempt to repair corrupted dm-verity images, pass in `--repair`.
|
||||
|
||||
### Computing vbmeta digest
|
||||
|
||||
```bash
|
||||
avbroot avb digest -i <root vbmeta image>
|
||||
```
|
||||
|
||||
This subcommand computes the vbmeta digest, which is defined as the SHA256 digest of the root vbmeta partition's header, followed by the chained partitions' headers (if any) in the order that they are listed. Chained partitions more than one level deep are ignored.
|
||||
|
||||
This digest is equal to the value of the `ro.boot.vbmeta.digest` property or the `RootOfTrust.verifiedBootHash` hardware attestation field.
|
||||
|
||||
## `avbroot boot`
|
||||
|
||||
### Unpacking a boot image
|
||||
|
||||
@@ -385,6 +385,12 @@ Note that avbroot will validate that the prepatched image is compatible with the
|
||||
|
||||
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
|
||||
|
||||
### Skipping recovery OTA certificate patches
|
||||
|
||||
avbroot can skip modifying `otacerts.zip` in the recovery image with the `--skip-recovery-ota-cert` option. **Do not do this unless you have a good reason to do so.** (For example, if you've already manually inserted the OTA certificate into a boot image specified with `--prepatched` or `--replace`.) When this option is used with `--rootless` (and `--dsu` is not specified), then no modifications are performed on any boot image besides ensuring they are properly signed.
|
||||
|
||||
When manually adding the OTA certificate to a boot image, [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done.
|
||||
|
||||
### Replacing partitions
|
||||
|
||||
avbroot supports replacing entire partitions in the OTA, even partitions that are not boot images (eg. `vendor_dlkm`). A partition can be replaced by passing in `--replace <partition name> /path/to/partition.img`.
|
||||
@@ -484,6 +490,57 @@ By default, this behavior is compatible with the `--signing_helper` option in AO
|
||||
|
||||
Note that avbroot will verify the signature returned by helper program against the public key. This ensures that the patching process will fail appropriately if the wrong private key was used.
|
||||
|
||||
### 16K page size developer option
|
||||
|
||||
On recent devices running Android 16 and newer, there may be an option in Android's developer options to switch to a 16K page size kernel. This will not work when running an avbroot-patched OS. The switch internally works by flashing incremental OTAs:
|
||||
|
||||
* `/vendor/boot_otas/boot_ota_16k.zip` to switch to the 16K page size kernel (requires the `boot` partition to be currently flashed with the 4K kernel)
|
||||
* `/vendor/boot_otas/boot_ota_4k.zip` to switch to the 4K page size kernel (requires the `boot` partition to be currently flashed with the 16K kernel)
|
||||
|
||||
These `boot_otas` are unflashable when running an avbroot-patched OS because the `payload.bin` inside of them are signed by the OEM's key. These are also not proper OTA files. They don't contain any OTA metadata and the zip file itself is not signed. It's nothing more than a plain old zip file that stores a signed `payload.bin`.
|
||||
|
||||
There are no plans to add support for patching these `boot_otas`. It requires support for modifying filesystems and handling incremental OTAs, both of which are very non-trivial.
|
||||
|
||||
Folks who are determined to make this work anyway can try these manual steps to sign these `boot_otas` with your own key. Since the incremental OTAs are not being regenerated, the `boot` partition must be left unmodified when running `avbroot ota patch`.
|
||||
|
||||
1. Unpack `vendor.img` with avbroot and [afsr](https://github.com/chenxiaolong/afsr).
|
||||
|
||||
```bash
|
||||
avbroot avb unpack -i vendor.img
|
||||
afsr unpack -i raw.img
|
||||
```
|
||||
|
||||
2. Extract `payload.bin` from `boot_otas/boot_ota_16k.zip`.
|
||||
|
||||
3. Re-sign `payload.bin` with your OTA key.
|
||||
|
||||
```bash
|
||||
avbroot payload repack \
|
||||
-i payload.bin.orig \
|
||||
-o payload.bin \
|
||||
-k ota.key \
|
||||
--output-properties payload_properties.txt
|
||||
```
|
||||
|
||||
4. Create a new zip of `payload.bin` and `payload_properties.txt`. The files must be stored uncompressed (eg. with `zip -0`).
|
||||
|
||||
5. Repeat the procedure for `boot_otas/boot_ota_4k.zip`.
|
||||
|
||||
6. Repack `vendor.img` and sign it with your AVB key.
|
||||
|
||||
```bash
|
||||
afsr pack -o raw.img
|
||||
avbroot avb pack -o vendor.img -k avb.key --recompute-size
|
||||
```
|
||||
|
||||
7. Patch the (normal) OTA with:
|
||||
|
||||
```bash
|
||||
avbroot ota patch \
|
||||
--replace vendor <modified vendor> \
|
||||
<normal arguments...>
|
||||
```
|
||||
|
||||
## Building from source
|
||||
|
||||
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
|
||||
|
||||
+7
-1
@@ -377,10 +377,16 @@ avbroot может подменить используемый загрузоч
|
||||
|
||||
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
|
||||
|
||||
### Пропуск патчей для root-доступа
|
||||
### Пропуск патча для root-доступа
|
||||
|
||||
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
|
||||
|
||||
### Пропуск патчинга сертификата OTA в разделе Recovery
|
||||
|
||||
avbroot может пропустить изменение файла `otacerts.zip` в разделе Recovery с помощью опции `--skip-recovery-ota-cert`. **Не используйте эту функцию, если на то нет веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный образ (`boot.img`) и передаете его программе через опции `--prepatched` или `--replace`.) Если эта опция применяется совместно с `--rootless` (и без указания параметра `--dsu`), то в загрузочный образ не будут внесены никакие изменения, кроме обеспечения его корректной подписи.
|
||||
|
||||
Если вы вручную добавили сертификат OTA в загрузочный образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
|
||||
|
||||
### Подмена разделов
|
||||
|
||||
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
|
||||
|
||||
+12
-13
@@ -13,7 +13,6 @@ anyhow = "1.0.75"
|
||||
base64 = "0.22.1"
|
||||
bitflags = { version = "2.4.1", features = ["serde"] }
|
||||
bstr = "1.6.2"
|
||||
byteorder = "1.4.3"
|
||||
cap-std = "3.0.0"
|
||||
cap-tempfile = "3.0.0"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
@@ -22,7 +21,7 @@ cms = { version = "0.2.2", features = ["std"] }
|
||||
const-oid = "0.9.5"
|
||||
crc32fast = "1.4.2"
|
||||
ctrlc = "3.4.0"
|
||||
dlv-list = "0.5.2"
|
||||
dlv-list = "0.6.0"
|
||||
flate2 = "1.0.27"
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
@@ -49,21 +48,21 @@ serde = { version = "1.0.188", features = ["derive"] }
|
||||
sha1 = "0.10.5"
|
||||
sha2 = "0.10.7"
|
||||
tempfile = "3.8.0"
|
||||
thiserror = "1.0.47"
|
||||
thiserror = "2.0.3"
|
||||
toml_edit = { version = "0.22.9", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
zerocopy = "0.7.35"
|
||||
zerocopy-derive = "0.7.35"
|
||||
zerocopy = { version = "0.8.10", features = ["std"] }
|
||||
zerocopy-derive = "0.8.5"
|
||||
|
||||
# There are multiple upstream bugs that cause infinite loops in the Drop
|
||||
# implementation of write::BzDecoder. Unfortunately, the project is no longer
|
||||
# maintained, so we have to maintain our own fork with the necessary fixes.
|
||||
# Waiting for next stable release.
|
||||
[dependencies.bzip2]
|
||||
git = "https://github.com/chenxiaolong/bzip2-rs"
|
||||
rev = "6e0f9836ec87b19261461b6cc1772e14aff8e851"
|
||||
git = "https://github.com/trifectatechfoundation/bzip2-rs"
|
||||
rev = "15258feb0cdc1114d6fc6b936254c4f4e1d5730c"
|
||||
default-features = false
|
||||
features = ["libbz2-rs-sys"]
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
@@ -85,7 +84,7 @@ protox = "0.7.0"
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
[features]
|
||||
static = ["bzip2/static", "liblzma/static"]
|
||||
static = ["liblzma/static"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -316,6 +316,10 @@ message PartitionUpdate {
|
||||
// as a hint. If set to 0, libsnapshot should use alternative
|
||||
// methods for estimating size.
|
||||
optional uint64 estimate_cow_size = 19;
|
||||
|
||||
// Information about the cow used by Cow Writer to specify
|
||||
// number of cow operations to be written
|
||||
optional uint64 estimate_op_count_max = 20;
|
||||
}
|
||||
|
||||
message DynamicPartitionGroup {
|
||||
@@ -368,6 +372,10 @@ message DynamicPartitionMetadata {
|
||||
|
||||
// A collection of knobs to tune Virtual AB Compression
|
||||
optional VABCFeatureSet vabc_feature_set = 6;
|
||||
|
||||
// Max bytes to be compressed at once during ota. Options: 4k, 8k, 16k, 32k,
|
||||
// 64k, 128k
|
||||
optional uint64 compression_factor = 7;
|
||||
}
|
||||
|
||||
// Definition has been duplicated from
|
||||
|
||||
+130
-19
@@ -5,9 +5,8 @@ use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Seek, SeekFrom, Write},
|
||||
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@@ -28,7 +27,7 @@ use crate::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
},
|
||||
stream::{self, PSeekFile, Reopen},
|
||||
stream::{self, check_cancel, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -340,7 +339,13 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
}
|
||||
|
||||
let originally_signed = !info.header.signature.is_empty();
|
||||
let mut sign_action = if originally_signed && &info.header != orig_header {
|
||||
let sign_action = if key_group.force {
|
||||
if key_group.key.is_some() {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
SignAction::Clear
|
||||
}
|
||||
} else if originally_signed && &info.header != orig_header {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
// If the original image was signed, we can preserve the existing
|
||||
@@ -349,14 +354,6 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
SignAction::None
|
||||
};
|
||||
|
||||
if key_group.force {
|
||||
sign_action = if key_group.key.is_some() {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
SignAction::Clear
|
||||
};
|
||||
}
|
||||
|
||||
match sign_action {
|
||||
SignAction::None => {
|
||||
if originally_signed {
|
||||
@@ -398,7 +395,9 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
|
||||
RsaSigningKey::Internal(private_key)
|
||||
};
|
||||
|
||||
info.header.set_algo_for_key(&signing_key)?;
|
||||
info.header
|
||||
.set_algo_for_key(&signing_key)
|
||||
.context("Failed to set signature algorithm")?;
|
||||
info.header
|
||||
.sign(&signing_key)
|
||||
.context("Failed to sign new AVB header")?;
|
||||
@@ -519,24 +518,21 @@ fn verify_and_repair(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let _span = debug_span!("image", name = name.unwrap_or_default()).entered();
|
||||
let suffix = match name {
|
||||
Some(n) => format!(" for: {n}"),
|
||||
None => String::new(),
|
||||
};
|
||||
let suffix = name.map_or_else(String::new, |n| format!(" for: {n}"));
|
||||
|
||||
match descriptor {
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
info!("Verifying hash tree descriptor{suffix}");
|
||||
|
||||
match d.verify(&file, cancel_signal) {
|
||||
Err(e @ avb::Error::HashTree(_)) if repair => {
|
||||
Err(e @ avb::Error::HashTreeVerify(_)) if repair => {
|
||||
warn!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warn!("Attempting to repair using FEC data{suffix}");
|
||||
|
||||
d.repair(&file, &file, cancel_signal)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
|
||||
d.verify(&file, cancel_signal).map(|_| {
|
||||
d.verify(&file, cancel_signal).inspect(|()| {
|
||||
info!("Successfully repaired data{suffix}");
|
||||
})
|
||||
}
|
||||
@@ -600,6 +596,89 @@ pub fn verify_descriptors(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn compute_digest_recursive(
|
||||
directory: &Dir,
|
||||
name: &str,
|
||||
context: &mut ring::digest::Context,
|
||||
max_depth: u8,
|
||||
seen: &mut HashSet<String>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
if max_depth == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
seen.insert(name.to_owned());
|
||||
|
||||
ensure_name_is_safe(name)?;
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let mut raw_reader = directory
|
||||
.open(&path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let (header, footer, _) = avb::load_image(&mut raw_reader)
|
||||
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
|
||||
|
||||
// We don't have a good way to get the length of the header, so we serialize
|
||||
// what we just parsed and compare it to the raw file so ensure that the
|
||||
// round-tripped data is identical.
|
||||
let raw_header = {
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
header
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to serialize header: {path:?}"))?;
|
||||
writer.into_inner()
|
||||
};
|
||||
|
||||
let header_offset = footer.map(|f| f.vbmeta_offset).unwrap_or_default();
|
||||
raw_reader
|
||||
.seek(SeekFrom::Start(header_offset))
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
|
||||
let raw_header_orig = raw_reader
|
||||
.read_vec_exact(raw_header.len())
|
||||
.with_context(|| format!("Failed to reread AVB header: {path:?}"))?;
|
||||
|
||||
if raw_header != raw_header_orig {
|
||||
bail!("Serialized header does not match original header: {path:?}");
|
||||
}
|
||||
|
||||
context.update(&raw_header);
|
||||
|
||||
for descriptor in &header.descriptors {
|
||||
if let avb::Descriptor::ChainPartition(d) = descriptor {
|
||||
compute_digest_recursive(
|
||||
directory,
|
||||
&d.partition_name,
|
||||
context,
|
||||
max_depth - 1,
|
||||
seen,
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the vbmeta digest. This is defined as the digest of the header in
|
||||
/// the root vbmeta image, followed by the headers in the immediate chained
|
||||
/// partitions. This digest is not defined to be recursive, so headers of
|
||||
/// chained partitions more than one level deep are ignored.
|
||||
pub fn compute_digest(directory: &Dir, name: &str, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
|
||||
let mut seen = HashSet::<String>::new();
|
||||
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
|
||||
compute_digest_recursive(directory, name, &mut context, 2, &mut seen, cancel_signal)?;
|
||||
|
||||
let digest = context.finish();
|
||||
|
||||
Ok(digest.as_ref().try_into().unwrap())
|
||||
}
|
||||
|
||||
fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (info, mut reader) = read_avb_image(&cli.input)?;
|
||||
display_info(&cli.display, &info);
|
||||
@@ -736,6 +815,25 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let authority = ambient_authority();
|
||||
let parent_path = util::parent_path(&cli.input);
|
||||
let directory = Dir::open_ambient_dir(parent_path, authority)
|
||||
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
|
||||
let name = cli
|
||||
.input
|
||||
.file_stem()
|
||||
.with_context(|| format!("Path is not a file: {:?}", cli.input))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
|
||||
|
||||
let digest = compute_digest(&directory, name, cancel_signal)?;
|
||||
|
||||
println!("{}", hex::encode(digest));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
AvbCommand::Unpack(c) => unpack_subcommand(c, cancel_signal),
|
||||
@@ -743,6 +841,7 @@ pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
AvbCommand::Repack(c) => repack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Info(c) => info_subcommand(c),
|
||||
AvbCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
AvbCommand::Digest(c) => digest_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,6 +1046,17 @@ struct VerifyCli {
|
||||
repair: bool,
|
||||
}
|
||||
|
||||
/// Compute the vbmeta digest.
|
||||
///
|
||||
/// This value is equal to what is reported by the ro.boot.vbmeta.digest
|
||||
/// property on a real device.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DigestCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum AvbCommand {
|
||||
Unpack(UnpackCli),
|
||||
@@ -955,6 +1065,7 @@ enum AvbCommand {
|
||||
#[command(alias = "dump")]
|
||||
Info(InfoCli),
|
||||
Verify(VerifyCli),
|
||||
Digest(DigestCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect AVB-protected images.
|
||||
|
||||
+10
-13
@@ -129,22 +129,19 @@ fn split_extents(extents: &[Extent]) -> Vec<CopyExtent> {
|
||||
/// Use the CLI-specified slot or automatically select one if all slots are
|
||||
/// identical.
|
||||
fn get_slot_number(metadata: &Metadata, cli_slot: Option<u32>) -> Result<usize> {
|
||||
match cli_slot {
|
||||
Some(n) => {
|
||||
let n = n as usize;
|
||||
if n >= metadata.slots.len() {
|
||||
bail!("Slot out of range: {n}");
|
||||
}
|
||||
|
||||
Ok(n)
|
||||
if let Some(n) = cli_slot {
|
||||
let n = n as usize;
|
||||
if n >= metadata.slots.len() {
|
||||
bail!("Slot out of range: {n}");
|
||||
}
|
||||
None => {
|
||||
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
|
||||
bail!("A slot must be specified because they are not all identical");
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
Ok(n)
|
||||
} else {
|
||||
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
|
||||
bail!("A slot must be specified because they are not all identical");
|
||||
}
|
||||
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+43
-17
@@ -8,7 +8,6 @@ use std::{
|
||||
fmt::Display,
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
@@ -193,7 +192,7 @@ fn open_input_files(
|
||||
fn patch_boot_images<'a, 'b: 'a>(
|
||||
required_images: &'b RequiredImages,
|
||||
input_files: &mut HashMap<String, InputFile>,
|
||||
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -201,7 +200,7 @@ fn patch_boot_images<'a, 'b: 'a>(
|
||||
let boot_partitions = required_images.iter_boot().collect::<Vec<_>>();
|
||||
|
||||
info!(
|
||||
"Patching boot images: {}",
|
||||
"Candidate boot images: {}",
|
||||
joined(sorted(boot_partitions.iter())),
|
||||
);
|
||||
|
||||
@@ -219,7 +218,7 @@ fn patch_boot_images<'a, 'b: 'a>(
|
||||
WriteSeekReopen::reopen_boxed(&input_file.file)
|
||||
},
|
||||
key_avb,
|
||||
&boot_patchers,
|
||||
boot_patchers,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| {
|
||||
@@ -348,7 +347,7 @@ fn ensure_partitions_protected(
|
||||
/// determine the order to patch the vbmeta images so that it can be done in a
|
||||
/// single pass.
|
||||
fn get_vbmeta_patch_order(
|
||||
images: &mut HashMap<String, InputFile>,
|
||||
images: &HashMap<String, InputFile>,
|
||||
vbmeta_headers: &HashMap<String, Header>,
|
||||
) -> Result<Vec<(String, HashSet<String>)>> {
|
||||
let mut dep_graph = HashMap::<&str, HashSet<String>>::new();
|
||||
@@ -593,7 +592,9 @@ fn update_vbmeta_headers(
|
||||
// have no dependencies and are only being processed to ensure that the
|
||||
// flags are set to a sane value.
|
||||
if parent_header != &orig_parent_header {
|
||||
parent_header.set_algo_for_key(key)?;
|
||||
parent_header
|
||||
.set_algo_for_key(key)
|
||||
.with_context(|| format!("Failed to set signature algorithm: {name}"))?;
|
||||
parent_header
|
||||
.sign(key)
|
||||
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
|
||||
@@ -720,7 +721,7 @@ fn patch_ota_payload(
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
writer: impl Write,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
clear_vbmeta_flags: bool,
|
||||
key_avb: &RsaSigningKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
@@ -792,7 +793,7 @@ fn patch_ota_payload(
|
||||
|
||||
ensure_partitions_protected(&required_images, &vbmeta_headers)?;
|
||||
|
||||
let mut vbmeta_order = get_vbmeta_patch_order(&mut input_files, &vbmeta_headers)?;
|
||||
let mut vbmeta_order = get_vbmeta_patch_order(&input_files, &vbmeta_headers)?;
|
||||
|
||||
info!(
|
||||
"Patching vbmeta images: {}",
|
||||
@@ -912,7 +913,7 @@ fn patch_ota_zip(
|
||||
zip_reader: &mut ZipArchive<impl Read + Seek>,
|
||||
mut zip_writer: &mut ZipWriter<impl Write>,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
mut boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
clear_vbmeta_flags: bool,
|
||||
zip_mode: ZipMode,
|
||||
key_avb: &RsaSigningKey,
|
||||
@@ -1012,7 +1013,7 @@ fn patch_ota_zip(
|
||||
// Use the user's certificate
|
||||
info!("Replacing zip entry: {path}");
|
||||
|
||||
crypto::write_pem_cert(&mut writer, cert_ota)
|
||||
crypto::write_pem_cert(Path::new(path), &mut writer, cert_ota)
|
||||
.with_context(|| format!("Failed to write entry: {path}"))?;
|
||||
}
|
||||
ota::PATH_PAYLOAD => {
|
||||
@@ -1034,8 +1035,7 @@ fn patch_ota_zip(
|
||||
&payload_reader,
|
||||
&mut writer,
|
||||
external_images,
|
||||
// There's only one payload in the OTA.
|
||||
mem::take(&mut boot_patchers),
|
||||
boot_patchers,
|
||||
clear_vbmeta_flags,
|
||||
key_avb,
|
||||
key_ota,
|
||||
@@ -1304,7 +1304,11 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
assert!(cli.root.rootless);
|
||||
};
|
||||
|
||||
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
|
||||
if cli.skip_recovery_ota_cert {
|
||||
warn!("Not inserting OTA cert into recovery image; sideloading further updates may fail");
|
||||
} else {
|
||||
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
|
||||
}
|
||||
|
||||
if cli.dsu {
|
||||
boot_patchers.push(Box::new(DsuPubKeyPatcher::new(key_avb.to_public_key())));
|
||||
@@ -1341,7 +1345,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
&mut zip_reader,
|
||||
&mut zip_writer,
|
||||
&external_images,
|
||||
boot_patchers,
|
||||
&boot_patchers,
|
||||
cli.clear_vbmeta_flags,
|
||||
cli.zip_mode,
|
||||
&key_avb,
|
||||
@@ -1627,7 +1631,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
|
||||
);
|
||||
} else if let Some(p) = &cli.cert_ota {
|
||||
let verify_cert = crypto::read_pem_cert_file(p)
|
||||
.with_context(|| format!("Failed to load certificate: {:?}", p))?;
|
||||
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
|
||||
|
||||
if embedded_cert != verify_cert {
|
||||
bail!("OTA has a valid signature, but was not signed with: {p:?}");
|
||||
@@ -1684,9 +1688,11 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
|
||||
|
||||
verify_partition_hashes(&temp_dir, &header, &unique_images, cancel_signal)?;
|
||||
|
||||
info!("Checking ramdisk's otacerts.zip");
|
||||
if cli.skip_recovery_ota_cert {
|
||||
warn!("Not verifying recovery ramdisk's otacerts.zip");
|
||||
} else {
|
||||
info!("Checking recovery ramdisk's otacerts.zip");
|
||||
|
||||
{
|
||||
let required_images = RequiredImages::new(&header.manifest);
|
||||
let boot_images =
|
||||
boot::load_boot_images(&required_images.iter_boot().collect::<Vec<_>>(), |name| {
|
||||
@@ -1922,6 +1928,17 @@ pub struct PatchCli {
|
||||
)]
|
||||
pub ignore_prepatched_compat: u8,
|
||||
|
||||
/// Skip adding OTA certificate to recovery image.
|
||||
///
|
||||
/// DO NOT USE THIS unless you've manually added the certificate to the
|
||||
/// recovery image already. Otherwise, sideloading further updates will not
|
||||
/// be possible.
|
||||
///
|
||||
/// When this option is used with --rootless, the boot images in the OTA
|
||||
/// will not be modified.
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub skip_recovery_ota_cert: bool,
|
||||
|
||||
/// Add AVB public key to trusted keys for DSU.
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub dsu: bool,
|
||||
@@ -2008,6 +2025,15 @@ pub struct VerifyCli {
|
||||
/// valid, not that they are trusted.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
pub public_key_avb: Option<PathBuf>,
|
||||
|
||||
/// Skip verifying OTA certificate in recovery image.
|
||||
///
|
||||
/// This should not be used unless the OTA uses a special boot image format
|
||||
/// that avbroot cannot parse. This certificate check ensures that the OTA
|
||||
/// is configured properly to allow sideloading further OTAs signed by the
|
||||
/// same key.
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub skip_recovery_ota_cert: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::{
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use crc32fast::Hasher;
|
||||
use zerocopy::{little_endian, AsBytes};
|
||||
use zerocopy::{little_endian, IntoBytes};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
@@ -28,7 +28,7 @@ use crate::{
|
||||
|
||||
struct CompactView<'a, T>(&'a [T]);
|
||||
|
||||
impl<'a, T: fmt::Debug> fmt::Debug for CompactView<'a, T> {
|
||||
impl<T: fmt::Debug> fmt::Debug for CompactView<'_, T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut list = f.debug_list();
|
||||
|
||||
@@ -110,7 +110,7 @@ fn split_chunks(chunks: &[Chunk], block_size: u32) -> Vec<Chunk> {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
fn find_allocated_regions(
|
||||
path: &Path,
|
||||
reader: &mut File,
|
||||
reader: &File,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<Range<u64>>> {
|
||||
use rustix::{fs::SeekFrom, io::Errno};
|
||||
@@ -122,13 +122,13 @@ fn find_allocated_regions(
|
||||
loop {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
start = match rustix::fs::seek(&*reader, SeekFrom::Data(end as i64)) {
|
||||
start = match rustix::fs::seek(reader, SeekFrom::Data(end as i64)) {
|
||||
Ok(offset) => offset,
|
||||
Err(e) if e == Errno::NXIO => break,
|
||||
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
|
||||
};
|
||||
|
||||
end = rustix::fs::seek(&*reader, SeekFrom::Hole(start as i64))
|
||||
end = rustix::fs::seek(reader, SeekFrom::Hole(start as i64))
|
||||
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
|
||||
|
||||
result.push(start..end);
|
||||
@@ -375,7 +375,7 @@ fn pack_subcommand(
|
||||
} else {
|
||||
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||
{
|
||||
let regions = find_allocated_regions(&cli.input, &mut reader, cancel_signal)?;
|
||||
let regions = find_allocated_regions(&cli.input, &reader, cancel_signal)?;
|
||||
|
||||
(regions, false)
|
||||
}
|
||||
|
||||
+143
-86
@@ -5,7 +5,7 @@ use std::{
|
||||
env::{self, VarError},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, BufWriter, Read, Write},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
time::Duration,
|
||||
@@ -42,20 +42,22 @@ use x509_cert::{
|
||||
Certificate,
|
||||
};
|
||||
|
||||
use crate::util::DebugString;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Signature algorithm not supported: {0:?}")]
|
||||
UnsupportedAlgorithm(SignatureAlgorithm),
|
||||
#[error("RSA key size ({}) not supported", .0 * 8)]
|
||||
UnsupportedKey(usize),
|
||||
UnsupportedKeySize(usize),
|
||||
#[error("Invalid digest length ({0} bytes) for {1:?}")]
|
||||
InvalidDigestLength(usize, SignatureAlgorithm),
|
||||
#[error("Invalid signature length ({0} bytes) for {1:?}")]
|
||||
InvalidSignatureLength(usize, SignatureAlgorithm),
|
||||
#[error("Failed to run command: {0}")]
|
||||
CommandSpawnFailed(String, #[source] io::Error),
|
||||
#[error("Command failed with status: {1}: {0}")]
|
||||
CommandExecutionFailed(String, ExitStatus),
|
||||
#[error("Failed to run command: {0:?}")]
|
||||
CommandSpawn(DebugString, #[source] io::Error),
|
||||
#[error("Command failed with status: {1}: {0:?}")]
|
||||
CommandExecution(DebugString, ExitStatus),
|
||||
#[error("Signature from signing helper does not match public key: {0:?}")]
|
||||
SigningHelperBadSignature(PathBuf),
|
||||
#[error("Passphrase prompt requires an interactive terminal")]
|
||||
@@ -68,28 +70,40 @@ pub enum Error {
|
||||
InvalidEnvVar(OsString, #[source] VarError),
|
||||
#[error("PEM has start tag, but no end tag")]
|
||||
PemNoEndTag,
|
||||
#[error("Failed to load encrypted private key")]
|
||||
#[error("Failed to load encrypted RSA private key")]
|
||||
LoadKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to load unencrypted private key")]
|
||||
#[error("Failed to load unencrypted RSA private key")]
|
||||
LoadKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save encrypted private key")]
|
||||
#[error("Failed to save encrypted RSA private key")]
|
||||
SaveKeyEncrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to save unencrypted private key")]
|
||||
#[error("Failed to save unencrypted RSA private key")]
|
||||
SaveKeyUnencrypted(#[source] pkcs8::Error),
|
||||
#[error("Failed to load RSA public key")]
|
||||
LoadPubKey(#[source] pkcs8::spki::Error),
|
||||
#[error("Failed to save RSA public key")]
|
||||
SavePubKey(#[source] pkcs8::spki::Error),
|
||||
#[error("Failed to load X509 certificate")]
|
||||
LoadCert(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to save X509 certificate")]
|
||||
SaveCert(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to generate RSA key")]
|
||||
RsaGenerate(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to RSA sign digest")]
|
||||
RsaSign(#[source] rsa::Error),
|
||||
RsaSign(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to RSA verify signature")]
|
||||
RsaVerify(#[source] rsa::Error),
|
||||
#[error("X509 error")]
|
||||
X509(#[from] x509_cert::builder::Error),
|
||||
#[error("SPKI error")]
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("RSA error")]
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
RsaVerify(#[source] Box<rsa::Error>),
|
||||
#[error("Failed to generate X509 certificate")]
|
||||
CertGenerate(#[source] x509_cert::builder::Error),
|
||||
#[error("Invalid parameters for X509 certificate generation")]
|
||||
CertParams(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to CMS sign digest")]
|
||||
CmsSign(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to parse CMS signature")]
|
||||
CmsParse(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to read file: {0:?}")]
|
||||
ReadFile(PathBuf, #[source] io::Error),
|
||||
#[error("Failed to write file: {0:?}")]
|
||||
WriteFile(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -175,8 +189,9 @@ impl PassphraseSource {
|
||||
first
|
||||
}
|
||||
Self::EnvVar(v) => env::var(v).map_err(|e| Error::InvalidEnvVar(v.clone(), e))?,
|
||||
Self::File(p) => fs::read_to_string(p)?
|
||||
.trim_end_matches(&['\r', '\n'])
|
||||
Self::File(p) => fs::read_to_string(p)
|
||||
.map_err(|e| Error::ReadFile(p.clone(), e))?
|
||||
.trim_end_matches(['\r', '\n'])
|
||||
.to_owned(),
|
||||
};
|
||||
|
||||
@@ -187,7 +202,7 @@ impl PassphraseSource {
|
||||
fn check_key_size(size: usize) -> Result<()> {
|
||||
// RustCrypto does not support 8192-bit keys.
|
||||
if size > 4096 / 8 {
|
||||
return Err(Error::UnsupportedKey(size));
|
||||
return Err(Error::UnsupportedKeySize(size));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -235,8 +250,8 @@ impl RsaSigningKey {
|
||||
/// Get the public key portion of the signing key.
|
||||
pub fn to_public_key(&self) -> RsaPublicKey {
|
||||
match self {
|
||||
RsaSigningKey::Internal(key) => key.to_public_key(),
|
||||
RsaSigningKey::External { public_key, .. } => public_key.clone(),
|
||||
Self::Internal(key) => key.to_public_key(),
|
||||
Self::External { public_key, .. } => public_key.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +271,9 @@ impl RsaSigningKey {
|
||||
};
|
||||
|
||||
match self {
|
||||
Self::Internal(key) => key.sign(scheme, digest).map_err(Error::RsaSign),
|
||||
Self::Internal(key) => key
|
||||
.sign(scheme, digest)
|
||||
.map_err(|e| Error::RsaSign(Box::new(e))),
|
||||
Self::External {
|
||||
program,
|
||||
public_key,
|
||||
@@ -292,7 +309,7 @@ impl RsaSigningKey {
|
||||
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|e| Error::CommandSpawnFailed(format!("{command:?}"), e))?;
|
||||
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
|
||||
|
||||
// We don't bother with spawning a thread. The pipe capacity on
|
||||
// all major OSs is significantly larger than the digest, so we
|
||||
@@ -304,14 +321,22 @@ impl RsaSigningKey {
|
||||
// * macOS: 4 KiB, 16 KiB (usually), or 64 KiB
|
||||
// * Windows: 4 KiB
|
||||
|
||||
let padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())?;
|
||||
child.stdin.as_mut().unwrap().write_all(&padded_digest)?;
|
||||
let padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())
|
||||
.map_err(|e| Error::RsaSign(Box::new(e)))?;
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(&padded_digest)
|
||||
.map_err(|e| Error::WriteFile("<signing helper stdin>".into(), e))?;
|
||||
|
||||
let child = child.wait_with_output()?;
|
||||
let child = child
|
||||
.wait_with_output()
|
||||
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
|
||||
|
||||
if !child.status.success() {
|
||||
return Err(Error::CommandExecutionFailed(
|
||||
format!("{command:?}"),
|
||||
return Err(Error::CommandExecution(
|
||||
DebugString::new(&command),
|
||||
child.status,
|
||||
));
|
||||
} else if child.stdout.len() != self.size() {
|
||||
@@ -355,7 +380,7 @@ impl RsaPublicKeyExt for RsaPublicKey {
|
||||
};
|
||||
|
||||
self.verify(scheme, digest, signature)
|
||||
.map_err(Error::RsaVerify)
|
||||
.map_err(|e| Error::RsaVerify(Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +389,7 @@ pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
// avbroot supports 4096-bit keys only.
|
||||
let key = RsaPrivateKey::new(&mut rng, 4096)?;
|
||||
let key = RsaPrivateKey::new(&mut rng, 4096).map_err(|e| Error::RsaGenerate(Box::new(e)))?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
@@ -376,20 +401,27 @@ pub fn generate_cert(
|
||||
validity: Duration,
|
||||
subject: &str,
|
||||
) -> Result<Certificate> {
|
||||
let public_key_der = key.to_public_key().to_public_key_der()?;
|
||||
let public_key_der = key
|
||||
.to_public_key()
|
||||
.to_public_key_der()
|
||||
.map_err(Error::SavePubKey)?;
|
||||
let signing_key = SigningKey::<Sha256>::new(key.clone());
|
||||
|
||||
let builder = CertificateBuilder::new(
|
||||
Profile::Root,
|
||||
SerialNumber::from(serial),
|
||||
Validity::from_now(validity)?,
|
||||
subject.parse()?,
|
||||
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())?,
|
||||
Validity::from_now(validity).map_err(Error::CertParams)?,
|
||||
subject.parse().map_err(Error::CertParams)?,
|
||||
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())
|
||||
.map_err(Error::CertParams)?,
|
||||
&signing_key,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::CertGenerate)?;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
let cert = builder.build_with_rng(&mut rng)?;
|
||||
let cert = builder
|
||||
.build_with_rng(&mut rng)
|
||||
.map_err(Error::CertGenerate)?;
|
||||
|
||||
Ok(cert)
|
||||
}
|
||||
@@ -442,80 +474,92 @@ fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
|
||||
}
|
||||
|
||||
/// Read PEM-encoded certificate from a reader.
|
||||
pub fn read_pem_cert(mut reader: impl Read) -> Result<Certificate> {
|
||||
pub fn read_pem_cert(path: &Path, mut reader: impl Read) -> Result<Certificate> {
|
||||
let mut data = vec![];
|
||||
reader.read_to_end(&mut data)?;
|
||||
reader
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
let data = reformat_pem(&data)?;
|
||||
let certificate = Certificate::from_pem(data)?;
|
||||
let certificate = Certificate::from_pem(data).map_err(Error::LoadCert)?;
|
||||
|
||||
Ok(certificate)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded certificate to a writer.
|
||||
pub fn write_pem_cert(mut writer: impl Write, cert: &Certificate) -> Result<()> {
|
||||
let data = cert.to_pem(LineEnding::LF)?;
|
||||
pub fn write_pem_cert(path: &Path, mut writer: impl Write, cert: &Certificate) -> Result<()> {
|
||||
let data = cert.to_pem(LineEnding::LF).map_err(Error::SaveCert)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded certificate from a file.
|
||||
pub fn read_pem_cert_file(path: &Path) -> Result<Certificate> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_cert(reader)
|
||||
read_pem_cert(path, reader)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded certificate to a file.
|
||||
pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_cert(writer, cert)
|
||||
write_pem_cert(path, writer, cert)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a reader.
|
||||
pub fn read_pem_public_key(mut reader: impl Read) -> Result<RsaPublicKey> {
|
||||
pub fn read_pem_public_key(path: &Path, mut reader: impl Read) -> Result<RsaPublicKey> {
|
||||
let mut data = String::new();
|
||||
reader.read_to_string(&mut data)?;
|
||||
reader
|
||||
.read_to_string(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
let key = RsaPublicKey::from_public_key_pem(&data)?;
|
||||
let key = RsaPublicKey::from_public_key_pem(&data).map_err(Error::LoadPubKey)?;
|
||||
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a writer.
|
||||
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key.to_public_key_pem(LineEnding::LF)?;
|
||||
pub fn write_pem_public_key(path: &Path, mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key
|
||||
.to_public_key_pem(LineEnding::LF)
|
||||
.map_err(Error::SavePubKey)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 public key from a file.
|
||||
pub fn read_pem_public_key_file(path: &Path) -> Result<RsaPublicKey> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_public_key(reader)
|
||||
read_pem_public_key(path, reader)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a file.
|
||||
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_public_key(writer, key)
|
||||
write_pem_public_key(path, writer, key)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a reader.
|
||||
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
pub fn read_pem_key(
|
||||
path: &Path,
|
||||
mut reader: impl Read,
|
||||
source: &PassphraseSource,
|
||||
) -> Result<RsaPrivateKey> {
|
||||
let mut data = String::new();
|
||||
reader.read_to_string(&mut data)?;
|
||||
reader
|
||||
.read_to_string(&mut data)
|
||||
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
if data.contains("ENCRYPTED") {
|
||||
let passphrase = source.acquire(false)?;
|
||||
@@ -528,6 +572,7 @@ pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<
|
||||
|
||||
/// Write PEM-encoded PKCS8 private key to a writer.
|
||||
pub fn write_pem_key(
|
||||
path: &Path,
|
||||
mut writer: impl Write,
|
||||
key: &RsaPrivateKey,
|
||||
source: &PassphraseSource,
|
||||
@@ -570,20 +615,24 @@ pub fn write_pem_key(
|
||||
.encrypt_with_params(pbes2_params, passphrase)
|
||||
.map_err(Error::SaveKeyEncrypted)?;
|
||||
|
||||
secret_doc.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)?
|
||||
secret_doc
|
||||
.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)
|
||||
.map_err(pkcs8::Error::Asn1)
|
||||
.map_err(Error::SaveKeyEncrypted)?
|
||||
};
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
writer
|
||||
.write_all(data.as_bytes())
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a file.
|
||||
pub fn read_pem_key_file(path: &Path, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
|
||||
|
||||
read_pem_key(reader, source)
|
||||
read_pem_key(path, reader, source)
|
||||
}
|
||||
|
||||
/// Save PEM-encoded PKCS8 private key to a file.
|
||||
@@ -603,16 +652,18 @@ pub fn write_pem_key_file(
|
||||
options.mode(0o600);
|
||||
}
|
||||
|
||||
let file = options.open(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
let writer = options
|
||||
.open(path)
|
||||
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
|
||||
|
||||
write_pem_key(writer, key, source)
|
||||
write_pem_key(path, writer, key, source)
|
||||
}
|
||||
|
||||
/// Get the RSA public key from a certificate.
|
||||
pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
|
||||
let public_key =
|
||||
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())?;
|
||||
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())
|
||||
.map_err(Error::LoadPubKey)?;
|
||||
|
||||
Ok(public_key)
|
||||
}
|
||||
@@ -626,8 +677,11 @@ pub fn cert_matches_key(cert: &Certificate, key: &RsaSigningKey) -> Result<bool>
|
||||
|
||||
/// Parse a CMS [`SignedData`] structure from raw DER-encoded data.
|
||||
pub fn parse_cms(data: &[u8]) -> Result<SignedData> {
|
||||
let ci = ContentInfo::from_der(data)?;
|
||||
let sd = ci.content.decode_as::<SignedData>()?;
|
||||
let ci = ContentInfo::from_der(data).map_err(Error::CmsParse)?;
|
||||
let sd = ci
|
||||
.content
|
||||
.decode_as::<SignedData>()
|
||||
.map_err(Error::CmsParse)?;
|
||||
|
||||
Ok(sd)
|
||||
}
|
||||
@@ -669,14 +723,16 @@ pub fn cms_sign_external(
|
||||
|
||||
let signed_data = SignedData {
|
||||
version: CmsVersion::V1,
|
||||
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])?,
|
||||
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])
|
||||
.map_err(Error::CmsSign)?,
|
||||
encap_content_info: EncapsulatedContentInfo {
|
||||
econtent_type: const_oid::db::rfc5911::ID_DATA,
|
||||
econtent: None,
|
||||
},
|
||||
certificates: Some(CertificateSet::try_from(vec![
|
||||
CertificateChoices::Certificate(cert.clone()),
|
||||
])?),
|
||||
certificates: Some(
|
||||
CertificateSet::try_from(vec![CertificateChoices::Certificate(cert.clone())])
|
||||
.map_err(Error::CmsSign)?,
|
||||
),
|
||||
crls: None,
|
||||
signer_infos: SignerInfos::try_from(vec![SignerInfo {
|
||||
version: CmsVersion::V1,
|
||||
@@ -690,14 +746,15 @@ pub fn cms_sign_external(
|
||||
oid: const_oid::db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
|
||||
parameters: None,
|
||||
},
|
||||
signature: SignatureValue::new(signature)?,
|
||||
signature: SignatureValue::new(signature).map_err(Error::CmsSign)?,
|
||||
unsigned_attrs: None,
|
||||
}])?,
|
||||
}])
|
||||
.map_err(Error::CmsSign)?,
|
||||
};
|
||||
|
||||
let signed_data = ContentInfo {
|
||||
content_type: const_oid::db::rfc5911::ID_SIGNED_DATA,
|
||||
content: Any::encode_from(&signed_data)?,
|
||||
content: Any::encode_from(&signed_data).map_err(Error::CmsSign)?,
|
||||
};
|
||||
|
||||
Ok(signed_data)
|
||||
|
||||
@@ -66,7 +66,7 @@ where
|
||||
{
|
||||
struct EscapedStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for EscapedStrVisitor<T>
|
||||
impl<T> Visitor<'_> for EscapedStrVisitor<T>
|
||||
where
|
||||
T: FromEscaped,
|
||||
<T as FromEscaped>::Error: fmt::Display,
|
||||
|
||||
+787
-490
File diff suppressed because it is too large
Load Diff
+651
-428
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use liblzma::{
|
||||
read::XzDecoder,
|
||||
@@ -14,6 +13,8 @@ use lz4_flex::frame::FrameDecoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::stream::ReadFixedSizeExt;
|
||||
|
||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
||||
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
@@ -22,10 +23,12 @@ static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
pub enum Error {
|
||||
#[error("Unknown compression format")]
|
||||
UnknownFormat,
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("I/O error when autodetecting compression format")]
|
||||
AutoDetect(#[source] io::Error),
|
||||
#[error("Failed to initialize legacy LZ4 encoder")]
|
||||
Lz4Init(#[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -59,7 +62,7 @@ impl<W: Write> Lz4LegacyEncoder<W> {
|
||||
let compressed = lz4_flex::block::compress(&self.buf[..self.n_filled]);
|
||||
|
||||
let writer = self.writer.as_mut().unwrap();
|
||||
writer.write_u32::<LittleEndian>(compressed.len() as u32)?;
|
||||
writer.write_all(&(compressed.len() as u32).to_le_bytes())?;
|
||||
writer.write_all(&compressed)?;
|
||||
|
||||
self.n_filled = 0;
|
||||
@@ -120,10 +123,9 @@ pub enum CompressedReader<R: Read> {
|
||||
|
||||
impl<R: Read + Seek> CompressedReader<R> {
|
||||
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
let magic = reader.read_array_exact::<6>().map_err(Error::AutoDetect)?;
|
||||
|
||||
reader.rewind()?;
|
||||
reader.rewind().map_err(Error::AutoDetect)?;
|
||||
|
||||
if &magic[0..2] == GZIP_MAGIC {
|
||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||
@@ -182,10 +184,13 @@ impl<W: Write> CompressedWriter<W> {
|
||||
CompressedFormat::Gzip => {
|
||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||
}
|
||||
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
|
||||
CompressedFormat::Lz4Legacy => {
|
||||
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4Init)?;
|
||||
Ok(Self::Lz4Legacy(encoder))
|
||||
}
|
||||
CompressedFormat::Xz => {
|
||||
// Some kernels are compiled without support for the default CRC64.
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32).map_err(Error::XzInit)?;
|
||||
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
|
||||
}
|
||||
}
|
||||
|
||||
+164
-100
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -13,6 +13,8 @@ use bstr::ByteSlice;
|
||||
use num_traits::{ToPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
escape,
|
||||
@@ -48,55 +50,94 @@ const VEC_CAP_THRESHOLD: usize = 16384;
|
||||
pub enum Error {
|
||||
#[error("Unknown magic: {0:?}")]
|
||||
UnknownMagic([u8; 6]),
|
||||
#[error("Path is not NULL-terminated: {:?}", .0.as_bstr())]
|
||||
PathNotNullTerminated(Vec<u8>),
|
||||
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
|
||||
HardLinksNotSupported(Vec<u8>),
|
||||
#[error("Entry of type {0} should not have data: {:?}", .1.as_bstr())]
|
||||
#[error("Entry of type {0} should not have data: {path:?}", path = .1.as_bstr())]
|
||||
EntryHasData(CpioEntryType, Vec<u8>),
|
||||
#[error("No inodes available for device {0:x},{1:x}")]
|
||||
DeviceFull(u32, u32),
|
||||
#[error("{0:?} field exceeds integer bounds")]
|
||||
IntegerTooLarge(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("No inodes available for device {major:x},{minor:x}")]
|
||||
DeviceFull { major: u32, minor: u32 },
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("{0:?} contains invalid hex integer")]
|
||||
InvalidHexInt(&'static str, #[source] InvalidHexCharError),
|
||||
#[error("Failed to read cpio data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write cpio data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Read u32 formatted as an ASCII 8-char wide hex string.
|
||||
fn read_int(mut reader: impl Read) -> io::Result<u32> {
|
||||
let mut buf = [0u8; 8];
|
||||
reader.read_exact(&mut buf)?;
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{0:?}: Invalid hex char: {1:?}")]
|
||||
pub struct InvalidHexCharError(RawHexU32, char);
|
||||
|
||||
let mut value = 0;
|
||||
/// ASCII-encoded hex integer value used in cpio header fields.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHexU32([u8; 8]);
|
||||
|
||||
for b in buf {
|
||||
let c = b as char;
|
||||
let digit = c.to_digit(16).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("{:?}: Invalid hex char: {c}", buf.as_bstr()),
|
||||
)
|
||||
})?;
|
||||
|
||||
value <<= 4;
|
||||
value |= digit;
|
||||
impl fmt::Debug for RawHexU32 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0.as_bstr())
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Write u32 formatted as an ASCII 8-char wide hex string.
|
||||
fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
|
||||
let mut buf = [b'0'; 8];
|
||||
let mut index = 7;
|
||||
#[allow(clippy::fallible_impl_from)]
|
||||
impl From<u32> for RawHexU32 {
|
||||
fn from(mut value: u32) -> Self {
|
||||
let mut buf = [b'0'; 8];
|
||||
let mut index = 7;
|
||||
|
||||
while value != 0 {
|
||||
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
|
||||
value >>= 4;
|
||||
index -= 1;
|
||||
while value != 0 {
|
||||
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
|
||||
value >>= 4;
|
||||
index -= 1;
|
||||
}
|
||||
|
||||
Self(buf)
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_all(&buf)
|
||||
impl TryFrom<RawHexU32> for u32 {
|
||||
type Error = InvalidHexCharError;
|
||||
|
||||
fn try_from(raw_value: RawHexU32) -> std::result::Result<Self, Self::Error> {
|
||||
let mut value = 0;
|
||||
|
||||
for b in raw_value.0 {
|
||||
let c = b as char;
|
||||
let digit = c.to_digit(16).ok_or(InvalidHexCharError(raw_value, c))?;
|
||||
|
||||
value <<= 4;
|
||||
value |= digit;
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the cpio header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`MAGIC_NEW`] or [`MAGIC_NEW_CRC`].
|
||||
magic: [u8; 6],
|
||||
inode: RawHexU32,
|
||||
mode: RawHexU32,
|
||||
uid: RawHexU32,
|
||||
gid: RawHexU32,
|
||||
nlink: RawHexU32,
|
||||
mtime: RawHexU32,
|
||||
file_size: RawHexU32,
|
||||
dev_maj: RawHexU32,
|
||||
dev_min: RawHexU32,
|
||||
rdev_maj: RawHexU32,
|
||||
rdev_min: RawHexU32,
|
||||
path_size: RawHexU32,
|
||||
crc32: RawHexU32,
|
||||
}
|
||||
|
||||
/// Read a chunk of bytes from the reader. If `size` is less than
|
||||
@@ -211,17 +252,14 @@ impl CpioEntryData {
|
||||
pub fn size(&self) -> Result<u32> {
|
||||
let size = match self {
|
||||
Self::Size(s) => *s,
|
||||
Self::Data(d) => d
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("data_size"))?,
|
||||
Self::Data(d) => d.len().to_u32().ok_or(Error::IntOverflow("data_size"))?,
|
||||
};
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn is_size(&self) -> bool {
|
||||
matches!(self, CpioEntryData::Size(_))
|
||||
matches!(self, Self::Size(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,41 +427,45 @@ impl<R: Read> FromReader<R> for CpioEntry {
|
||||
fn from_reader(reader: R) -> Result<Self> {
|
||||
let mut reader = CountingReader::new(reader);
|
||||
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
if magic != *MAGIC_NEW && magic != *MAGIC_NEW_CRC {
|
||||
return Err(Error::UnknownMagic(magic));
|
||||
if header.magic != *MAGIC_NEW && header.magic != *MAGIC_NEW_CRC {
|
||||
return Err(Error::UnknownMagic(header.magic));
|
||||
}
|
||||
|
||||
let inode = read_int(&mut reader)?;
|
||||
let mode = read_int(&mut reader)?;
|
||||
let uid = read_int(&mut reader)?;
|
||||
let gid = read_int(&mut reader)?;
|
||||
let nlink = read_int(&mut reader)?;
|
||||
let mtime = read_int(&mut reader)?;
|
||||
let file_size = read_int(&mut reader)?;
|
||||
let dev_maj = read_int(&mut reader)?;
|
||||
let dev_min = read_int(&mut reader)?;
|
||||
let rdev_maj = read_int(&mut reader)?;
|
||||
let rdev_min = read_int(&mut reader)?;
|
||||
let path_size = read_int(&mut reader)?;
|
||||
let crc32 = read_int(&mut reader)?;
|
||||
macro_rules! get_field {
|
||||
($name:ident) => {
|
||||
let $name = u32::try_from(header.$name)
|
||||
.map_err(|e| Error::InvalidHexInt(stringify!($name), e))?;
|
||||
};
|
||||
}
|
||||
|
||||
get_field!(inode);
|
||||
get_field!(mode);
|
||||
get_field!(uid);
|
||||
get_field!(gid);
|
||||
get_field!(nlink);
|
||||
get_field!(mtime);
|
||||
get_field!(file_size);
|
||||
get_field!(dev_maj);
|
||||
get_field!(dev_min);
|
||||
get_field!(rdev_maj);
|
||||
get_field!(rdev_min);
|
||||
get_field!(path_size);
|
||||
get_field!(crc32);
|
||||
|
||||
let mut path = read_data(
|
||||
&mut reader,
|
||||
path_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("path", e))?;
|
||||
if path.last() != Some(&b'\0') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Filename is not NULL-terminated",
|
||||
)
|
||||
.into());
|
||||
return Err(Error::PathNotNullTerminated(path));
|
||||
}
|
||||
path.pop();
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
padding::read_discard(&mut reader, 4).map_err(|e| Error::DataRead("path_padding", e))?;
|
||||
|
||||
let file_type = CpioEntryType::from_mode(mode);
|
||||
let data = match file_type {
|
||||
@@ -435,8 +477,10 @@ impl<R: Read> FromReader<R> for CpioEntry {
|
||||
&mut reader,
|
||||
file_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("content", e))?;
|
||||
padding::read_discard(&mut reader, 4)
|
||||
.map_err(|e| Error::DataRead("content_padding", e))?;
|
||||
|
||||
CpioEntryData::Data(content)
|
||||
}
|
||||
@@ -475,7 +519,7 @@ impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
.len()
|
||||
.checked_add(1)
|
||||
.and_then(|s| s.to_u32())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
|
||||
.ok_or(Error::IntOverflow("path_size"))?;
|
||||
|
||||
let file_size = self.data.size()?;
|
||||
if file_size != 0
|
||||
@@ -485,35 +529,47 @@ impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
return Err(Error::EntryHasData(self.file_type, self.path.clone()));
|
||||
}
|
||||
|
||||
if self.crc32 == 0 {
|
||||
writer.write_all(MAGIC_NEW)?;
|
||||
} else {
|
||||
writer.write_all(MAGIC_NEW_CRC)?;
|
||||
}
|
||||
|
||||
let mode = self.file_type.to_mode() | u32::from(self.file_mode & 0o7777);
|
||||
|
||||
write_int(&mut writer, self.inode)?;
|
||||
write_int(&mut writer, mode)?;
|
||||
write_int(&mut writer, self.uid)?;
|
||||
write_int(&mut writer, self.gid)?;
|
||||
write_int(&mut writer, self.nlink)?;
|
||||
write_int(&mut writer, self.mtime)?;
|
||||
write_int(&mut writer, file_size)?;
|
||||
write_int(&mut writer, self.dev_maj)?;
|
||||
write_int(&mut writer, self.dev_min)?;
|
||||
write_int(&mut writer, self.rdev_maj)?;
|
||||
write_int(&mut writer, self.rdev_min)?;
|
||||
write_int(&mut writer, path_size)?;
|
||||
write_int(&mut writer, self.crc32)?;
|
||||
let raw_header = RawHeader {
|
||||
magic: if self.crc32 == 0 {
|
||||
*MAGIC_NEW
|
||||
} else {
|
||||
*MAGIC_NEW_CRC
|
||||
},
|
||||
inode: self.inode.into(),
|
||||
mode: mode.into(),
|
||||
uid: self.uid.into(),
|
||||
gid: self.gid.into(),
|
||||
nlink: self.nlink.into(),
|
||||
mtime: self.mtime.into(),
|
||||
file_size: file_size.into(),
|
||||
dev_maj: self.dev_maj.into(),
|
||||
dev_min: self.dev_min.into(),
|
||||
rdev_maj: self.rdev_maj.into(),
|
||||
rdev_min: self.rdev_min.into(),
|
||||
path_size: path_size.into(),
|
||||
crc32: self.crc32.into(),
|
||||
};
|
||||
|
||||
writer.write_all(&self.path)?;
|
||||
writer.write_zeros_exact(1)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
raw_header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
|
||||
writer
|
||||
.write_all(&self.path)
|
||||
.map_err(|e| Error::DataWrite("path", e))?;
|
||||
writer
|
||||
.write_zeros_exact(1)
|
||||
.map_err(|e| Error::DataWrite("path", e))?;
|
||||
padding::write_zeros(&mut writer, 4).map_err(|e| Error::DataWrite("path_padding", e))?;
|
||||
|
||||
if let CpioEntryData::Data(d) = &self.data {
|
||||
writer.write_all(d)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
writer
|
||||
.write_all(d)
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
padding::write_zeros(&mut writer, 4)
|
||||
.map_err(|e| Error::DataWrite("content_padding", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -557,7 +613,8 @@ impl<R: Read> CpioReader<R> {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.skip_data()?;
|
||||
self.skip_data()
|
||||
.map_err(|e| Error::DataRead("content", e))?;
|
||||
|
||||
let entry = CpioEntry::from_reader(&mut self.reader)?;
|
||||
|
||||
@@ -622,7 +679,8 @@ impl<W: Write> CpioWriter<W> {
|
||||
}
|
||||
|
||||
pub fn start_entry(&mut self, entry: &CpioEntry) -> Result<()> {
|
||||
self.finish_entry()?;
|
||||
self.finish_entry()
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
|
||||
entry.to_writer(&mut self.writer)?;
|
||||
|
||||
@@ -636,13 +694,15 @@ impl<W: Write> CpioWriter<W> {
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Result<W> {
|
||||
self.finish_entry()?;
|
||||
self.finish_entry()
|
||||
.map_err(|e| Error::DataWrite("content", e))?;
|
||||
|
||||
self.start_entry(&CpioEntry::new_trailer())?;
|
||||
|
||||
// Pad until the end of the block.
|
||||
if self.pad_to_block_size {
|
||||
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)?;
|
||||
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)
|
||||
.map_err(|e| Error::DataWrite("block_padding", e))?;
|
||||
}
|
||||
|
||||
Ok(self.writer.finish().0)
|
||||
@@ -680,14 +740,15 @@ pub fn load(
|
||||
let mut entries = vec![];
|
||||
|
||||
while let Some(mut entry) = cpio_reader.next_entry()? {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("entry", e))?;
|
||||
|
||||
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
|
||||
return Err(Error::HardLinksNotSupported(entry.path.clone()));
|
||||
return Err(Error::HardLinksNotSupported(entry.path));
|
||||
}
|
||||
|
||||
if let CpioEntryData::Size(s) = entry.data {
|
||||
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)?;
|
||||
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)
|
||||
.map_err(|e| Error::DataWrite("data", e))?;
|
||||
entry.data = CpioEntryData::Data(data);
|
||||
}
|
||||
|
||||
@@ -743,7 +804,10 @@ pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()
|
||||
|
||||
while set.contains(&unused) {
|
||||
if unused == *last {
|
||||
return Err(Error::DeviceFull(entry.dev_maj, entry.dev_min));
|
||||
return Err(Error::DeviceFull {
|
||||
major: entry.dev_maj,
|
||||
minor: entry.dev_min,
|
||||
});
|
||||
}
|
||||
|
||||
unused = next_non_zero(unused);
|
||||
@@ -767,7 +831,7 @@ pub fn save(
|
||||
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
|
||||
|
||||
for entry in entries {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataWrite("entry", e))?;
|
||||
|
||||
cpio_writer.start_entry(entry)?;
|
||||
// CpioEntryData::Data will have already been written.
|
||||
|
||||
+128
-91
@@ -1,31 +1,32 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
prelude::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::{ParallelSlice, ParallelSliceMut},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::verityrs,
|
||||
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
|
||||
util::{self, NumBytes},
|
||||
util::{self, NumBytes, OutOfBoundsError},
|
||||
};
|
||||
|
||||
// Not to be confused with the 255-byte RS block size.
|
||||
const FEC_BLOCK_SIZE: usize = 4096;
|
||||
const FEC_HEADER_SIZE: usize = 60;
|
||||
const FEC_MAGIC: u32 = 0xFECFECFE;
|
||||
const FEC_VERSION: u32 = 0;
|
||||
|
||||
@@ -64,9 +65,17 @@ pub enum Error {
|
||||
#[error("Expected FEC digest {expected}, but have {actual}")]
|
||||
InvalidFecDigest { expected: String, actual: String },
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to reopen input file")]
|
||||
InputReopen(#[source] io::Error),
|
||||
#[error("Failed to reopen output file")]
|
||||
OutputReopen(#[source] io::Error),
|
||||
#[error("Failed to read FEC data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write FEC data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -94,7 +103,7 @@ impl Codeword {
|
||||
&mut self.data[..usize::from(self.rs_k)]
|
||||
}
|
||||
|
||||
fn parity(&mut self) -> &[u8] {
|
||||
fn parity(&self) -> &[u8] {
|
||||
&self.data[usize::from(self.rs_k)..]
|
||||
}
|
||||
|
||||
@@ -155,10 +164,11 @@ impl Fec {
|
||||
input: file_size,
|
||||
block: block_size,
|
||||
});
|
||||
} else if block_size > FEC_MAX_BLOCK_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("block_size"));
|
||||
}
|
||||
|
||||
util::check_bounds(block_size, ..=FEC_MAX_BLOCK_SIZE)
|
||||
.map_err(|e| Error::IntOutOfBounds("block_size", e))?;
|
||||
|
||||
let rs_k = 255 - parity;
|
||||
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
|
||||
return Err(Error::UnsupportedParity(parity));
|
||||
@@ -172,11 +182,11 @@ impl Fec {
|
||||
.checked_mul(u64::from(parity))
|
||||
.and_then(|s| s.checked_mul(u64::from(block_size)))
|
||||
.and_then(|s| s.to_usize())
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_size"))?;
|
||||
.ok_or(Error::IntOverflow("fec_data_size"))?;
|
||||
rounds
|
||||
.checked_mul(u64::from(rs_k))
|
||||
.and_then(|s| s.checked_mul(u64::from(block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_grid_size"))?;
|
||||
.ok_or(Error::IntOverflow("fec_grid_size"))?;
|
||||
|
||||
Ok(Self {
|
||||
file_size,
|
||||
@@ -210,9 +220,8 @@ impl Fec {
|
||||
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > self.file_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
util::check_bounds(last.end, ..=self.file_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
@@ -361,7 +370,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let encode = verityrs::FN_ENCODE[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
@@ -382,7 +393,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let is_correct = verityrs::FN_IS_CORRECT[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
@@ -412,7 +425,9 @@ impl Fec {
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let mut grid = self.read_round(reader, round)?;
|
||||
let mut grid = self
|
||||
.read_round(reader, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
let mut num_corrected = 0;
|
||||
@@ -430,7 +445,8 @@ impl Fec {
|
||||
}
|
||||
|
||||
if num_corrected > 0 {
|
||||
self.write_round(writer, round, &grid)?;
|
||||
self.write_round(writer, round, &grid)
|
||||
.map_err(|e| Error::DataWrite("round", e))?;
|
||||
}
|
||||
|
||||
Ok(num_corrected)
|
||||
@@ -451,9 +467,9 @@ impl Fec {
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -486,9 +502,9 @@ impl Fec {
|
||||
.enumerate()
|
||||
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -519,9 +535,9 @@ impl Fec {
|
||||
fec.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.verify_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
@@ -560,10 +576,10 @@ impl Fec {
|
||||
.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<u64> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
|
||||
let reader = input.reopen_boxed()?;
|
||||
let writer = output.reopen_boxed()?;
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let writer = output.reopen_boxed().map_err(Error::OutputReopen)?;
|
||||
self.repair_one_round(reader, writer, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<Vec<u64>>>()?
|
||||
@@ -574,6 +590,26 @@ impl Fec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the FEC image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`FEC_MAGIC`].
|
||||
magic: little_endian::U32,
|
||||
/// Image version. This should be equal to [`FEC_VERSION`].
|
||||
version: little_endian::U32,
|
||||
/// Size of this [`RawHeader`].
|
||||
header_size: little_endian::U32,
|
||||
/// Number of parity bytes per 255-byte Reed-Solomon codeword.
|
||||
parity: little_endian::U32,
|
||||
/// Size of the FEC data.
|
||||
fec_size: little_endian::U32,
|
||||
/// Size of the actual data.
|
||||
data_size: little_endian::U64,
|
||||
/// SHA-256 digest of the FEC data.
|
||||
digest: [u8; 32],
|
||||
}
|
||||
|
||||
/// A type for reading and writing AOSP's standalone FEC image format.
|
||||
///
|
||||
/// The FEC data parser in this implementation is strict. All header fields,
|
||||
@@ -604,10 +640,10 @@ impl FecImage {
|
||||
parity: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let data_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let data_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
|
||||
let fec_data = fec.generate(input, cancel_signal)?;
|
||||
|
||||
@@ -667,26 +703,23 @@ impl FecImage {
|
||||
|
||||
/// Build one instance of the FEC header. The caller is responsible for
|
||||
/// writing it to both of the header locations at the end of the file.
|
||||
fn build_header(&self) -> Result<[u8; FEC_HEADER_SIZE]> {
|
||||
let fec_size = self
|
||||
.fec
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
|
||||
|
||||
let mut writer = Cursor::new([0u8; FEC_HEADER_SIZE]);
|
||||
fn build_header(&self) -> Result<RawHeader> {
|
||||
let fec_size: u32 =
|
||||
util::try_cast(self.fec.len()).map_err(|e| Error::IntOutOfBounds("fec_size", e))?;
|
||||
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, &self.fec);
|
||||
|
||||
writer.write_u32::<LittleEndian>(FEC_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(FEC_VERSION)?;
|
||||
writer.write_u32::<LittleEndian>(FEC_HEADER_SIZE as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.parity.into())?;
|
||||
writer.write_u32::<LittleEndian>(fec_size)?;
|
||||
writer.write_u64::<LittleEndian>(self.data_size)?;
|
||||
writer.write_all(digest.as_ref())?;
|
||||
let header = RawHeader {
|
||||
magic: FEC_MAGIC.into(),
|
||||
version: FEC_VERSION.into(),
|
||||
header_size: (mem::size_of::<RawHeader>() as u32).into(),
|
||||
parity: u32::from(self.parity).into(),
|
||||
fec_size: fec_size.into(),
|
||||
data_size: self.data_size.into(),
|
||||
digest: digest.as_ref().try_into().unwrap(),
|
||||
};
|
||||
|
||||
Ok(writer.into_inner())
|
||||
Ok(header)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,48 +730,47 @@ impl<R: Read> FromReader<R> for FecImage {
|
||||
// Avoid requiring seekable readers since we need to read everything
|
||||
// into memory anyway.
|
||||
let mut fec = Vec::new();
|
||||
reader.read_to_end(&mut fec)?;
|
||||
reader
|
||||
.read_to_end(&mut fec)
|
||||
.map_err(|e| Error::DataRead("fec", e))?;
|
||||
|
||||
if fec.len() < FEC_BLOCK_SIZE {
|
||||
return Err(Error::DataTooSmall);
|
||||
}
|
||||
|
||||
// Make sure both headers match.
|
||||
let header1_offset = fec.len() - FEC_BLOCK_SIZE;
|
||||
let header2_offset = fec.len() - FEC_HEADER_SIZE;
|
||||
let header1_raw = &fec[header1_offset..header1_offset + FEC_HEADER_SIZE];
|
||||
let header2_raw = &fec[header2_offset..header2_offset + FEC_HEADER_SIZE];
|
||||
|
||||
let (header, _) =
|
||||
RawHeader::ref_from_prefix(&fec[header1_offset..]).map_err(|_| Error::DataTooSmall)?;
|
||||
let header_size = header.header_size.get() as usize;
|
||||
|
||||
if header_size > FEC_BLOCK_SIZE / 2 {
|
||||
// ref_from_prefix() already handles the "too small" case.
|
||||
return Err(Error::InvalidHeaderSize(header.header_size.get()));
|
||||
}
|
||||
|
||||
let header2_offset = fec.len() - header_size;
|
||||
|
||||
// Make sure both headers match, accounting for potential custom fields.
|
||||
let header1_raw = &fec[header1_offset..][..header_size];
|
||||
let header2_raw = &fec[header2_offset..][..header_size];
|
||||
|
||||
if header1_raw != header2_raw {
|
||||
return Err(Error::HeadersDifferent);
|
||||
}
|
||||
|
||||
let mut header_reader = Cursor::new(header1_raw);
|
||||
|
||||
let magic = header_reader.read_u32::<LittleEndian>()?;
|
||||
if magic != FEC_MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(magic));
|
||||
if header.magic != FEC_MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(header.magic.get()));
|
||||
}
|
||||
|
||||
let version = header_reader.read_u32::<LittleEndian>()?;
|
||||
if version != FEC_VERSION {
|
||||
return Err(Error::UnsupportedHeaderVersion(version));
|
||||
if header.version != FEC_VERSION {
|
||||
return Err(Error::UnsupportedHeaderVersion(header.version.get()));
|
||||
}
|
||||
|
||||
let header_size = header_reader.read_u32::<LittleEndian>()?;
|
||||
if header_size != FEC_HEADER_SIZE as u32 {
|
||||
return Err(Error::InvalidHeaderSize(header_size));
|
||||
}
|
||||
let parity: u8 =
|
||||
util::try_cast(header.parity.get()).map_err(|e| Error::IntOutOfBounds("parity", e))?;
|
||||
|
||||
let parity = header_reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_u8()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("parity"))?;
|
||||
|
||||
let fec_size = header_reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
|
||||
let fec_size = header.fec_size.get() as usize;
|
||||
let actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
|
||||
if fec_size != actual_fec_size {
|
||||
return Err(Error::InvalidHeaderFecSize {
|
||||
@@ -747,25 +779,22 @@ impl<R: Read> FromReader<R> for FecImage {
|
||||
});
|
||||
}
|
||||
|
||||
let input_size = header_reader.read_u64::<LittleEndian>()?;
|
||||
let data_size = header.data_size.get();
|
||||
|
||||
let mut digest = [0u8; 32];
|
||||
header_reader.read_exact(&mut digest)?;
|
||||
|
||||
// Chop off headers.
|
||||
fec.resize(fec_size, 0);
|
||||
|
||||
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec);
|
||||
if digest != actual_digest.as_ref() {
|
||||
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec[..fec_size]);
|
||||
if header.digest != actual_digest.as_ref() {
|
||||
return Err(Error::InvalidFecDigest {
|
||||
expected: hex::encode(digest),
|
||||
expected: hex::encode(header.digest),
|
||||
actual: hex::encode(actual_digest),
|
||||
});
|
||||
}
|
||||
|
||||
// Chop off headers.
|
||||
fec.resize(fec_size, 0);
|
||||
|
||||
Ok(Self {
|
||||
fec,
|
||||
data_size: input_size,
|
||||
data_size,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
@@ -777,10 +806,18 @@ impl<W: Write> ToWriter<W> for FecImage {
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let header = self.build_header()?;
|
||||
|
||||
writer.write_all(&self.fec)?;
|
||||
writer.write_all(&header)?;
|
||||
writer.write_zeros_exact((FEC_BLOCK_SIZE - 2 * FEC_HEADER_SIZE) as u64)?;
|
||||
writer.write_all(&header)?;
|
||||
writer
|
||||
.write_all(&self.fec)
|
||||
.map_err(|e| Error::DataWrite("fec_data", e))?;
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("fec_header_1", e))?;
|
||||
writer
|
||||
.write_zeros_exact((FEC_BLOCK_SIZE - 2 * header.as_bytes().len()) as u64)
|
||||
.map_err(|e| Error::DataWrite("fec_header_padding", e))?;
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("fec_header_2", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -789,7 +826,7 @@ impl<W: Write> ToWriter<W> for FecImage {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::Seek,
|
||||
io::{Cursor, Seek},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
|
||||
+129
-86
@@ -1,27 +1,31 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, SeekFrom, Write},
|
||||
ops::Range,
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::ParallelSliceMut,
|
||||
};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::{avb, padding},
|
||||
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
|
||||
util::{self, NumBytes},
|
||||
format::{
|
||||
avb,
|
||||
padding::{self, ZeroPadding},
|
||||
},
|
||||
stream::{self, FromReader, ReadFixedSizeExt, ReadSeekReopen, ToWriter},
|
||||
util::{self, NumBytes, OutOfBoundsError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -40,12 +44,20 @@ pub enum Error {
|
||||
InvalidHeaderMagic([u8; 16]),
|
||||
#[error("Invalid hash tree header version: {0}")]
|
||||
InvalidHeaderVersion(u16),
|
||||
#[error("Hashing algorithm not supported: {0:?}")]
|
||||
UnsupportedHashAlgorithm(String),
|
||||
#[error("Hashing algorithm not supported: {:?}", .0.as_bstr())]
|
||||
UnsupportedHashAlgorithm(Vec<u8>),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to reopen input file")]
|
||||
InputReopen(#[source] io::Error),
|
||||
#[error("Failed to compute hash tree of input file")]
|
||||
InputDigest(#[source] io::Error),
|
||||
#[error("Failed to read hash tree data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write hash tree data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -81,13 +93,12 @@ impl HashTree {
|
||||
level_size = blocks
|
||||
.checked_mul(digest_size as u64)
|
||||
.and_then(|s| padding::round(s, u64::from(self.block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
.ok_or(Error::IntOverflow("level_size"))?;
|
||||
|
||||
// Depending on the chosen block size, the original file size could
|
||||
// overflow a usize without the first level's size doing the same.
|
||||
let level_size_usize = level_size
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
|
||||
let level_size_usize: usize =
|
||||
util::try_cast(level_size).map_err(|e| Error::IntOutOfBounds("level_size", e))?;
|
||||
|
||||
ranges.push(0..level_size_usize);
|
||||
}
|
||||
@@ -109,9 +120,8 @@ impl HashTree {
|
||||
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
|
||||
let ranges = util::merge_overlapping(ranges);
|
||||
if let Some(last) = ranges.last() {
|
||||
if last.end > image_size {
|
||||
return Err(Error::FieldOutOfBounds("ranges"));
|
||||
}
|
||||
util::check_bounds(last.end, ..=image_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
|
||||
}
|
||||
|
||||
let block_size = u64::from(self.block_size);
|
||||
@@ -185,7 +195,7 @@ impl HashTree {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
assert!(
|
||||
size > self.block_size as u64,
|
||||
size > u64::from(self.block_size),
|
||||
"Images smaller than block size must use a normal hash",
|
||||
);
|
||||
|
||||
@@ -260,9 +270,10 @@ impl HashTree {
|
||||
) -> Result<Vec<u8>> {
|
||||
// Small files are hashed directly.
|
||||
if image_size <= u64::from(self.block_size) {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut buf = vec![0u8; image_size as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let buf = reader
|
||||
.read_vec_exact(image_size as usize)
|
||||
.map_err(Error::InputDigest)?;
|
||||
|
||||
let mut context = self.salted_context.clone();
|
||||
context.update(&buf);
|
||||
@@ -287,7 +298,8 @@ impl HashTree {
|
||||
prev_size as u64,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::InputDigest)?;
|
||||
} else if let Some(r) = ranges {
|
||||
// Read partial blocks from file.
|
||||
let block_ranges = self.blocks_for_ranges(image_size, r)?;
|
||||
@@ -298,10 +310,12 @@ impl HashTree {
|
||||
&block_ranges,
|
||||
level_data,
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(Error::InputDigest)?;
|
||||
} else {
|
||||
// Read entire file.
|
||||
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)?;
|
||||
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)
|
||||
.map_err(Error::InputDigest)?;
|
||||
}
|
||||
|
||||
// No need to explicitly ensure the level is padded to the block
|
||||
@@ -325,7 +339,7 @@ impl HashTree {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
let mut hash_tree_data = vec![0u8; hash_tree_size];
|
||||
|
||||
let root_digest = self.calculate(
|
||||
@@ -351,7 +365,7 @@ impl HashTree {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
@@ -380,7 +394,7 @@ impl HashTree {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let offsets = self.compute_level_offsets(image_size)?;
|
||||
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
|
||||
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
|
||||
if hash_tree_data.len() != hash_tree_size {
|
||||
return Err(Error::InvalidHashTreeSize {
|
||||
input: image_size,
|
||||
@@ -415,6 +429,28 @@ impl HashTree {
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for our custom hash tree image header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HashTreeImage::MAGIC`].
|
||||
magic: [u8; 16],
|
||||
/// Image version. This should be equal to [`HashTreeImage::VERSION`].
|
||||
version: little_endian::U16,
|
||||
/// Size of the actual data.
|
||||
image_size: little_endian::U64,
|
||||
/// Block size.
|
||||
block_size: little_endian::U32,
|
||||
/// Hash algorithm.
|
||||
algorithm: [u8; 16],
|
||||
/// Salt size.
|
||||
salt_size: little_endian::U16,
|
||||
/// Root digest size.
|
||||
root_digest_size: little_endian::U16,
|
||||
/// Hash tree size.
|
||||
hash_tree_size: little_endian::U32,
|
||||
}
|
||||
|
||||
/// A type for reading and writing a custom hash tree image format.
|
||||
///
|
||||
/// File format:
|
||||
@@ -442,6 +478,7 @@ pub struct HashTreeImage {
|
||||
impl fmt::Debug for HashTreeImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("HashTreeImage")
|
||||
.field("image_size", &self.image_size)
|
||||
.field("block_size", &self.block_size)
|
||||
.field("algorithm", &self.algorithm)
|
||||
.field("salt", &hex::encode(&self.salt))
|
||||
@@ -455,9 +492,9 @@ impl HashTreeImage {
|
||||
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
|
||||
const VERSION: u16 = 1;
|
||||
|
||||
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::ring_algorithm(name, false)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
|
||||
}
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
@@ -468,10 +505,10 @@ impl HashTreeImage {
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let image_size = {
|
||||
let mut file = input.reopen_boxed()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let image_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let ring_algorithm = Self::ring_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
|
||||
@@ -530,40 +567,37 @@ impl<R: Read> FromReader<R> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(mut reader: R) -> Result<Self> {
|
||||
let mut magic = [0u8; 16];
|
||||
reader.read_exact(&mut magic)?;
|
||||
if magic != *Self::MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(magic));
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
if header.magic != *Self::MAGIC {
|
||||
return Err(Error::InvalidHeaderMagic(header.magic));
|
||||
}
|
||||
|
||||
let version = reader.read_u16::<LittleEndian>()?;
|
||||
if version != Self::VERSION {
|
||||
return Err(Error::InvalidHeaderVersion(version));
|
||||
if header.version != Self::VERSION {
|
||||
return Err(Error::InvalidHeaderVersion(header.version.get()));
|
||||
}
|
||||
|
||||
let image_size = reader.read_u64::<LittleEndian>()?;
|
||||
let block_size = reader.read_u32::<LittleEndian>()?;
|
||||
let algorithm = reader.read_string_padded(16)?;
|
||||
let salt_size = reader.read_u16::<LittleEndian>()?;
|
||||
let root_digest_size = reader.read_u16::<LittleEndian>()?;
|
||||
let hash_tree_size = reader
|
||||
.read_u32::<LittleEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
let algorithm = header.algorithm.trim_end_padding();
|
||||
let algorithm = str::from_utf8(algorithm)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(algorithm.to_vec()))?;
|
||||
|
||||
let mut salt = vec![0u8; usize::from(salt_size)];
|
||||
reader.read_exact(&mut salt)?;
|
||||
let salt = reader
|
||||
.read_vec_exact(usize::from(header.salt_size))
|
||||
.map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
|
||||
reader.read_exact(&mut root_digest)?;
|
||||
let root_digest = reader
|
||||
.read_vec_exact(usize::from(header.root_digest_size))
|
||||
.map_err(|e| Error::DataRead("root_digest", e))?;
|
||||
|
||||
let mut hash_tree = vec![0u8; hash_tree_size];
|
||||
reader.read_exact(&mut hash_tree)?;
|
||||
let hash_tree = reader
|
||||
.read_vec_exact(header.hash_tree_size.get() as usize)
|
||||
.map_err(|e| Error::DataRead("hash_tree", e))?;
|
||||
|
||||
Ok(Self {
|
||||
image_size,
|
||||
block_size,
|
||||
algorithm,
|
||||
image_size: header.image_size.get(),
|
||||
block_size: header.block_size.get(),
|
||||
algorithm: algorithm.to_owned(),
|
||||
salt,
|
||||
root_digest,
|
||||
hash_tree,
|
||||
@@ -575,33 +609,42 @@ impl<W: Write> ToWriter<W> for HashTreeImage {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let salt_size = self
|
||||
.salt
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
|
||||
let root_digest_size = self
|
||||
.root_digest
|
||||
.len()
|
||||
.to_u16()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
|
||||
let hash_tree_size = self
|
||||
.hash_tree
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
|
||||
let algorithm = self
|
||||
.algorithm
|
||||
.as_bytes()
|
||||
.to_padded_array::<16>()
|
||||
.ok_or_else(|| Error::UnsupportedHashAlgorithm(self.algorithm.as_bytes().to_vec()))?;
|
||||
|
||||
writer.write_all(Self::MAGIC)?;
|
||||
writer.write_u16::<LittleEndian>(Self::VERSION)?;
|
||||
writer.write_u64::<LittleEndian>(self.image_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.block_size)?;
|
||||
writer.write_string_padded(&self.algorithm, 16)?;
|
||||
writer.write_u16::<LittleEndian>(salt_size)?;
|
||||
writer.write_u16::<LittleEndian>(root_digest_size)?;
|
||||
writer.write_u32::<LittleEndian>(hash_tree_size)?;
|
||||
writer.write_all(&self.salt)?;
|
||||
writer.write_all(&self.root_digest)?;
|
||||
writer.write_all(&self.hash_tree)?;
|
||||
let salt_size: u16 =
|
||||
util::try_cast(self.salt.len()).map_err(|e| Error::IntOutOfBounds("salt_size", e))?;
|
||||
let root_digest_size: u16 = util::try_cast(self.root_digest.len())
|
||||
.map_err(|e| Error::IntOutOfBounds("root_digest_size", e))?;
|
||||
let hash_tree_size: u32 = util::try_cast(self.hash_tree.len())
|
||||
.map_err(|e| Error::IntOutOfBounds("hash_tree_size", e))?;
|
||||
|
||||
let header = RawHeader {
|
||||
magic: *Self::MAGIC,
|
||||
version: Self::VERSION.into(),
|
||||
image_size: self.image_size.into(),
|
||||
block_size: self.block_size.into(),
|
||||
algorithm,
|
||||
salt_size: salt_size.into(),
|
||||
root_digest_size: root_digest_size.into(),
|
||||
hash_tree_size: hash_tree_size.into(),
|
||||
};
|
||||
|
||||
header
|
||||
.write_to_io(&mut writer)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
writer
|
||||
.write_all(&self.salt)
|
||||
.map_err(|e| Error::DataWrite("salt", e))?;
|
||||
writer
|
||||
.write_all(&self.root_digest)
|
||||
.map_err(|e| Error::DataWrite("root_digest", e))?;
|
||||
writer
|
||||
.write_all(&self.hash_tree)
|
||||
.map_err(|e| Error::DataWrite("hash_tree", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -646,7 +689,7 @@ mod tests {
|
||||
);
|
||||
assert_matches!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16385]),
|
||||
Err(Error::FieldOutOfBounds(_))
|
||||
Err(Error::IntOutOfBounds(_, _))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+369
-226
@@ -14,12 +14,15 @@ use bitflags::bitflags;
|
||||
use bstr::ByteSlice;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{byteorder::little_endian, AsBytes, FromBytes, FromZeroes, Unaligned};
|
||||
use zerocopy::{byteorder::little_endian, FromBytes, FromZeros, Immutable, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::padding,
|
||||
stream::{CountingReader, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt},
|
||||
util::{self, is_zero},
|
||||
stream::{
|
||||
CountingReader, FromReader, ReadDiscardExt, ReadFixedSizeExt, ToWriter, WriteZerosExt,
|
||||
},
|
||||
util::{self, is_zero, DebugString},
|
||||
};
|
||||
|
||||
/// Magic value for [`RawGeometry::magic`].
|
||||
@@ -56,28 +59,138 @@ const METADATA_MAX_SIZE: u32 = 128 * 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Invalid partition name: {0}")]
|
||||
PartitionNameInvalid(String),
|
||||
#[error("Geometry: {0}")]
|
||||
Geometry(String),
|
||||
#[error("Descriptor offset #{0}: {1}")]
|
||||
Descriptor(u32, String),
|
||||
#[error("Header: {0}")]
|
||||
Header(String),
|
||||
#[error("Partition: {0}: {1}")]
|
||||
Partition(String, String),
|
||||
#[error("Metadata extent #{0}: {1}")]
|
||||
Extent(usize, String),
|
||||
#[error("Partition group: {0}: {1}")]
|
||||
PartitionGroup(String, String),
|
||||
#[error("Block device: {0}: {1}")]
|
||||
BlockDevice(String, String),
|
||||
#[error("Metadata: {0}")]
|
||||
Metadata(String),
|
||||
// Naming errors.
|
||||
#[error("Invalid partition name: {0:?}")]
|
||||
PartitionNameInvalid(DebugString),
|
||||
// Geometry errors.
|
||||
#[error("Invalid geometry magic: {0:#010x}")]
|
||||
GeometryInvalidMagic(u32),
|
||||
#[error("Invalid geometry size: {0} != {size}", size = mem::size_of::<RawGeometry>())]
|
||||
GeometryInvalidSize(u32),
|
||||
#[error("Expected geometry digest {expected}, but have {actual}")]
|
||||
GeometryInvalidDigest { expected: String, actual: String },
|
||||
#[error("Maximum metadata size is not sector-aligned: {0}")]
|
||||
MaxMetadataSizeUnaligned(u32),
|
||||
#[error("Maximum metadata size exceeds limit: {0} > {METADATA_MAX_SIZE}")]
|
||||
MaxMetadataSizeTooLarge(u32),
|
||||
#[error("No metadata slots defined")]
|
||||
NoMetadataSlots,
|
||||
#[error("Logical block size is not sector-aligned: {0}")]
|
||||
LogicalBlockSizeUnaligned(u32),
|
||||
// Descriptor errors.
|
||||
#[error("Descriptor offset #{0}: Entry count too large")]
|
||||
DescriptorEntryCountTooLarge(u32),
|
||||
#[error("Descriptor offset #{0}: Next entry offset too large")]
|
||||
DescriptorNextOffsetTooLarge(u32),
|
||||
// Header errors.
|
||||
#[error("Invalid header magic: {0:#010x}")]
|
||||
HeaderInvalidMagic(u32),
|
||||
#[error("Unsupported header version: {major}.{minor}")]
|
||||
HeaderUnsupportedVersion { major: u16, minor: u16 },
|
||||
#[error("Invalid header size: {0} != {size}", size = mem::size_of::<RawHeader>())]
|
||||
HeaderInvalidSize(u32),
|
||||
#[error("Expected header digest {expected}, but have {actual}")]
|
||||
HeaderInvalidDigest { expected: String, actual: String },
|
||||
#[error("Metadata slot exceeds maximum size: {metadata_size} > {max_size} - {header_size}")]
|
||||
MetadataTooLarge {
|
||||
metadata_size: u32,
|
||||
max_size: u32,
|
||||
header_size: u32,
|
||||
},
|
||||
#[error("Descriptors too large or have gaps")]
|
||||
DescriptorsTooLargeOrHaveGaps,
|
||||
#[error("Gap after last descriptor")]
|
||||
DescriptorsFinalGap,
|
||||
#[error("Invalid descriptor entry sizes")]
|
||||
DescriptorsInvalidEntrySizes,
|
||||
#[error("Descriptor entry count {entry_count} does not match {name} table length {table_len}")]
|
||||
DescriptorMismatchedEntryCount {
|
||||
name: &'static str,
|
||||
entry_count: u32,
|
||||
table_len: usize,
|
||||
},
|
||||
#[error("Expected tables digest {expected}, but have {actual}")]
|
||||
HeaderInvalidTablesDigest { expected: String, actual: String },
|
||||
// Partition errors.
|
||||
#[error("Partition {name:?}: Invalid attributes: {}", .attributes.0)]
|
||||
PartitionInvalidAttributes {
|
||||
name: DebugString,
|
||||
attributes: PartitionAttributes,
|
||||
},
|
||||
#[error("Partition {name:?}: Extent indices too large")]
|
||||
PartitionExtentIndicesTooLarge { name: DebugString },
|
||||
#[error("Partition {name:?}: Extent indices set on empty image")]
|
||||
PartitionExtentIndicesEmptyImage { name: DebugString },
|
||||
#[error("Partition {name:?}: Extent index too large")]
|
||||
PartitionExtentIndexTooLarge { name: DebugString },
|
||||
#[error("Partition {name:?}: Extent count too large")]
|
||||
PartitionExtentCountTooLarge { name: DebugString },
|
||||
#[error("Partition {name:?}: Invalid partition group index: {index}")]
|
||||
PartitionInvalidGroupIndex { name: DebugString, index: u32 },
|
||||
#[error("Partition {name:?}: Sector count too large")]
|
||||
PartitionSectorCountTooLarge { name: DebugString },
|
||||
#[error("Partition {name:?}: Byte count too large")]
|
||||
PartitionByteCountTooLarge { name: DebugString },
|
||||
// Extent errors.
|
||||
#[error("Extent #{index}: Invalid block device index: {device_index}")]
|
||||
ExtentInvalidDeviceIndex { index: usize, device_index: u32 },
|
||||
#[error("Extent #{index}: End sector too large: {start} + {count}")]
|
||||
ExtentEndSectorTooLarge {
|
||||
index: usize,
|
||||
start: u64,
|
||||
count: u64,
|
||||
},
|
||||
#[error("Extent #{index}: {start} starts before block device's first sector {sector}")]
|
||||
ExtentStartBeforeDeviceStart {
|
||||
index: usize,
|
||||
start: u64,
|
||||
sector: u64,
|
||||
},
|
||||
#[error("Extent #{index}: {end} ends after block device's last sector {sector}")]
|
||||
ExtentEndsAfterDeviceEnd { index: usize, end: u64, sector: u64 },
|
||||
#[error("Extent #{index}: Type zero extents cannot have non-zero sector or device")]
|
||||
ExtentTypeZeroNotEmpty { index: usize },
|
||||
#[error("Extent #{index}: Invalid type: {extent_type}")]
|
||||
ExtentInvalidType { index: usize, extent_type: u32 },
|
||||
#[error("Extent #{index}: Overlaps previous extent")]
|
||||
ExtentOverlapsPrevious { index: usize },
|
||||
#[error("Extent #{index}: Earlier block device index than previous extent")]
|
||||
ExtentDeviceNotConsecutive { index: usize },
|
||||
#[error("Extent #{index}: Block device index too large")]
|
||||
ExtentDeviceIndexTooLarge { index: usize },
|
||||
// Partition group errors.
|
||||
#[error("Group {name:?}: Total size of partitions too large")]
|
||||
GroupTotalSizeTooLarge { name: DebugString },
|
||||
#[error("Group {name:?}: Total partition size {size} exceeds limit {limit}")]
|
||||
GroupTotalSizeExceedsLimit {
|
||||
name: DebugString,
|
||||
size: u64,
|
||||
limit: u64,
|
||||
},
|
||||
#[error("Group {name:?}: Index too large")]
|
||||
GroupIndexTooLarge { name: DebugString },
|
||||
// Block device errors.
|
||||
#[error("Device {name:?}: Alignment is 0")]
|
||||
DeviceAlignmentIsZero { name: DebugString },
|
||||
#[error("Device {name:?}: Partition alignment is not sector-aligned")]
|
||||
DeviceAlignmentNotSectorAligned { name: DebugString },
|
||||
#[error("Device {name:?}: First logical sector is not partition-aligned")]
|
||||
DeviceFirstSectorNotAligned { name: DebugString },
|
||||
#[error("Device {name:?}: Alignment offset is not sector-aligned")]
|
||||
DeviceOffsetNotSectorAligned { name: DebugString },
|
||||
#[error("Device {name:?}: Size is not sector-aligned")]
|
||||
DeviceSizeNotSectorAligned { name: DebugString },
|
||||
// Metadata errors.
|
||||
#[error("Expected slot count {expected}, but have {actual}")]
|
||||
MismatchedSlotCount { expected: usize, actual: usize },
|
||||
// Allocator errors.
|
||||
#[error("Insufficient space on block devices to allocate sectors")]
|
||||
AllocatorDeviceFull,
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
// Wrapped errors.
|
||||
#[error("Failed to read LP data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write LP data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -139,7 +252,7 @@ impl PartitionAttributes {
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the metadata geometry.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawGeometry {
|
||||
/// Magic value. This should be equal to [`GEOMETRY_MAGIC`].
|
||||
@@ -179,17 +292,11 @@ impl RawGeometry {
|
||||
/// further checks.
|
||||
fn validate(&self) -> Result<()> {
|
||||
if self.magic.get() != GEOMETRY_MAGIC {
|
||||
return Err(Error::Geometry(format!(
|
||||
"Invalid magic: {:#010x}",
|
||||
self.magic.get(),
|
||||
)));
|
||||
return Err(Error::GeometryInvalidMagic(self.magic.get()));
|
||||
}
|
||||
|
||||
if self.struct_size.get() != mem::size_of::<Self>() as u32 {
|
||||
return Err(Error::Geometry(format!(
|
||||
"Invalid struct size: {}",
|
||||
self.struct_size.get(),
|
||||
)));
|
||||
return Err(Error::GeometryInvalidSize(self.struct_size.get()));
|
||||
}
|
||||
|
||||
#[cfg(not(fuzzing))]
|
||||
@@ -199,33 +306,27 @@ impl RawGeometry {
|
||||
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, copy.as_bytes());
|
||||
if digest.as_ref() != self.checksum {
|
||||
return Err(Error::Geometry(format!(
|
||||
"Expected digest {}, but have {}",
|
||||
hex::encode(self.checksum),
|
||||
hex::encode(digest),
|
||||
)));
|
||||
return Err(Error::GeometryInvalidDigest {
|
||||
expected: hex::encode(self.checksum),
|
||||
actual: hex::encode(digest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.metadata_max_size.get() == 0 || self.metadata_max_size.get() % SECTOR_SIZE != 0 {
|
||||
return Err(Error::Geometry(format!(
|
||||
"Maximum metadata size is not sector-aligned: {}",
|
||||
return Err(Error::MaxMetadataSizeUnaligned(
|
||||
self.metadata_max_size.get(),
|
||||
)));
|
||||
));
|
||||
} else if self.metadata_max_size.get() > METADATA_MAX_SIZE {
|
||||
return Err(Error::Geometry(format!(
|
||||
"Maximum metadata size exceeds limit: {} > {METADATA_MAX_SIZE}",
|
||||
self.metadata_max_size.get(),
|
||||
)));
|
||||
return Err(Error::MaxMetadataSizeTooLarge(self.metadata_max_size.get()));
|
||||
} else if self.metadata_slot_count.get() == 0 {
|
||||
return Err(Error::Geometry("No metadata slots defined".into()));
|
||||
return Err(Error::NoMetadataSlots);
|
||||
}
|
||||
|
||||
if self.logical_block_size.get() % SECTOR_SIZE != 0 {
|
||||
return Err(Error::Geometry(format!(
|
||||
"Logical block size is not sector-aligned: {}",
|
||||
return Err(Error::LogicalBlockSizeUnaligned(
|
||||
self.logical_block_size.get(),
|
||||
)));
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -233,7 +334,7 @@ impl RawGeometry {
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for a table descriptor within a [`RawHeader`].
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawTableDescriptor {
|
||||
/// Offset relative to the end of the [`RawHeader`].
|
||||
@@ -259,7 +360,7 @@ impl RawTableDescriptor {
|
||||
/// buffer. `buf` must have size [`RawHeader::tables_size`] and the header
|
||||
/// containing this descriptor must have already passed
|
||||
/// [`RawHeader::validate`]. Otherwise, this function may panic.
|
||||
fn slice_from_buf<'a, T: FromBytes + 'a>(&self, buf: &'a [u8]) -> &'a [T] {
|
||||
fn slice_from_buf<'a, T: FromBytes + Immutable + 'a>(&self, buf: &'a [u8]) -> &'a [T] {
|
||||
let offset = self.offset.get() as usize;
|
||||
let entry_size = self.entry_size.get() as usize;
|
||||
let size = self.num_entries.get() as usize * entry_size;
|
||||
@@ -267,21 +368,21 @@ impl RawTableDescriptor {
|
||||
|
||||
assert_eq!(mem::size_of::<T>(), entry_size);
|
||||
|
||||
T::slice_from(buf).unwrap()
|
||||
<[T]>::ref_from_bytes(buf).unwrap()
|
||||
}
|
||||
|
||||
/// Update all fields to match the slice of items beginning at the specified
|
||||
/// table offset. Returns the starting offset for the next table.
|
||||
fn update<T: AsBytes>(&mut self, items: &[T], offset: u32) -> Result<u32> {
|
||||
fn update<T: IntoBytes>(&mut self, items: &[T], offset: u32) -> Result<u32> {
|
||||
let entry_size = mem::size_of::<T>() as u32;
|
||||
let num_entries: u32 = items
|
||||
.len()
|
||||
.try_into()
|
||||
.map_err(|_| Error::Descriptor(offset, "Entry count out of bounds".into()))?;
|
||||
.map_err(|_| Error::DescriptorEntryCountTooLarge(offset))?;
|
||||
let next_offset = entry_size
|
||||
.checked_mul(num_entries)
|
||||
.and_then(|o| o.checked_add(offset))
|
||||
.ok_or_else(|| Error::Descriptor(offset, "Next entry offset out of bounds".into()))?;
|
||||
.ok_or(Error::DescriptorNextOffsetTooLarge(offset))?;
|
||||
|
||||
self.offset = offset.into();
|
||||
self.entry_size = entry_size.into();
|
||||
@@ -292,7 +393,7 @@ impl RawTableDescriptor {
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the metadata header.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HEADER_MAGIC`].
|
||||
@@ -389,28 +490,21 @@ impl RawHeader {
|
||||
/// function is called.
|
||||
fn validate(&self, geometry: &RawGeometry) -> Result<()> {
|
||||
if self.magic.get() != HEADER_MAGIC {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid magic: {:#010x}",
|
||||
self.magic.get(),
|
||||
)));
|
||||
return Err(Error::HeaderInvalidMagic(self.magic.get()));
|
||||
}
|
||||
|
||||
if self.major_version.get() != MAJOR_VERSION || self.minor_version.get() > MINOR_VERSION_MAX
|
||||
{
|
||||
return Err(Error::Header(format!(
|
||||
"Unsupported version: {}.{}",
|
||||
self.major_version.get(),
|
||||
self.minor_version.get(),
|
||||
)));
|
||||
return Err(Error::HeaderUnsupportedVersion {
|
||||
major: self.major_version.get(),
|
||||
minor: self.minor_version.get(),
|
||||
});
|
||||
}
|
||||
|
||||
let expected_size = self.size();
|
||||
|
||||
if self.header_size.get() != expected_size as u32 {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid struct size: {}",
|
||||
self.header_size.get(),
|
||||
)));
|
||||
return Err(Error::HeaderInvalidSize(self.header_size.get()));
|
||||
}
|
||||
|
||||
if self.minor_version.get() < VERSION_FOR_EXPANDED_HEADER {
|
||||
@@ -428,18 +522,21 @@ impl RawHeader {
|
||||
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, portion);
|
||||
if digest.as_ref() != self.header_checksum {
|
||||
return Err(Error::Header(format!(
|
||||
"Expected header digest {}, but have {}",
|
||||
hex::encode(self.header_checksum),
|
||||
hex::encode(digest),
|
||||
)));
|
||||
return Err(Error::HeaderInvalidDigest {
|
||||
expected: hex::encode(self.header_checksum),
|
||||
actual: hex::encode(digest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// metadata_max_size is guaranteed to be at least one sector, so the
|
||||
// subtraction cannot overflow.
|
||||
if self.tables_size.get() > geometry.metadata_max_size.get() - self.header_size.get() {
|
||||
return Err(Error::Header("Metadata slot exceeds maximum size".into()));
|
||||
return Err(Error::MetadataTooLarge {
|
||||
metadata_size: self.tables_size.get(),
|
||||
max_size: geometry.metadata_max_size.get(),
|
||||
header_size: self.header_size.get(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut offset = 0;
|
||||
@@ -453,12 +550,12 @@ impl RawHeader {
|
||||
] {
|
||||
offset = self
|
||||
.validate_descriptor(descriptor, offset)
|
||||
.ok_or_else(|| Error::Header("Descriptors out of bounds".into()))?;
|
||||
.ok_or(Error::DescriptorsTooLargeOrHaveGaps)?;
|
||||
}
|
||||
|
||||
// There cannot be a gap at the end either.
|
||||
if offset != self.tables_size.get() {
|
||||
return Err(Error::Header("Gap after last descriptor".into()));
|
||||
return Err(Error::DescriptorsFinalGap);
|
||||
}
|
||||
|
||||
if self.partitions.entry_size.get() != mem::size_of::<RawPartition>() as u32
|
||||
@@ -466,7 +563,7 @@ impl RawHeader {
|
||||
|| self.groups.entry_size.get() != mem::size_of::<RawPartitionGroup>() as u32
|
||||
|| self.block_devices.entry_size.get() != mem::size_of::<RawBlockDevice>() as u32
|
||||
{
|
||||
return Err(Error::Header("Invalid descriptor entry sizes".into()));
|
||||
return Err(Error::DescriptorsInvalidEntrySizes);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -474,7 +571,7 @@ impl RawHeader {
|
||||
}
|
||||
|
||||
/// A potentially invalid raw partition name string.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct PartitionName([u8; 36]);
|
||||
|
||||
@@ -493,10 +590,10 @@ impl fmt::Debug for PartitionName {
|
||||
|
||||
impl PartitionName {
|
||||
fn split(&self) -> (&[u8], &[u8]) {
|
||||
match self.0.iter().position(|b| *b == 0) {
|
||||
Some(i) => self.0.split_at(i),
|
||||
None => (&self.0, &[]),
|
||||
}
|
||||
self.0
|
||||
.iter()
|
||||
.position(|b| *b == 0)
|
||||
.map_or((&self.0, &[]), |i| self.0.split_at(i))
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<()> {
|
||||
@@ -507,14 +604,14 @@ impl PartitionName {
|
||||
match b {
|
||||
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' => has_alnum = true,
|
||||
b'_' => {}
|
||||
_ => return Err(Error::PartitionNameInvalid(format!("{self:?}"))),
|
||||
_ => return Err(Error::PartitionNameInvalid(DebugString::new(self))),
|
||||
}
|
||||
}
|
||||
|
||||
if has_alnum && is_zero(suffix) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::PartitionNameInvalid(format!("{self:?}")))
|
||||
Err(Error::PartitionNameInvalid(DebugString::new(self)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,7 +630,7 @@ impl FromStr for PartitionName {
|
||||
let mut name = Self([0u8; 36]);
|
||||
|
||||
if s.len() > name.0.len() {
|
||||
return Err(Error::PartitionNameInvalid(format!("{s:?}")));
|
||||
return Err(Error::PartitionNameInvalid(DebugString::new(s)));
|
||||
}
|
||||
|
||||
let to_copy = s.len().min(name.0.len());
|
||||
@@ -546,7 +643,7 @@ impl FromStr for PartitionName {
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for an entry in the logical partitions table.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawPartition {
|
||||
/// Partition name in ASCII. This must be unique across all partitions.
|
||||
@@ -597,10 +694,10 @@ impl RawPartition {
|
||||
let attributes = PartitionAttributes::from_bits_retain(self.attributes.get());
|
||||
|
||||
if !(attributes - valid_attributes).is_empty() {
|
||||
return Err(Error::Partition(
|
||||
format!("{:?}", self.name),
|
||||
format!("Invalid attributes: {}", attributes.0),
|
||||
));
|
||||
return Err(Error::PartitionInvalidAttributes {
|
||||
name: DebugString::new(self.name),
|
||||
attributes,
|
||||
});
|
||||
}
|
||||
|
||||
match image_type {
|
||||
@@ -611,27 +708,25 @@ impl RawPartition {
|
||||
.checked_add(self.num_extents.get())
|
||||
.map_or(true, |n| n as usize > extents.len())
|
||||
{
|
||||
return Err(Error::Partition(
|
||||
format!("{:?}", self.name),
|
||||
"Extent indices out of bounds".into(),
|
||||
));
|
||||
return Err(Error::PartitionExtentIndicesTooLarge {
|
||||
name: DebugString::new(self.name),
|
||||
});
|
||||
}
|
||||
}
|
||||
ImageType::Empty => {
|
||||
if self.first_extent_index.get() != 0 || self.num_extents.get() != 0 {
|
||||
return Err(Error::Partition(
|
||||
format!("{:?}", self.name),
|
||||
"Extent indices set on empty image".into(),
|
||||
));
|
||||
return Err(Error::PartitionExtentIndicesEmptyImage {
|
||||
name: DebugString::new(self.name),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.group_index.get() as usize >= groups.len() {
|
||||
return Err(Error::Partition(
|
||||
format!("{:?}", self.name),
|
||||
format!("Invalid partition group index: {}", self.group_index.get()),
|
||||
));
|
||||
return Err(Error::PartitionInvalidGroupIndex {
|
||||
name: DebugString::new(self.name),
|
||||
index: self.group_index.get(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -639,7 +734,7 @@ impl RawPartition {
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for an entry in the extent table.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawExtent {
|
||||
/// Number of [`SECTOR_SIZE`]-byte sectors in this extent.
|
||||
@@ -679,49 +774,51 @@ impl RawExtent {
|
||||
match self.target_type.get() {
|
||||
Self::TARGET_TYPE_LINEAR => {
|
||||
let Some(device) = block_devices.get(self.target_source.get() as usize) else {
|
||||
return Err(Error::Extent(
|
||||
return Err(Error::ExtentInvalidDeviceIndex {
|
||||
index,
|
||||
format!("Invalid block device index: {}", self.target_source.get()),
|
||||
));
|
||||
device_index: self.target_source.get(),
|
||||
});
|
||||
};
|
||||
|
||||
let count = self.num_sectors.get();
|
||||
let start = self.target_data.get();
|
||||
let end = start.checked_add(count).ok_or_else(|| {
|
||||
Error::Extent(
|
||||
let end = start.checked_add(count).ok_or({
|
||||
Error::ExtentEndSectorTooLarge {
|
||||
index,
|
||||
format!("End sector out of bounds: {start} + {count}"),
|
||||
)
|
||||
start,
|
||||
count,
|
||||
}
|
||||
})?;
|
||||
|
||||
if start < device.first_logical_sector.get() {
|
||||
return Err(Error::Extent(
|
||||
return Err(Error::ExtentStartBeforeDeviceStart {
|
||||
index,
|
||||
format!(
|
||||
"{start} starts before block device's first logical sector {}",
|
||||
device.first_logical_sector,
|
||||
),
|
||||
));
|
||||
start,
|
||||
sector: device.first_logical_sector.get(),
|
||||
});
|
||||
}
|
||||
|
||||
let device_sectors = device.size.get() / u64::from(SECTOR_SIZE);
|
||||
|
||||
if end > device_sectors {
|
||||
return Err(Error::Extent(
|
||||
return Err(Error::ExtentEndsAfterDeviceEnd {
|
||||
index,
|
||||
format!("{end} ends after block device's sector size {device_sectors}"),
|
||||
));
|
||||
end,
|
||||
sector: device_sectors,
|
||||
});
|
||||
}
|
||||
}
|
||||
Self::TARGET_TYPE_ZERO => {
|
||||
if self.target_data.get() != 0 || self.target_source.get() != 0 {
|
||||
return Err(Error::Extent(
|
||||
index,
|
||||
"Type zero extents cannot have non-zero sector or device".into(),
|
||||
));
|
||||
return Err(Error::ExtentTypeZeroNotEmpty { index });
|
||||
}
|
||||
}
|
||||
n => return Err(Error::Extent(index, format!("Invalid type: {n}"))),
|
||||
n => {
|
||||
return Err(Error::ExtentInvalidType {
|
||||
index,
|
||||
extent_type: n,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -729,7 +826,7 @@ impl RawExtent {
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for an entry in the partition groups table.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawPartitionGroup {
|
||||
/// Partition group name in ASCII. This must be unique across all groups.
|
||||
@@ -775,24 +872,19 @@ impl RawPartitionGroup {
|
||||
for extent in &extents[first..][..count] {
|
||||
total_size = total_size
|
||||
.checked_add(extent.num_sectors.get())
|
||||
.ok_or_else(|| {
|
||||
Error::PartitionGroup(
|
||||
format!("{:?}", self.name),
|
||||
"Size of group's partitions out of bounds".into(),
|
||||
)
|
||||
.ok_or_else(|| Error::GroupTotalSizeTooLarge {
|
||||
name: DebugString::new(self.name),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if total_size > self.maximum_size.get() {
|
||||
return Err(Error::PartitionGroup(
|
||||
format!("{:?}", self.name),
|
||||
format!(
|
||||
"Total partition size {total_size} exceeds limit {}",
|
||||
self.maximum_size.get(),
|
||||
),
|
||||
));
|
||||
return Err(Error::GroupTotalSizeExceedsLimit {
|
||||
name: DebugString::new(self.name),
|
||||
size: total_size,
|
||||
limit: self.maximum_size.get(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -801,7 +893,7 @@ impl RawPartitionGroup {
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for an entry in the block devices table.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawBlockDevice {
|
||||
/// The first [`SECTOR_SIZE`]-byte sector where actual data for the logical
|
||||
@@ -839,37 +931,32 @@ impl RawBlockDevice {
|
||||
/// further checks.
|
||||
fn validate(&self) -> Result<()> {
|
||||
if self.alignment.get() == 0 {
|
||||
return Err(Error::BlockDevice(
|
||||
format!("{:?}", self.partition_name),
|
||||
"Alignment is 0".into(),
|
||||
));
|
||||
return Err(Error::DeviceAlignmentIsZero {
|
||||
name: DebugString::new(self.partition_name),
|
||||
});
|
||||
} else if self.alignment.get() % SECTOR_SIZE != 0 {
|
||||
return Err(Error::BlockDevice(
|
||||
format!("{:?}", self.partition_name),
|
||||
"Partition alignment is not sector-aligned".into(),
|
||||
));
|
||||
return Err(Error::DeviceAlignmentNotSectorAligned {
|
||||
name: DebugString::new(self.partition_name),
|
||||
});
|
||||
}
|
||||
|
||||
let alignment_sectors = u64::from(self.alignment.get() / SECTOR_SIZE);
|
||||
if self.first_logical_sector.get() % alignment_sectors != 0 {
|
||||
return Err(Error::BlockDevice(
|
||||
format!("{:?}", self.partition_name),
|
||||
"First logical sector is not partition-aligned".into(),
|
||||
));
|
||||
return Err(Error::DeviceFirstSectorNotAligned {
|
||||
name: DebugString::new(self.partition_name),
|
||||
});
|
||||
}
|
||||
|
||||
if self.alignment_offset.get() % SECTOR_SIZE != 0 {
|
||||
return Err(Error::BlockDevice(
|
||||
format!("{:?}", self.partition_name),
|
||||
"Alignment offset is not sector-aligned".into(),
|
||||
));
|
||||
return Err(Error::DeviceOffsetNotSectorAligned {
|
||||
name: DebugString::new(self.partition_name),
|
||||
});
|
||||
}
|
||||
|
||||
if self.size.get() % u64::from(SECTOR_SIZE) != 0 {
|
||||
return Err(Error::BlockDevice(
|
||||
format!("{:?}", self.partition_name),
|
||||
"Size is not sector-aligned".into(),
|
||||
));
|
||||
return Err(Error::DeviceSizeNotSectorAligned {
|
||||
name: DebugString::new(self.partition_name),
|
||||
});
|
||||
}
|
||||
|
||||
self.partition_name.validate()
|
||||
@@ -903,10 +990,11 @@ impl RawMetadataSlot {
|
||||
),
|
||||
] {
|
||||
if len != descriptor.num_entries.get() as usize {
|
||||
return Err(Error::Header(format!(
|
||||
"Descriptor entries {} does not match {name} table length {len}",
|
||||
descriptor.num_entries.get(),
|
||||
)));
|
||||
return Err(Error::DescriptorMismatchedEntryCount {
|
||||
name,
|
||||
entry_count: descriptor.num_entries.get(),
|
||||
table_len: len,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,14 +1020,11 @@ impl RawMetadataSlot {
|
||||
match a.target_source.get().cmp(&b.target_source.get()) {
|
||||
Ordering::Equal => {
|
||||
if a.target_data.get() + a.num_sectors.get() > b.target_data.get() {
|
||||
return Err(Error::Extent(i, "Overlaps previous extent".into()));
|
||||
return Err(Error::ExtentOverlapsPrevious { index: i });
|
||||
}
|
||||
}
|
||||
Ordering::Greater => {
|
||||
return Err(Error::Extent(
|
||||
i,
|
||||
"Earlier block device index than previous extent".into(),
|
||||
));
|
||||
return Err(Error::ExtentDeviceNotConsecutive { index: i });
|
||||
}
|
||||
Ordering::Less => {}
|
||||
}
|
||||
@@ -971,7 +1056,9 @@ impl RawMetadata {
|
||||
/// Read the [`RawGeometry`] at the current offset.
|
||||
fn read_geometry(mut reader: impl Read) -> Result<(ImageType, RawGeometry)> {
|
||||
let mut buf = [0u8; GEOMETRY_SIZE as usize];
|
||||
reader.read_exact(&mut buf)?;
|
||||
reader
|
||||
.read_exact(&mut buf)
|
||||
.map_err(|e| Error::DataRead("geometry", e))?;
|
||||
|
||||
let image_type = if util::is_zero(&buf) {
|
||||
ImageType::Normal
|
||||
@@ -986,22 +1073,25 @@ impl RawMetadata {
|
||||
// as a boot sector.
|
||||
|
||||
// Read the primary copy of the geometry.
|
||||
reader.read_exact(&mut buf)?;
|
||||
reader
|
||||
.read_exact(&mut buf)
|
||||
.map_err(|e| Error::DataRead("geometry_primary", e))?;
|
||||
|
||||
let mut geometry = RawGeometry::ref_from_prefix(&buf).unwrap();
|
||||
let mut geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
|
||||
|
||||
match geometry.validate() {
|
||||
Ok(_) => {
|
||||
// Skip the backup copy.
|
||||
reader.read_discard_exact(GEOMETRY_SIZE.into())?;
|
||||
}
|
||||
Err(_) => {
|
||||
// Try to parse the backup copy.
|
||||
reader.read_exact(&mut buf)?;
|
||||
if geometry.validate().is_ok() {
|
||||
// Skip the backup copy.
|
||||
reader
|
||||
.read_discard_exact(GEOMETRY_SIZE.into())
|
||||
.map_err(|e| Error::DataRead("geometry_secondary", e))?;
|
||||
} else {
|
||||
// Try to parse the backup copy.
|
||||
reader
|
||||
.read_exact(&mut buf)
|
||||
.map_err(|e| Error::DataRead("geometry_secondary", e))?;
|
||||
|
||||
geometry = RawGeometry::ref_from_prefix(&buf).unwrap();
|
||||
geometry.validate()?;
|
||||
}
|
||||
geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
|
||||
geometry.validate()?;
|
||||
}
|
||||
|
||||
geometry
|
||||
@@ -1010,7 +1100,7 @@ impl RawMetadata {
|
||||
// This is an empty image for use with fastboot. These have no
|
||||
// extra padding at the beginning of the file nor backup copies
|
||||
// of the geometry and metadata structs.
|
||||
let geometry = RawGeometry::ref_from_prefix(&buf).unwrap();
|
||||
let geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
|
||||
geometry.validate()?;
|
||||
|
||||
geometry
|
||||
@@ -1030,9 +1120,13 @@ impl RawMetadata {
|
||||
) -> Result<RawMetadataSlot> {
|
||||
let mut header = RawHeader::new_zeroed();
|
||||
|
||||
reader.read_exact(&mut header.as_bytes_mut()[..RawHeader::SIZE_V1_0])?;
|
||||
reader
|
||||
.read_exact(&mut header.as_mut_bytes()[..RawHeader::SIZE_V1_0])
|
||||
.map_err(|e| Error::DataRead("header_v1.0", e))?;
|
||||
if header.size() > RawHeader::SIZE_V1_0 {
|
||||
reader.read_exact(&mut header.as_bytes_mut()[RawHeader::SIZE_V1_0..])?;
|
||||
reader
|
||||
.read_exact(&mut header.as_mut_bytes()[RawHeader::SIZE_V1_0..])
|
||||
.map_err(|e| Error::DataRead("header_v1.2", e))?;
|
||||
}
|
||||
|
||||
// We'll end up validating this again at the end, but this initial
|
||||
@@ -1040,8 +1134,9 @@ impl RawMetadata {
|
||||
// parsing the tables.
|
||||
header.validate(geometry)?;
|
||||
|
||||
let mut tables_buf = vec![0u8; header.tables_size.get() as usize];
|
||||
reader.read_exact(&mut tables_buf)?;
|
||||
let tables_buf = reader
|
||||
.read_vec_exact(header.tables_size.get() as usize)
|
||||
.map_err(|e| Error::DataRead("tables", e))?;
|
||||
|
||||
let partitions = header
|
||||
.partitions
|
||||
@@ -1077,10 +1172,10 @@ impl RawMetadata {
|
||||
ImageType::Empty => 1,
|
||||
};
|
||||
if self.slots.len() != expected_slots {
|
||||
return Err(Error::Metadata(format!(
|
||||
"Expected slot count {expected_slots}, but have {}",
|
||||
self.slots.len(),
|
||||
)));
|
||||
return Err(Error::MismatchedSlotCount {
|
||||
expected: expected_slots,
|
||||
actual: self.slots.len(),
|
||||
});
|
||||
}
|
||||
|
||||
for slot in &self.slots {
|
||||
@@ -1094,11 +1189,10 @@ impl RawMetadata {
|
||||
let digest = context.finish();
|
||||
|
||||
if digest.as_ref() != slot.header.tables_checksum {
|
||||
return Err(Error::Header(format!(
|
||||
"Expected tables digest {}, but have {}",
|
||||
hex::encode(slot.header.tables_checksum),
|
||||
hex::encode(digest),
|
||||
)));
|
||||
return Err(Error::HeaderInvalidTablesDigest {
|
||||
expected: hex::encode(slot.header.tables_checksum),
|
||||
actual: hex::encode(digest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1133,20 +1227,26 @@ impl<R: Read> FromReader<R> for RawMetadata {
|
||||
let mut to_skip = u64::from(geometry.metadata_max_size.get());
|
||||
|
||||
if slot.is_none() {
|
||||
let orig_offset = reader.stream_position()?;
|
||||
let orig_offset = reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("orig_offset", e))?;
|
||||
|
||||
match Self::read_metadata(&mut reader, image_type, &geometry) {
|
||||
Ok(m) => *slot = Some(m),
|
||||
Err(e @ Error::Io(_)) => return Err(e),
|
||||
Err(e @ Error::DataRead(_, _)) => return Err(e),
|
||||
Err(e) => last_err = Some(e),
|
||||
}
|
||||
|
||||
// Skip the remaining padding.
|
||||
let cur_offset = reader.stream_position()?;
|
||||
let cur_offset = reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("cur_offset", e))?;
|
||||
to_skip -= cur_offset - orig_offset;
|
||||
}
|
||||
|
||||
reader.read_discard(to_skip)?;
|
||||
reader
|
||||
.read_discard(to_skip)
|
||||
.map_err(|e| Error::DataRead("slot_padding", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1174,11 +1274,17 @@ impl<W: Write> ToWriter<W> for RawMetadata {
|
||||
|
||||
match self.image_type {
|
||||
ImageType::Normal => {
|
||||
writer.write_zeros_exact(PARTITION_RESERVED_BYTES.into())?;
|
||||
writer
|
||||
.write_zeros_exact(PARTITION_RESERVED_BYTES.into())
|
||||
.map_err(|e| Error::DataWrite("reserved", e))?;
|
||||
|
||||
for _ in 0..2 {
|
||||
writer.write_all(geometry)?;
|
||||
writer.write_zeros_exact(geometry_padding as u64)?;
|
||||
writer
|
||||
.write_all(geometry)
|
||||
.map_err(|e| Error::DataWrite("geometry", e))?;
|
||||
writer
|
||||
.write_zeros_exact(geometry_padding as u64)
|
||||
.map_err(|e| Error::DataWrite("geometry_padding", e))?;
|
||||
}
|
||||
|
||||
let metadata_max_size = self.geometry.metadata_max_size.get() as usize;
|
||||
@@ -1193,24 +1299,50 @@ impl<W: Write> ToWriter<W> for RawMetadata {
|
||||
let tables_size = slot.header.tables_size.get() as usize;
|
||||
let metadata_padding = metadata_max_size - header.len() - tables_size;
|
||||
|
||||
writer.write_all(header)?;
|
||||
writer.write_all(slot.partitions.as_bytes())?;
|
||||
writer.write_all(slot.extents.as_bytes())?;
|
||||
writer.write_all(slot.groups.as_bytes())?;
|
||||
writer.write_all(slot.block_devices.as_bytes())?;
|
||||
writer.write_zeros_exact(metadata_padding as u64)?;
|
||||
writer
|
||||
.write_all(header)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
writer
|
||||
.write_all(slot.partitions.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("partition_tables", e))?;
|
||||
writer
|
||||
.write_all(slot.extents.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("extent_tables", e))?;
|
||||
writer
|
||||
.write_all(slot.groups.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("group_tables", e))?;
|
||||
writer
|
||||
.write_all(slot.block_devices.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("block_device_tables", e))?;
|
||||
writer
|
||||
.write_zeros_exact(metadata_padding as u64)
|
||||
.map_err(|e| Error::DataWrite("metadata_padding", e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageType::Empty => {
|
||||
writer.write_all(geometry)?;
|
||||
writer.write_zeros_exact(geometry_padding as u64)?;
|
||||
writer
|
||||
.write_all(geometry)
|
||||
.map_err(|e| Error::DataWrite("geometry", e))?;
|
||||
writer
|
||||
.write_zeros_exact(geometry_padding as u64)
|
||||
.map_err(|e| Error::DataWrite("geometry_padding", e))?;
|
||||
|
||||
writer.write_all(self.slots[0].header.as_bytes())?;
|
||||
writer.write_all(self.slots[0].partitions.as_bytes())?;
|
||||
writer.write_all(self.slots[0].extents.as_bytes())?;
|
||||
writer.write_all(self.slots[0].groups.as_bytes())?;
|
||||
writer.write_all(self.slots[0].block_devices.as_bytes())?;
|
||||
writer
|
||||
.write_all(self.slots[0].header.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
writer
|
||||
.write_all(self.slots[0].partitions.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("partition_tables", e))?;
|
||||
writer
|
||||
.write_all(self.slots[0].extents.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("extent_tables", e))?;
|
||||
writer
|
||||
.write_all(self.slots[0].groups.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("group_tables", e))?;
|
||||
writer
|
||||
.write_all(self.slots[0].block_devices.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("block_device_tables", e))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1261,7 +1393,9 @@ impl Partition {
|
||||
self.extents
|
||||
.iter()
|
||||
.try_fold(0u64, |total, e| total.checked_add(e.num_sectors))
|
||||
.ok_or_else(|| Error::Partition(self.name.clone(), "Sector count overflow".into()))
|
||||
.ok_or_else(|| Error::PartitionSectorCountTooLarge {
|
||||
name: DebugString::new(&self.name),
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute the number of bytes covered by the extents.
|
||||
@@ -1269,7 +1403,9 @@ impl Partition {
|
||||
self.num_sectors()
|
||||
.ok()
|
||||
.and_then(|n| n.checked_mul(SECTOR_SIZE.into()))
|
||||
.ok_or_else(|| Error::Partition(self.name.clone(), "Byte count overflow".into()))
|
||||
.ok_or_else(|| Error::PartitionByteCountTooLarge {
|
||||
name: DebugString::new(&self.name),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1620,16 +1756,24 @@ impl TryFrom<&MetadataSlot> for RawMetadataSlot {
|
||||
};
|
||||
|
||||
let group_index: u32 =
|
||||
raw_slot.groups.len().try_into().map_err(|_| {
|
||||
Error::PartitionGroup(group.name.clone(), "Index too large".into())
|
||||
})?;
|
||||
raw_slot
|
||||
.groups
|
||||
.len()
|
||||
.try_into()
|
||||
.map_err(|_| Error::GroupIndexTooLarge {
|
||||
name: DebugString::new(&group.name),
|
||||
})?;
|
||||
|
||||
for partition in &group.partitions {
|
||||
let extent_index: u32 = raw_slot.extents.len().try_into().map_err(|_| {
|
||||
Error::Partition(partition.name.clone(), "Extent index too large".into())
|
||||
Error::PartitionExtentIndexTooLarge {
|
||||
name: DebugString::new(&partition.name),
|
||||
}
|
||||
})?;
|
||||
let num_extents: u32 = partition.extents.len().try_into().map_err(|_| {
|
||||
Error::Partition(partition.name.clone(), "Too many extents".into())
|
||||
Error::PartitionExtentCountTooLarge {
|
||||
name: DebugString::new(&partition.name),
|
||||
}
|
||||
})?;
|
||||
|
||||
let raw_partition = RawPartition {
|
||||
@@ -1648,10 +1792,9 @@ impl TryFrom<&MetadataSlot> for RawMetadataSlot {
|
||||
} => {
|
||||
let block_device_index: u32 =
|
||||
block_device_index.try_into().map_err(|_| {
|
||||
Error::Extent(
|
||||
raw_slot.extents.len(),
|
||||
"Block device index too large".into(),
|
||||
)
|
||||
Error::ExtentDeviceIndexTooLarge {
|
||||
index: raw_slot.extents.len(),
|
||||
}
|
||||
})?;
|
||||
|
||||
(
|
||||
|
||||
+200
-106
@@ -6,6 +6,7 @@ use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
iter,
|
||||
path::Path,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@@ -23,7 +24,7 @@ use crate::{
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
format::payload::{self, PayloadHeader},
|
||||
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter, ReadFixedSizeExt},
|
||||
};
|
||||
|
||||
pub const PATH_METADATA: &str = "META-INF/com/android/metadata";
|
||||
@@ -49,10 +50,12 @@ pub enum Error {
|
||||
OtaMagicNotFound,
|
||||
#[error("Cannot find EOCD magic")]
|
||||
EocdMagicNotFound,
|
||||
#[error("EOCD magic found in archive comment")]
|
||||
EocdMagicInComment,
|
||||
#[error("EOCD magic found in archive comment at offset {0}")]
|
||||
EocdMagicInComment(usize),
|
||||
#[error("Zip is too small to contain EOCD")]
|
||||
ZipTooSmall,
|
||||
#[error("Zip archive comment is not empty: {0}")]
|
||||
ZipNonEmptyComment(u16),
|
||||
#[error("Signature offset exceeds archive comment size")]
|
||||
SignatureOffsetTooLarge,
|
||||
#[error("Expected exactly one CMS embedded certificate, but found {0}")]
|
||||
@@ -69,32 +72,50 @@ pub enum Error {
|
||||
UnsupportedLegacyMetadataField { key: String, value: String },
|
||||
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
|
||||
MismatchedPropertyFiles { expected: String, actual: String },
|
||||
#[error("Property files {0:?} exceed {1} byte reserved space")]
|
||||
InsufficientReservedSpace(String, usize),
|
||||
#[error("Property files {value:?} exceed {reserved} byte reserved space")]
|
||||
InsufficientReservedSpace { value: String, reserved: usize },
|
||||
#[error("Invalid property file entry: {0:?}")]
|
||||
InvalidPropertyFileEntry(String),
|
||||
#[error("Missing entry in OTA zip: {0}")]
|
||||
#[error("Missing entry in OTA zip: {0:?}")]
|
||||
MissingZipEntry(&'static str),
|
||||
#[error("CMS signing error")]
|
||||
CmsSign(#[from] crypto::Error),
|
||||
#[error("Payload error")]
|
||||
Payload(#[from] payload::Error),
|
||||
#[error("Failed to decode protobuf message")]
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("SPKI error")]
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Failed to decode OTA metadata protobuf message")]
|
||||
MetadataDecode(#[source] prost::DecodeError),
|
||||
#[error("Failed to open zip file")]
|
||||
ZipOpen(#[source] ZipError),
|
||||
#[error("Failed to open zip entry: {0:?}")]
|
||||
ZipEntryOpen(&'static str, #[source] ZipError),
|
||||
#[error("Failed to start new zip entry: {0:?}")]
|
||||
ZipEntryStart(&'static str, #[source] ZipError),
|
||||
#[error("Failed to read zip entry: {0:?}")]
|
||||
ZipEntryRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write zip entry: {0:?}")]
|
||||
ZipEntryWrite(&'static str, #[source] io::Error),
|
||||
#[error("Failed to open zip entry #{0}")]
|
||||
ZipIndexOpen(usize, #[source] ZipError),
|
||||
#[error("Failed to load OTA certificate")]
|
||||
OtaCertLoad(#[source] crypto::Error),
|
||||
#[error("Failed to extract public key from OTA certificate")]
|
||||
OtaCertExtractPubKey(#[source] crypto::Error),
|
||||
#[error("Failed to load payload binary")]
|
||||
PayloadLoad(#[source] payload::Error),
|
||||
#[error("Failed to load CMS signature")]
|
||||
CmsLoad(#[source] crypto::Error),
|
||||
#[error("Failed to save CMS signature")]
|
||||
CmsSave(#[source] x509_cert::der::Error),
|
||||
#[error("Failed to generate CMS signature")]
|
||||
CmsSign(#[source] crypto::Error),
|
||||
#[error("Failed to verify CMS signature")]
|
||||
CmsVerify(#[source] crypto::Error),
|
||||
#[error("Failed to read OTA data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write OTA data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub fn parse_protobuf_metadata(data: &[u8]) -> Result<OtaMetadata> {
|
||||
Ok(OtaMetadata::decode(data)?)
|
||||
OtaMetadata::decode(data).map_err(Error::MetadataDecode)
|
||||
}
|
||||
|
||||
/// Synthesize protobuf structure from legacy plain-text metadata.
|
||||
@@ -135,7 +156,7 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
|
||||
}
|
||||
"ota-wipe" => metadata.wipe = parse_yes()?,
|
||||
"ota-retrofit-dynamic-partitions" => {
|
||||
metadata.retrofit_dynamic_partitions = parse_yes()?
|
||||
metadata.retrofit_dynamic_partitions = parse_yes()?;
|
||||
}
|
||||
"ota-downgrade" => metadata.downgrade = parse_yes()?,
|
||||
"ota-required-cache" => {
|
||||
@@ -190,7 +211,7 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
|
||||
|
||||
/// Generate the legacy plain-text and modern protobuf serializations of the
|
||||
/// given metadata instance.
|
||||
fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
fn serialize_metadata(metadata: &OtaMetadata) -> (String, Vec<u8>) {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut pairs = BTreeMap::<String, String>::new();
|
||||
@@ -254,7 +275,7 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
});
|
||||
let modern_metadata = metadata.encode_to_vec();
|
||||
|
||||
Ok((legacy_metadata, modern_metadata))
|
||||
(legacy_metadata, modern_metadata)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -300,12 +321,13 @@ fn compute_property_files(
|
||||
pf_name: &str,
|
||||
entries: &[ZipEntry],
|
||||
max_length: Option<usize>,
|
||||
want_pb: bool,
|
||||
) -> Result<String> {
|
||||
let compute = |path: &'static str| -> Result<String> {
|
||||
let entry = entries
|
||||
.iter()
|
||||
.find(|e| e.name == path)
|
||||
.ok_or_else(|| Error::MissingZipEntry(path))?;
|
||||
.ok_or(Error::MissingZipEntry(path))?;
|
||||
let name = path.rsplit_once('/').map_or(path, |p| p.1);
|
||||
|
||||
Ok(format!("{name}:{}:{}", entry.offset, entry.size))
|
||||
@@ -334,17 +356,24 @@ fn compute_property_files(
|
||||
|
||||
if max_length.is_none() {
|
||||
tokens.push(format!("metadata:{}", " ".repeat(15)));
|
||||
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
|
||||
if want_pb {
|
||||
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
|
||||
}
|
||||
} else {
|
||||
tokens.push(compute(PATH_METADATA)?);
|
||||
tokens.push(compute(PATH_METADATA_PB)?);
|
||||
if want_pb {
|
||||
tokens.push(compute(PATH_METADATA_PB)?);
|
||||
}
|
||||
}
|
||||
|
||||
let mut joined = tokens.join(",");
|
||||
|
||||
if let Some(l) = max_length {
|
||||
if joined.len() > l {
|
||||
return Err(Error::InsufficientReservedSpace(joined, l));
|
||||
return Err(Error::InsufficientReservedSpace {
|
||||
value: joined,
|
||||
reserved: l,
|
||||
});
|
||||
}
|
||||
|
||||
let remain = l - joined.len();
|
||||
@@ -363,7 +392,7 @@ fn add_payload_metadata_entry(
|
||||
let payload_offset = entries
|
||||
.iter()
|
||||
.find(|e| e.name == PATH_PAYLOAD)
|
||||
.ok_or_else(|| Error::MissingZipEntry(PATH_PAYLOAD))?
|
||||
.ok_or(Error::MissingZipEntry(PATH_PAYLOAD))?
|
||||
.offset;
|
||||
entries.push(ZipEntry {
|
||||
name: NAME_PAYLOAD_METADATA.to_owned(),
|
||||
@@ -415,26 +444,38 @@ pub fn add_metadata(
|
||||
for pf in [PF_NAME, PF_STREAMING_NAME] {
|
||||
metadata.property_files.insert(
|
||||
pf.to_owned(),
|
||||
compute_property_files(pf, &zip_entries, None)?,
|
||||
compute_property_files(pf, &zip_entries, None, true)?,
|
||||
);
|
||||
}
|
||||
|
||||
// Add the placeholders to a temporary zip to compute final property files.
|
||||
let (temp_legacy_offset, temp_modern_offset) = {
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = match zip_mode {
|
||||
ZipMode::Streaming => ZipWriter::new_streaming(raw_writer),
|
||||
ZipMode::Seekable => ZipWriter::new(raw_writer),
|
||||
};
|
||||
|
||||
writer.start_file_with_extra_data(PATH_METADATA, options)?;
|
||||
let legacy_offset = writer.end_extra_data()?;
|
||||
writer.write_all(legacy_raw.as_bytes())?;
|
||||
writer
|
||||
.start_file_with_extra_data(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
let legacy_offset = writer
|
||||
.end_extra_data()
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
writer
|
||||
.write_all(legacy_raw.as_bytes())
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
|
||||
|
||||
writer.start_file_with_extra_data(PATH_METADATA_PB, options)?;
|
||||
let modern_offset = writer.end_extra_data()?;
|
||||
writer.write_all(&modern_raw)?;
|
||||
writer
|
||||
.start_file_with_extra_data(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
let modern_offset = writer
|
||||
.end_extra_data()
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
writer
|
||||
.write_all(&modern_raw)
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
|
||||
|
||||
zip_entries.push(ZipEntry {
|
||||
name: PATH_METADATA.to_owned(),
|
||||
@@ -452,20 +493,32 @@ pub fn add_metadata(
|
||||
|
||||
// Compute the final property files using the offsets of the fake entries.
|
||||
for (key, value) in &mut metadata.property_files {
|
||||
*value = compute_property_files(key, &zip_entries, Some(value.len()))?;
|
||||
*value = compute_property_files(key, &zip_entries, Some(value.len()), true)?;
|
||||
}
|
||||
|
||||
// Add the final metadata files to the real zip.
|
||||
{
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
|
||||
|
||||
zip_writer.start_file_with_extra_data(PATH_METADATA, options)?;
|
||||
let legacy_offset = zip_writer.end_extra_data()?;
|
||||
zip_writer.write_all(legacy_raw.as_bytes())?;
|
||||
zip_writer
|
||||
.start_file_with_extra_data(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
let legacy_offset = zip_writer
|
||||
.end_extra_data()
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
zip_writer
|
||||
.write_all(legacy_raw.as_bytes())
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
|
||||
|
||||
zip_writer.start_file_with_extra_data(PATH_METADATA_PB, options)?;
|
||||
let modern_offset = zip_writer.end_extra_data()?;
|
||||
zip_writer.write_all(&modern_raw)?;
|
||||
zip_writer
|
||||
.start_file_with_extra_data(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
let modern_offset = zip_writer
|
||||
.end_extra_data()
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
zip_writer
|
||||
.write_all(&modern_raw)
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
|
||||
|
||||
assert_eq!(legacy_offset, temp_legacy_offset);
|
||||
assert_eq!(modern_offset, temp_modern_offset);
|
||||
@@ -480,11 +533,13 @@ pub fn verify_metadata(
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<()> {
|
||||
let mut zip_reader = ZipArchive::new(reader)?;
|
||||
let mut zip_reader = ZipArchive::new(reader).map_err(Error::ZipOpen)?;
|
||||
let mut zip_entries = vec![];
|
||||
|
||||
for i in 0..zip_reader.len() {
|
||||
let entry = zip_reader.by_index(i)?;
|
||||
let entry = zip_reader
|
||||
.by_index(i)
|
||||
.map_err(|e| Error::ZipIndexOpen(i, e))?;
|
||||
zip_entries.push(ZipEntry {
|
||||
name: entry.name().to_owned(),
|
||||
offset: entry.data_start(),
|
||||
@@ -494,8 +549,11 @@ pub fn verify_metadata(
|
||||
|
||||
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
|
||||
|
||||
let metadata_pb = zip_entries.iter().find(|e| e.name == PATH_METADATA_PB);
|
||||
|
||||
for (key, value) in &metadata.property_files {
|
||||
let new_value = compute_property_files(key, &zip_entries, Some(value.len()))?;
|
||||
let new_value =
|
||||
compute_property_files(key, &zip_entries, Some(value.len()), metadata_pb.is_some())?;
|
||||
if *value != new_value {
|
||||
return Err(Error::MismatchedPropertyFiles {
|
||||
expected: value.clone(),
|
||||
@@ -512,11 +570,16 @@ pub fn verify_metadata(
|
||||
/// that's covered by the signature. This does not perform any parsing of zip
|
||||
/// data structures.
|
||||
fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
let file_size = reader.seek(SeekFrom::End(0))?;
|
||||
let file_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.map_err(|e| Error::DataRead("file_size", e))?;
|
||||
|
||||
reader.seek(SeekFrom::Current(-6))?;
|
||||
let mut footer = [0u8; 6];
|
||||
reader.read_exact(&mut footer)?;
|
||||
reader
|
||||
.seek(SeekFrom::Current(-6))
|
||||
.map_err(|e| Error::DataRead("footer", e))?;
|
||||
let footer = reader
|
||||
.read_array_exact::<6>()
|
||||
.map_err(|e| Error::DataRead("footer", e))?;
|
||||
|
||||
let abs_eoc_offset = u16::from_le_bytes(footer[0..2].try_into().unwrap());
|
||||
let sig_magic = u16::from_le_bytes(footer[2..4].try_into().unwrap());
|
||||
@@ -535,20 +598,24 @@ fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
return Err(Error::SignatureOffsetTooLarge);
|
||||
}
|
||||
|
||||
reader.seek(SeekFrom::Start(file_size - eocd_size))?;
|
||||
let mut eocd = vec![0u8; eocd_size as usize];
|
||||
reader.read_exact(&mut eocd)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(file_size - eocd_size))
|
||||
.map_err(|e| Error::DataRead("eocd", e))?;
|
||||
let eocd = reader
|
||||
.read_vec_exact(eocd_size as usize)
|
||||
.map_err(|e| Error::DataRead("eocd", e))?;
|
||||
|
||||
let mut eocd_magic_iter = memmem::find_iter(&eocd, ZIP_EOCD_MAGIC);
|
||||
if eocd_magic_iter.next() != Some(0) {
|
||||
return Err(Error::EocdMagicNotFound);
|
||||
}
|
||||
if eocd_magic_iter.next().is_some() {
|
||||
return Err(Error::EocdMagicInComment);
|
||||
if let Some(offset) = eocd_magic_iter.next() {
|
||||
return Err(Error::EocdMagicInComment(offset));
|
||||
}
|
||||
|
||||
let sig_offset = eocd_size as usize - usize::from(abs_eoc_offset);
|
||||
let sd = crypto::parse_cms(&eocd[sig_offset..eocd_size as usize - 6])?;
|
||||
let sd =
|
||||
crypto::parse_cms(&eocd[sig_offset..eocd_size as usize - 6]).map_err(Error::CmsLoad)?;
|
||||
// The signature covers everything aside from the archive comment and its
|
||||
// length field.
|
||||
let hashed_size = file_size - 2 - u64::from(comment_size);
|
||||
@@ -574,7 +641,7 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
|
||||
}
|
||||
|
||||
let cert = &certs[0];
|
||||
let public_key = crypto::get_public_key(cert)?;
|
||||
let public_key = crypto::get_public_key(cert).map_err(Error::OtaCertExtractPubKey)?;
|
||||
|
||||
// Make sure this is a signature scheme we can handle. There's currently no
|
||||
// Rust library to verify arbitrary CMS signatures for large files without
|
||||
@@ -595,7 +662,9 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
|
||||
}
|
||||
|
||||
// Manually hash the parts of the file covered by the signature.
|
||||
reader.seek(SeekFrom::Start(0))?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(0))
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
// We support SHA1 for verification only.
|
||||
let (algorithm, algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
@@ -609,13 +678,16 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
|
||||
|
||||
let mut hashing_reader = HashingReader::new(reader, Context::new(algorithm));
|
||||
|
||||
stream::copy_n(&mut hashing_reader, io::sink(), hashed_size, cancel_signal)?;
|
||||
stream::copy_n(&mut hashing_reader, io::sink(), hashed_size, cancel_signal)
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
let (_, context) = hashing_reader.finish();
|
||||
let digest = context.finish();
|
||||
|
||||
// Verify the signature against the public key.
|
||||
public_key.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())?;
|
||||
public_key
|
||||
.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())
|
||||
.map_err(Error::CmsVerify)?;
|
||||
|
||||
Ok(cert.clone())
|
||||
}
|
||||
@@ -625,29 +697,52 @@ pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> R
|
||||
pub fn parse_zip_ota_info(
|
||||
reader: impl Read + Seek,
|
||||
) -> Result<(OtaMetadata, Certificate, PayloadHeader, String)> {
|
||||
let mut zip = ZipArchive::new(reader)?;
|
||||
let mut zip = ZipArchive::new(reader).map_err(Error::ZipOpen)?;
|
||||
|
||||
let metadata = {
|
||||
let mut entry = zip.by_name(PATH_METADATA_PB)?;
|
||||
let mut buf = Vec::new();
|
||||
entry.read_to_end(&mut buf)?;
|
||||
OtaMetadata::decode(buf.as_slice())?
|
||||
let metadata = match zip.by_name(PATH_METADATA_PB) {
|
||||
Ok(mut entry) => {
|
||||
let mut buf = Vec::new();
|
||||
entry
|
||||
.read_to_end(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_METADATA_PB, e))?;
|
||||
parse_protobuf_metadata(&buf)?
|
||||
}
|
||||
e @ Err(ZipError::FileNotFound) => {
|
||||
drop(e);
|
||||
let mut entry = zip
|
||||
.by_name(PATH_METADATA)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_METADATA, e))?;
|
||||
let mut buf = String::new();
|
||||
entry
|
||||
.read_to_string(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_METADATA, e))?;
|
||||
parse_legacy_metadata(&buf)?
|
||||
}
|
||||
Err(e) => return Err(Error::ZipEntryOpen(PATH_METADATA_PB, e)),
|
||||
};
|
||||
|
||||
let certificate = {
|
||||
let entry = zip.by_name(PATH_OTACERT)?;
|
||||
crypto::read_pem_cert(entry)?
|
||||
let entry = zip
|
||||
.by_name(PATH_OTACERT)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_OTACERT, e))?;
|
||||
crypto::read_pem_cert(Path::new(PATH_OTACERT), entry).map_err(Error::OtaCertLoad)?
|
||||
};
|
||||
|
||||
let header = {
|
||||
let entry = zip.by_name(PATH_PAYLOAD)?;
|
||||
PayloadHeader::from_reader(entry)?
|
||||
let entry = zip
|
||||
.by_name(PATH_PAYLOAD)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_PAYLOAD, e))?;
|
||||
PayloadHeader::from_reader(entry).map_err(Error::PayloadLoad)?
|
||||
};
|
||||
|
||||
let properties = {
|
||||
let mut entry = zip.by_name(PATH_PROPERTIES)?;
|
||||
let mut entry = zip
|
||||
.by_name(PATH_PROPERTIES)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_PROPERTIES, e))?;
|
||||
let mut buf = String::new();
|
||||
entry.read_to_string(&mut buf)?;
|
||||
entry
|
||||
.read_to_string(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_PROPERTIES, e))?;
|
||||
buf
|
||||
};
|
||||
|
||||
@@ -655,17 +750,12 @@ pub fn parse_zip_ota_info(
|
||||
}
|
||||
|
||||
/// Ensure that we're using a non-zip64 EOCD and there's no archive comment.
|
||||
fn validate_eocd(eocd: &[u8]) -> io::Result<()> {
|
||||
fn validate_eocd(eocd: &[u8]) -> Result<()> {
|
||||
if &eocd[..4] != b"PK\x05\x06" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"EOCD magic not found",
|
||||
));
|
||||
return Err(Error::EocdMagicNotFound);
|
||||
} else if &eocd[20..22] != b"\0\0" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Archive comment is not 0 bytes",
|
||||
));
|
||||
let size = u16::from_le_bytes(eocd[20..22].try_into().unwrap());
|
||||
return Err(Error::ZipNonEmptyComment(size));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -680,8 +770,9 @@ fn compute_signature_comment(
|
||||
cert: &Certificate,
|
||||
digest: ring::digest::Digest,
|
||||
) -> Result<Vec<u8>> {
|
||||
let cms_signature = crypto::cms_sign_external(key, cert, digest.as_ref())?;
|
||||
let cms_signature_der = cms_signature.to_der()?;
|
||||
let cms_signature =
|
||||
crypto::cms_sign_external(key, cert, digest.as_ref()).map_err(Error::CmsSign)?;
|
||||
let cms_signature_der = cms_signature.to_der().map_err(Error::CmsSave)?;
|
||||
|
||||
// Includes placeholder for the EOCD comment size field.
|
||||
let mut buf = vec![0; 2];
|
||||
@@ -703,12 +794,8 @@ fn compute_signature_comment(
|
||||
// Archive comment size (for use by the OTA signature verifier).
|
||||
buf.extend(((comment_size) as u16).to_le_bytes());
|
||||
|
||||
if let Some(o) = memmem::find(&buf[2..], ZIP_EOCD_MAGIC) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Archive comment contains EOCD magic at offset {o}"),
|
||||
)
|
||||
.into());
|
||||
if let Some(offset) = memmem::find(&buf[2..], ZIP_EOCD_MAGIC) {
|
||||
return Err(Error::EocdMagicInComment(offset));
|
||||
}
|
||||
|
||||
// Archive comment size (for the EOCD comment size field).
|
||||
@@ -741,21 +828,23 @@ impl<W: Write> StreamingSigningWriter<W> {
|
||||
|
||||
pub fn finish(mut self, key: &RsaSigningKey, cert: &Certificate) -> Result<W> {
|
||||
if self.used < self.queue.len() {
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
|
||||
);
|
||||
return Err(Error::ZipTooSmall);
|
||||
}
|
||||
|
||||
validate_eocd(&self.queue)?;
|
||||
|
||||
// Chop off the archive comment size field and write the remaining data.
|
||||
self.inner.write_all(&self.queue[..20])?;
|
||||
self.inner
|
||||
.write_all(&self.queue[..20])
|
||||
.map_err(|e| Error::DataWrite("eocd_minus_comment", e))?;
|
||||
|
||||
let (mut raw_writer, context) = self.inner.finish();
|
||||
let digest = context.finish();
|
||||
|
||||
let size_and_comment = compute_signature_comment(key, cert, digest)?;
|
||||
raw_writer.write_all(&size_and_comment)?;
|
||||
raw_writer
|
||||
.write_all(&size_and_comment)
|
||||
.map_err(|e| Error::DataWrite("size_and_comment", e))?;
|
||||
|
||||
Ok(raw_writer)
|
||||
}
|
||||
@@ -815,18 +904,20 @@ impl<W: Read + Write + Seek> SeekableSigningWriter<W> {
|
||||
cert: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<W> {
|
||||
let file_size = self.seek(SeekFrom::End(0))?;
|
||||
let file_size = self
|
||||
.seek(SeekFrom::End(0))
|
||||
.map_err(|e| Error::DataRead("file_size", e))?;
|
||||
|
||||
// Android only supports non-zip64 EOCD.
|
||||
if file_size < 22 {
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
|
||||
);
|
||||
return Err(Error::ZipTooSmall);
|
||||
}
|
||||
|
||||
self.seek_relative(-22)?;
|
||||
let mut eocd = [0u8; 22];
|
||||
self.read_exact(&mut eocd)?;
|
||||
self.seek_relative(-22)
|
||||
.map_err(|e| Error::DataRead("eocd", e))?;
|
||||
let eocd = self
|
||||
.read_array_exact::<22>()
|
||||
.map_err(|e| Error::DataRead("eocd", e))?;
|
||||
|
||||
validate_eocd(&eocd)?;
|
||||
|
||||
@@ -836,13 +927,16 @@ impl<W: Read + Write + Seek> SeekableSigningWriter<W> {
|
||||
ring::digest::Context::new(&ring::digest::SHA256),
|
||||
);
|
||||
|
||||
self.rewind()?;
|
||||
stream::copy_n(&mut self, &mut hashing_writer, file_size - 2, cancel_signal)?;
|
||||
self.rewind().map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
stream::copy_n(&mut self, &mut hashing_writer, file_size - 2, cancel_signal)
|
||||
.map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
let digest = hashing_writer.finish().1.finish();
|
||||
|
||||
let size_and_comment = compute_signature_comment(key, cert, digest)?;
|
||||
self.inner.write_all(&size_and_comment)?;
|
||||
self.inner
|
||||
.write_all(&size_and_comment)
|
||||
.map_err(|e| Error::DataWrite("size_and_comment", e))?;
|
||||
|
||||
Ok(self.inner)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
@@ -45,3 +45,35 @@ pub fn write_zeros(mut writer: impl Write + Seek, page_size: u64) -> io::Result<
|
||||
|
||||
Ok(padding)
|
||||
}
|
||||
|
||||
pub trait ZeroPadding {
|
||||
/// Trim trailing zeros. Intermediate zeros before the last non-zero byte
|
||||
/// are kept.
|
||||
fn trim_end_padding(&self) -> &[u8];
|
||||
|
||||
/// Return the slice as an array padded with zeros at the end.
|
||||
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]>;
|
||||
}
|
||||
|
||||
impl ZeroPadding for [u8] {
|
||||
fn trim_end_padding(&self) -> &[u8] {
|
||||
let first_ending_zero = self
|
||||
.iter()
|
||||
.rposition(|b| *b != 0)
|
||||
.map(|pos| pos + 1)
|
||||
.unwrap_or_default();
|
||||
|
||||
&self[..first_ending_zero]
|
||||
}
|
||||
|
||||
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]> {
|
||||
if self.len() > N {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut result = [0u8; N];
|
||||
result[..self.len()].copy_from_slice(self);
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
+204
-145
@@ -11,14 +11,12 @@ use std::{
|
||||
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use bzip2::write::BzDecoder;
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
write::XzDecoder,
|
||||
write::XzEncoder,
|
||||
};
|
||||
use num_traits::ToPrimitive;
|
||||
use prost::Message;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, IntoParallelRefMutIterator},
|
||||
@@ -28,6 +26,8 @@ use ring::digest::{Context, Digest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use x509_cert::Certificate;
|
||||
use zerocopy::{big_endian, FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
@@ -36,14 +36,14 @@ use crate::{
|
||||
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadSeekReopen, WriteSeek,
|
||||
WriteSeekReopen,
|
||||
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadFixedSizeExt,
|
||||
ReadSeekReopen, WriteSeek, WriteSeekReopen,
|
||||
},
|
||||
util,
|
||||
util::{self, OutOfBoundsError},
|
||||
};
|
||||
|
||||
const OTA_MAGIC: &[u8; 4] = b"CrAU";
|
||||
const OTA_HEADER_SIZE: usize = OTA_MAGIC.len() + 8 + 8 + 4;
|
||||
const PAYLOAD_MAGIC: &[u8; 4] = b"CrAU";
|
||||
const PAYLOAD_VERSION: u64 = 2;
|
||||
|
||||
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
|
||||
|
||||
@@ -91,19 +91,62 @@ pub enum Error {
|
||||
#[error("{0:?} field is missing")]
|
||||
MissingField(&'static str),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("Failed to decode protobuf message")]
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to decode payload manifest protobuf message")]
|
||||
ManifestDecode(#[source] prost::DecodeError),
|
||||
#[error("Failed to decode payload signatures protobuf message")]
|
||||
SignaturesDecode(#[source] prost::DecodeError),
|
||||
#[error("Failed to generate payload signature")]
|
||||
SignatureGenerate(#[source] crypto::Error),
|
||||
#[error("Failed to verify payload signature")]
|
||||
SignatureVerify(#[source] crypto::Error),
|
||||
#[error("Failed to read payload data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write payload data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
#[error("Expected {expected} bytes, but only wrote {actual} bytes")]
|
||||
UnwrittenData { actual: u64, expected: u64 },
|
||||
#[error("I/O error when applying {op_type:?} operation for {num_blocks} blocks starting at {start_block}")]
|
||||
OperationApply {
|
||||
op_type: Type,
|
||||
start_block: u64,
|
||||
num_blocks: u64,
|
||||
source: io::Error,
|
||||
},
|
||||
#[error("Failed to reopen payload")]
|
||||
PayloadReopen(#[source] io::Error),
|
||||
#[error("Failed to open input file for partition: {0}")]
|
||||
InputOpen(String, #[source] io::Error),
|
||||
#[error("Failed to open output file for partition: {0}")]
|
||||
OutputOpen(String, #[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
#[error("Failed to XZ compress partition image chunk")]
|
||||
XzCompress(#[source] io::Error),
|
||||
#[error("Failed to read uncompressed input partition image chunk")]
|
||||
ChunkRead(#[source] io::Error),
|
||||
#[error("Failed to write XZ-compressed output partition image chunk")]
|
||||
ChunkWrite(#[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Raw on-disk layout for the payload header.
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`PAYLOAD_MAGIC`].
|
||||
magic: [u8; 4],
|
||||
/// Image version. This should be equal to [`PAYLOAD_VERSION`].
|
||||
file_format_version: big_endian::U64,
|
||||
/// Size of the [`DeltaArchiveManifest`] blob.
|
||||
manifest_size: big_endian::U64,
|
||||
/// Size of the [`Signatures`] blob.
|
||||
metadata_signature_size: big_endian::U32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PayloadHeader {
|
||||
pub version: u64,
|
||||
@@ -131,42 +174,39 @@ impl<R: Read> FromReader<R> for PayloadHeader {
|
||||
fn from_reader(reader: R) -> Result<Self> {
|
||||
let mut reader = CountingReader::new(reader);
|
||||
|
||||
let mut magic = [0u8; 4];
|
||||
reader.read_exact(&mut magic)?;
|
||||
if magic != *OTA_MAGIC {
|
||||
return Err(Error::UnknownMagic(magic));
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
if header.magic != *PAYLOAD_MAGIC {
|
||||
return Err(Error::UnknownMagic(header.magic));
|
||||
}
|
||||
|
||||
let version = reader.read_u64::<BigEndian>()?;
|
||||
if version != 2 {
|
||||
return Err(Error::UnsupportedVersion(version));
|
||||
if header.file_format_version != PAYLOAD_VERSION {
|
||||
return Err(Error::UnsupportedVersion(header.file_format_version.get()));
|
||||
}
|
||||
|
||||
let manifest_size = reader
|
||||
.read_u64::<BigEndian>()?
|
||||
.to_usize()
|
||||
.and_then(|s| {
|
||||
if s <= MANIFEST_MAX_SIZE {
|
||||
Some(s)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("manifest_size"))?;
|
||||
let metadata_signature_size = reader.read_u32::<BigEndian>()?;
|
||||
let manifest_size: usize = util::try_cast(header.manifest_size.get())
|
||||
.and_then(|s| util::check_bounds(s, ..=MANIFEST_MAX_SIZE))
|
||||
.map_err(|e| Error::IntOutOfBounds("manifest_size", e))?;
|
||||
|
||||
let mut manifest_raw = vec![0u8; manifest_size];
|
||||
reader.read_exact(&mut manifest_raw)?;
|
||||
let manifest = DeltaArchiveManifest::decode(manifest_raw.as_slice())?;
|
||||
let manifest_raw = reader
|
||||
.read_vec_exact(manifest_size)
|
||||
.map_err(|e| Error::DataRead("manifest", e))?;
|
||||
let manifest =
|
||||
DeltaArchiveManifest::decode(manifest_raw.as_slice()).map_err(Error::ManifestDecode)?;
|
||||
|
||||
// Skip manifest signatures.
|
||||
reader.read_discard_exact(metadata_signature_size.into())?;
|
||||
reader
|
||||
.read_discard_exact(header.metadata_signature_size.into())
|
||||
.map_err(|e| Error::DataRead("metadata_signatures", e))?;
|
||||
|
||||
Ok(Self {
|
||||
version,
|
||||
version: header.file_format_version.get(),
|
||||
manifest,
|
||||
metadata_signature_size,
|
||||
blob_offset: reader.stream_position()?,
|
||||
metadata_signature_size: header.metadata_signature_size.get(),
|
||||
blob_offset: reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("blob_offset", e))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -174,7 +214,9 @@ impl<R: Read> FromReader<R> for PayloadHeader {
|
||||
/// Sign `digest` with `key` and return a [`Signatures`] protobuf struct with
|
||||
/// the signature padded to the maximum size.
|
||||
fn sign_digest(digest: &[u8], key: &RsaSigningKey) -> Result<Signatures> {
|
||||
let mut digest_signed = key.sign(SignatureAlgorithm::Sha256WithRsa, digest)?;
|
||||
let mut digest_signed = key
|
||||
.sign(SignatureAlgorithm::Sha256WithRsa, digest)
|
||||
.map_err(Error::SignatureGenerate)?;
|
||||
assert!(
|
||||
digest_signed.len() <= key.size(),
|
||||
"Signature exceeds maximum size",
|
||||
@@ -199,25 +241,25 @@ fn sign_digest(digest: &[u8], key: &RsaSigningKey) -> Result<Signatures> {
|
||||
|
||||
/// Verify `digest` inside `signatures` using `cert`.
|
||||
fn verify_digest(digest: &[u8], signatures: &Signatures, cert: &Certificate) -> Result<()> {
|
||||
let public_key = crypto::get_public_key(cert)?;
|
||||
let public_key = crypto::get_public_key(cert).map_err(Error::SignatureVerify)?;
|
||||
let mut last_error = None;
|
||||
|
||||
for signature in &signatures.signatures {
|
||||
let Some(data) = &signature.data else {
|
||||
continue;
|
||||
};
|
||||
let Some(size) = signature.unpadded_signature_size else {
|
||||
continue;
|
||||
};
|
||||
let without_padding = &data[..size as usize];
|
||||
let size = signature
|
||||
.unpadded_signature_size
|
||||
.map_or(data.len(), |s| s as usize);
|
||||
let without_padding = &data[..size];
|
||||
|
||||
match public_key.verify_sig(SignatureAlgorithm::Sha256WithRsa, digest, without_padding) {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(e) => last_error = Some(e),
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => last_error = Some(Error::SignatureVerify(e)),
|
||||
}
|
||||
}
|
||||
|
||||
Err(last_error.map_or(Error::NoSignatures, |e| e.into()))
|
||||
Err(last_error.unwrap_or(Error::NoSignatures))
|
||||
}
|
||||
|
||||
fn parse_properties(data: &str) -> Result<HashMap<String, String>> {
|
||||
@@ -347,34 +389,32 @@ impl<W: Write> PayloadWriter<W> {
|
||||
let mut h_full = Context::new(&ring::digest::SHA256);
|
||||
|
||||
// Write header to output file.
|
||||
write_hash!(inner, [h_partial, h_full], OTA_MAGIC)?;
|
||||
write_hash!(inner, [h_partial, h_full], &header.version.to_be_bytes())?;
|
||||
write_hash!(
|
||||
inner,
|
||||
[h_partial, h_full],
|
||||
&(manifest_raw_new.len() as u64).to_be_bytes(),
|
||||
)?;
|
||||
write_hash!(
|
||||
inner,
|
||||
[h_partial, h_full],
|
||||
&(dummy_sig_size as u32).to_be_bytes()
|
||||
)?;
|
||||
let raw_header = RawHeader {
|
||||
magic: *PAYLOAD_MAGIC,
|
||||
file_format_version: header.version.into(),
|
||||
manifest_size: (manifest_raw_new.len() as u64).into(),
|
||||
metadata_signature_size: (dummy_sig_size as u32).into(),
|
||||
};
|
||||
write_hash!(inner, [h_partial, h_full], raw_header.as_bytes())
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
|
||||
// Write new manifest.
|
||||
write_hash!(inner, [h_partial, h_full], &manifest_raw_new)?;
|
||||
write_hash!(inner, [h_partial, h_full], &manifest_raw_new)
|
||||
.map_err(|e| Error::DataWrite("manifest", e))?;
|
||||
|
||||
// Sign metadata (header + manifest) hash. The signature is not included
|
||||
// in the payload hash.
|
||||
let metadata_hash = h_partial.clone().finish();
|
||||
let metadata_sig = sign_digest(metadata_hash.as_ref(), &key)?;
|
||||
let metadata_sig_raw = metadata_sig.encode_to_vec();
|
||||
write_hash!(inner, [h_full], &metadata_sig_raw)?;
|
||||
write_hash!(inner, [h_full], &metadata_sig_raw)
|
||||
.map_err(|e| Error::DataWrite("metadata_signatures", e))?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
header,
|
||||
metadata_hash,
|
||||
metadata_size: OTA_HEADER_SIZE + manifest_raw_new.len(),
|
||||
metadata_size: raw_header.as_bytes().len() + manifest_raw_new.len(),
|
||||
partition_index: None,
|
||||
operation_index: None,
|
||||
done: false,
|
||||
@@ -396,7 +436,8 @@ impl<W: Write> PayloadWriter<W> {
|
||||
let payload_partial_hash = self.h_partial.clone().finish();
|
||||
let payload_sig = sign_digest(payload_partial_hash.as_ref(), &self.key)?;
|
||||
let payload_sig_raw = payload_sig.encode_to_vec();
|
||||
write_hash!(self.inner, [self.h_full], &payload_sig_raw)?;
|
||||
write_hash!(self.inner, [self.h_full], &payload_sig_raw)
|
||||
.map_err(|e| Error::DataWrite("payload_signatures", e))?;
|
||||
|
||||
// Everything before the blob.
|
||||
let metadata_with_sig_size =
|
||||
@@ -421,23 +462,20 @@ impl<W: Write> PayloadWriter<W> {
|
||||
/// Prepare for writing the next source data blob corresponding to an
|
||||
/// [`InstallOperation`]. To write all of the payload data, call this method
|
||||
/// followed by [`Self::write()`] repeatedly until `Ok(false)` is returned
|
||||
/// or an error occurs. This function will fail if the amount of data
|
||||
/// written for the previous operation does not match
|
||||
/// or an error occurs. [`Error::UnwrittenData`] will be returned if the
|
||||
/// amount of data written for the previous operation does not match
|
||||
/// [`InstallOperation::data_length`].
|
||||
pub fn begin_next_operation(&mut self) -> Result<bool> {
|
||||
if let Some(operation) = self.operation() {
|
||||
// Only operations that reference data in the blob will have a
|
||||
// length set.
|
||||
if self.written < operation.data_length.unwrap_or(0) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!(
|
||||
"Expected {} bytes, but only wrote {} bytes",
|
||||
operation.data_length.unwrap(),
|
||||
self.written,
|
||||
),
|
||||
)
|
||||
.into());
|
||||
let expected = operation.data_length.unwrap_or(0);
|
||||
|
||||
if self.written < expected {
|
||||
return Err(Error::UnwrittenData {
|
||||
actual: self.written,
|
||||
expected,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,16 +585,16 @@ pub fn verify_payload(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let header = PayloadHeader::from_reader(&mut reader)?;
|
||||
reader.rewind()?;
|
||||
reader.rewind().map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
let payload_signatures_offset = header
|
||||
.manifest
|
||||
.signatures_offset
|
||||
.ok_or_else(|| Error::MissingField("signatures_offset"))?;
|
||||
.ok_or(Error::MissingField("signatures_offset"))?;
|
||||
let payload_signatures_size = header
|
||||
.manifest
|
||||
.signatures_size
|
||||
.ok_or_else(|| Error::MissingField("signatures_size"))?;
|
||||
.ok_or(Error::MissingField("signatures_size"))?;
|
||||
|
||||
// Excludes signatures (hashes are for signing).
|
||||
let mut h_partial = Context::new(&ring::digest::SHA256);
|
||||
@@ -574,7 +612,8 @@ pub fn verify_payload(
|
||||
h_full.update(data);
|
||||
},
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("metadata", e))?;
|
||||
let metadata_hash = h_partial.clone().finish();
|
||||
|
||||
// Read the metadata signatures.
|
||||
@@ -587,10 +626,11 @@ pub fn verify_payload(
|
||||
header.metadata_signature_size.into(),
|
||||
|data| h_full.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("metadata_signatures", e))?;
|
||||
|
||||
let buf = writer.into_inner();
|
||||
Signatures::decode(buf.as_slice())?
|
||||
Signatures::decode(buf.as_slice()).map_err(Error::SignaturesDecode)?
|
||||
};
|
||||
|
||||
// Check the metadata signatures.
|
||||
@@ -598,7 +638,9 @@ pub fn verify_payload(
|
||||
|
||||
// Check the blob offset.
|
||||
{
|
||||
let actual = reader.stream_position()?;
|
||||
let actual = reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("blob_offset", e))?;
|
||||
if header.blob_offset != actual {
|
||||
return Err(Error::InvalidBlobOffset {
|
||||
expected: header.blob_offset,
|
||||
@@ -617,13 +659,16 @@ pub fn verify_payload(
|
||||
h_full.update(data);
|
||||
},
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("blob", e))?;
|
||||
let payload_hash = h_partial.clone().finish();
|
||||
|
||||
// Check the payload signatures offset.
|
||||
{
|
||||
let expected = header.blob_offset + payload_signatures_offset;
|
||||
let actual = reader.stream_position()?;
|
||||
let actual = reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("payload_signatures_offset", e))?;
|
||||
if expected != actual {
|
||||
return Err(Error::InvalidPayloadSignaturesOffset { expected, actual });
|
||||
}
|
||||
@@ -639,10 +684,11 @@ pub fn verify_payload(
|
||||
payload_signatures_size,
|
||||
|data| h_full.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(|e| Error::DataRead("payload_signatures", e))?;
|
||||
|
||||
let buf = writer.into_inner();
|
||||
Signatures::decode(buf.as_slice())?
|
||||
Signatures::decode(buf.as_slice()).map_err(Error::SignaturesDecode)?
|
||||
};
|
||||
|
||||
// Check the payload signatures.
|
||||
@@ -651,7 +697,9 @@ pub fn verify_payload(
|
||||
// Check properties file.
|
||||
let expected_properties_raw = generate_properties(
|
||||
h_full.finish().as_ref(),
|
||||
reader.stream_position()?,
|
||||
reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("payload_size", e))?,
|
||||
metadata_hash.as_ref(),
|
||||
metadata_size,
|
||||
);
|
||||
@@ -686,19 +734,24 @@ pub fn apply_operation(
|
||||
for extent in &op.dst_extents {
|
||||
let start_block = extent
|
||||
.start_block
|
||||
.ok_or_else(|| Error::MissingField("start_block"))?;
|
||||
let num_blocks = extent
|
||||
.num_blocks
|
||||
.ok_or_else(|| Error::MissingField("num_blocks"))?;
|
||||
.ok_or(Error::MissingField("start_block"))?;
|
||||
let num_blocks = extent.num_blocks.ok_or(Error::MissingField("num_blocks"))?;
|
||||
|
||||
let out_offset = start_block
|
||||
.checked_mul(block_size.into())
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("out_offset"))?;
|
||||
.ok_or(Error::IntOverflow("out_offset"))?;
|
||||
let out_data_length = num_blocks
|
||||
.checked_mul(block_size.into())
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("out_data_length"))?;
|
||||
.ok_or(Error::IntOverflow("out_data_length"))?;
|
||||
|
||||
writer.seek(SeekFrom::Start(out_offset))?;
|
||||
let error_fn = |e: io::Error| Error::OperationApply {
|
||||
op_type: op.r#type(),
|
||||
start_block,
|
||||
num_blocks,
|
||||
source: e,
|
||||
};
|
||||
|
||||
writer.seek(SeekFrom::Start(out_offset)).map_err(error_fn)?;
|
||||
|
||||
let mut hasher = Context::new(&ring::digest::SHA256);
|
||||
|
||||
@@ -712,20 +765,17 @@ pub fn apply_operation(
|
||||
out_data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
other => {
|
||||
let data_offset = op
|
||||
.data_offset
|
||||
.ok_or_else(|| Error::MissingField("data_offset"))?;
|
||||
let data_length = op
|
||||
.data_length
|
||||
.ok_or_else(|| Error::MissingField("data_length"))?;
|
||||
let data_offset = op.data_offset.ok_or(Error::MissingField("data_offset"))?;
|
||||
let data_length = op.data_length.ok_or(Error::MissingField("data_length"))?;
|
||||
let in_offset = blob_offset
|
||||
.checked_add(data_offset)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("in_offset"))?;
|
||||
.ok_or(Error::IntOverflow("in_offset"))?;
|
||||
|
||||
reader.seek(SeekFrom::Start(in_offset))?;
|
||||
reader.seek(SeekFrom::Start(in_offset)).map_err(error_fn)?;
|
||||
|
||||
match other {
|
||||
Type::Replace => {
|
||||
@@ -735,7 +785,8 @@ pub fn apply_operation(
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
)
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
Type::ReplaceBz => {
|
||||
let mut decoder = BzDecoder::new(&mut writer);
|
||||
@@ -745,8 +796,9 @@ pub fn apply_operation(
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
decoder.finish()?;
|
||||
)
|
||||
.and_then(|()| decoder.finish())
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
Type::ReplaceXz => {
|
||||
let mut decoder = XzDecoder::new(&mut writer);
|
||||
@@ -756,8 +808,9 @@ pub fn apply_operation(
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)?;
|
||||
decoder.finish()?;
|
||||
)
|
||||
.and_then(|()| decoder.finish())
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
_ => return Err(Error::UnsupportedOperation(op.r#type())),
|
||||
}
|
||||
@@ -799,8 +852,10 @@ pub fn extract_image(
|
||||
.operations
|
||||
.par_iter()
|
||||
.map(|op| -> Result<()> {
|
||||
let reader = payload.reopen_boxed()?;
|
||||
let writer = output.reopen_boxed()?;
|
||||
let reader = payload.reopen_boxed().map_err(Error::PayloadReopen)?;
|
||||
let writer = output
|
||||
.reopen_boxed()
|
||||
.map_err(|e| Error::OutputOpen(partition_name.to_owned(), e))?;
|
||||
|
||||
apply_operation(
|
||||
reader,
|
||||
@@ -847,8 +902,8 @@ pub fn extract_images<'a>(
|
||||
operations
|
||||
.into_par_iter()
|
||||
.map(|(name, op)| -> Result<()> {
|
||||
let reader = payload.reopen_boxed()?;
|
||||
let writer = open_output(name)?;
|
||||
let reader = payload.reopen_boxed().map_err(Error::PayloadReopen)?;
|
||||
let writer = open_output(name).map_err(|e| Error::OutputOpen(name.to_owned(), e))?;
|
||||
|
||||
apply_operation(
|
||||
reader,
|
||||
@@ -873,12 +928,13 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
|
||||
// decompression. Also, we intentionally pick the lowest compression level
|
||||
// since we primarily care about squishing zeros. The non-zero portions of
|
||||
// boot images are usually already-compressed kernels and ramdisks.
|
||||
let stream = Stream::new_easy_encoder(0, Check::None)?;
|
||||
let stream = Stream::new_easy_encoder(0, Check::None).map_err(Error::XzInit)?;
|
||||
let mut xz_writer = XzEncoder::new_stream(hashing_writer, stream);
|
||||
|
||||
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)?;
|
||||
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)
|
||||
.map_err(Error::XzCompress)?;
|
||||
|
||||
let hashing_writer = xz_writer.finish()?;
|
||||
let hashing_writer = xz_writer.finish().map_err(Error::XzCompress)?;
|
||||
let (writer, context_compressed) = hashing_writer.finish();
|
||||
let digest_compressed = context_compressed.finish();
|
||||
let data = writer.into_inner();
|
||||
@@ -901,7 +957,7 @@ impl VabcAlgo {
|
||||
}
|
||||
}
|
||||
|
||||
fn compressed_size(&self, mut raw_data: &[u8], block_size: u32) -> u64 {
|
||||
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> u64 {
|
||||
let mut total = 0;
|
||||
|
||||
while !raw_data.is_empty() {
|
||||
@@ -957,7 +1013,10 @@ pub fn compress_image(
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
const CHUNK_GROUP: u64 = 32;
|
||||
|
||||
let file_size = input.reopen_boxed()?.seek(SeekFrom::End(0))?;
|
||||
let file_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut r| r.seek(SeekFrom::End(0)))
|
||||
.map_err(|e| Error::InputOpen(partition_name.to_owned(), e))?;
|
||||
let final_chunk_different = file_size % CHUNK_SIZE != 0;
|
||||
|
||||
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
|
||||
@@ -983,7 +1042,7 @@ pub fn compress_image(
|
||||
|
||||
let uncompressed_data_group = (chunks_done..chunks_done + chunks_group)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<(u64, Vec<u8>)> {
|
||||
.map(|chunk| -> io::Result<(u64, Vec<u8>)> {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let offset = reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
|
||||
|
||||
@@ -992,14 +1051,14 @@ pub fn compress_image(
|
||||
} else {
|
||||
CHUNK_SIZE
|
||||
};
|
||||
let mut data = vec![0u8; chunk_size as usize];
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
reader.read_exact(&mut data)?;
|
||||
let data = reader.read_vec_exact(chunk_size as usize)?;
|
||||
|
||||
Ok((offset, data))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(Error::ChunkRead)?;
|
||||
|
||||
for (_, data) in &uncompressed_data_group {
|
||||
context_uncompressed.update(data);
|
||||
@@ -1010,11 +1069,8 @@ pub fn compress_image(
|
||||
.map(
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
|
||||
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
|
||||
let cow_size = if let Some(algo) = vabc_algo {
|
||||
algo.compressed_size(&raw_data, block_size)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let cow_size =
|
||||
vabc_algo.map_or(0, |a| a.compressed_size(&raw_data, block_size));
|
||||
|
||||
let extent = Extent {
|
||||
start_block: Some(raw_offset / u64::from(block_size)),
|
||||
@@ -1040,14 +1096,15 @@ pub fn compress_image(
|
||||
|
||||
let group_operations = compressed_data_group
|
||||
.into_par_iter()
|
||||
.map(|(data, operation, _)| -> Result<InstallOperation> {
|
||||
.map(|(data, operation, _)| -> io::Result<InstallOperation> {
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
|
||||
Ok(operation)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(Error::ChunkWrite)?;
|
||||
|
||||
operations.extend(group_operations.into_iter());
|
||||
}
|
||||
@@ -1158,28 +1215,29 @@ pub fn compress_modified_image(
|
||||
let extents_start = operation.dst_extents[0]
|
||||
.start_block()
|
||||
.checked_mul(u64::from(block_size))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_start"))?;
|
||||
.ok_or(Error::IntOverflow("extents_start"))?;
|
||||
let extents_size = operation
|
||||
.dst_extents
|
||||
.iter()
|
||||
.map(|e| e.num_blocks())
|
||||
.try_fold(0u64, |acc, n| acc.checked_add(n))
|
||||
.and_then(|n| n.checked_mul(u64::from(block_size)))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
|
||||
.ok_or(Error::IntOverflow("extents_size"))?;
|
||||
let extents_end = extents_start
|
||||
.checked_add(extents_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_end"))?;
|
||||
let extents_size = extents_size
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
|
||||
.ok_or(Error::IntOverflow("extents_end"))?;
|
||||
let extents_size: usize = util::try_cast(extents_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("extents_size", e))?;
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(extents_start))?;
|
||||
let mut reader = input.reopen_boxed().map_err(Error::ChunkRead)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(extents_start))
|
||||
.map_err(Error::ChunkRead)?;
|
||||
|
||||
let mut data = vec![0u8; extents_size];
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
reader.read_exact(&mut data)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::ChunkRead)?;
|
||||
let data = reader
|
||||
.read_vec_exact(extents_size)
|
||||
.map_err(Error::ChunkRead)?;
|
||||
|
||||
let was_modified = util::ranges_overlaps(ranges, &(extents_start..extents_end));
|
||||
|
||||
@@ -1224,7 +1282,8 @@ pub fn compress_modified_image(
|
||||
|
||||
Ok(i..i + 1)
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(Error::ChunkWrite)?;
|
||||
|
||||
modified_operations.extend(modified_group_operations);
|
||||
}
|
||||
|
||||
+159
-118
@@ -11,7 +11,10 @@ use std::{
|
||||
use crc32fast::Hasher;
|
||||
use dlv_list::{Index, VecList};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{byteorder::little_endian, AsBytes, FromBytes, FromZeroes, Unaligned};
|
||||
use zerocopy::{byteorder::little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::stream::ReadDiscardExt;
|
||||
|
||||
/// Magic value for [`RawHeader::magic`].
|
||||
const HEADER_MAGIC: u32 = 0xed26ff3a;
|
||||
@@ -32,22 +35,77 @@ pub const MINOR_VERSION: u16 = 0;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Sparse header: {0}")]
|
||||
Header(String),
|
||||
#[error("Sparse chunk #{0}: {1}")]
|
||||
Chunk(u32, String),
|
||||
#[error("Sparse reader: {0}")]
|
||||
Reader(String),
|
||||
#[error("Sparse writer: {0}")]
|
||||
Writer(String),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
// Header errors.
|
||||
#[error("Invalid magic: {0:#010x}")]
|
||||
InvalidMagic(u32),
|
||||
#[error("Unsupported major version: {0}")]
|
||||
UnsupportedMajorVersion(u16),
|
||||
#[error("Invalid file header size: {0} < {size}", size = mem::size_of::<RawHeader>())]
|
||||
InvalidFileHeaderSize(u16),
|
||||
#[error("Invalid chunk header size: {0} < {size}", size = mem::size_of::<RawChunk>())]
|
||||
InvalidChunkHeaderSize(u16),
|
||||
#[error("Invalid block size (must be a non-zero multiple of 4): {0}")]
|
||||
InvalidBlockSize(u32),
|
||||
// Chunk errors.
|
||||
#[error("Chunk #{index}: Size overflow: {chunk_size} * {block_size}")]
|
||||
ChunkSizeOverflow {
|
||||
index: u32,
|
||||
chunk_size: u32,
|
||||
block_size: u32,
|
||||
},
|
||||
#[error("Chunk #{index}: Invalid type: {chunk_type}")]
|
||||
InvalidChunkType { index: u32, chunk_type: u16 },
|
||||
#[error("Chunk #{index}: Data size too large: {data_size}")]
|
||||
DataSizeTooLarge { index: u32, data_size: u32 },
|
||||
#[error("Chunk #{index}: Block count overflow: {start_block} + {chunk_size}")]
|
||||
BlockCountOverflow {
|
||||
index: u32,
|
||||
start_block: u32,
|
||||
chunk_size: u32,
|
||||
},
|
||||
#[error("Chunk #{index}: End block {end_block} exceeds total blocks {total_blocks}")]
|
||||
EndBlockExceedsTotal {
|
||||
index: u32,
|
||||
end_block: u32,
|
||||
total_blocks: u32,
|
||||
},
|
||||
#[error("Chunk #{index}: CRC32 chunk is not empty")]
|
||||
Crc32ChunkNotEmpty { index: u32, chunk_size: u32 },
|
||||
#[error("Chunk #{index}: Expected total size {expected_size}, but have {total_size}")]
|
||||
InvalidChunkSize {
|
||||
index: u32,
|
||||
expected_size: u32,
|
||||
total_size: u32,
|
||||
},
|
||||
// Reader errors.
|
||||
#[error("Must fully consume data when CRC validation is enabled")]
|
||||
Crc32RandomRead,
|
||||
#[error("Previous chunk still has {0} unread bytes")]
|
||||
UnreadChunkData(u32),
|
||||
#[error("Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}")]
|
||||
MismatchedCrc32Checkpoint { expected: u32, actual: u32 },
|
||||
#[error("Expected final CRC32 {expected:08x}, but have {actual:08x}")]
|
||||
MismatchedCrc32Final { expected: u32, actual: u32 },
|
||||
// Writer errors.
|
||||
#[error("Minor version not supported for writing: {0}")]
|
||||
UnsupportedMinorVersion(u16),
|
||||
#[error("Previous chunk still has {0} unwritten bytes")]
|
||||
UnwrittenChunkData(u32),
|
||||
#[error("Already wrote all chunk headers")]
|
||||
TooManyChunks,
|
||||
#[error("Gap between end of last chunk {prev_end} and start of new chunk {cur_start}")]
|
||||
GapBetweenChunks { prev_end: u32, cur_start: u32 },
|
||||
// Wrapped errors.
|
||||
#[error("Failed to read sparse data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write sparse data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Raw on-disk layout for the header.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawHeader {
|
||||
/// Magic value. This should be equal to [`HEADER_MAGIC`].
|
||||
@@ -94,44 +152,37 @@ impl fmt::Debug for RawHeader {
|
||||
impl RawHeader {
|
||||
fn validate(&self) -> Result<()> {
|
||||
if self.magic.get() != HEADER_MAGIC {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid magic: {:#010x}",
|
||||
self.magic.get(),
|
||||
)));
|
||||
return Err(Error::InvalidMagic(self.magic.get()));
|
||||
}
|
||||
|
||||
if self.major_version.get() != MAJOR_VERSION {
|
||||
return Err(Error::Header(format!(
|
||||
"Unsupported major version: {}",
|
||||
self.major_version.get(),
|
||||
)));
|
||||
return Err(Error::UnsupportedMajorVersion(self.major_version.get()));
|
||||
}
|
||||
|
||||
if self.file_hdr_sz.get() != mem::size_of::<RawHeader>() as u16 {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid file header size: {}",
|
||||
self.file_hdr_sz.get(),
|
||||
)));
|
||||
} else if self.chunk_hdr_sz.get() != mem::size_of::<RawChunk>() as u16 {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid chunk header size: {}",
|
||||
self.chunk_hdr_sz.get(),
|
||||
)));
|
||||
if self.file_hdr_sz.get() < mem::size_of::<RawHeader>() as u16 {
|
||||
return Err(Error::InvalidFileHeaderSize(self.file_hdr_sz.get()));
|
||||
} else if self.chunk_hdr_sz.get() < mem::size_of::<RawChunk>() as u16 {
|
||||
return Err(Error::InvalidChunkHeaderSize(self.chunk_hdr_sz.get()));
|
||||
}
|
||||
|
||||
if self.blk_sz.get() == 0 || self.blk_sz.get() % 4 != 0 {
|
||||
return Err(Error::Header(format!(
|
||||
"Invalid block size: {}",
|
||||
self.blk_sz.get(),
|
||||
)));
|
||||
return Err(Error::InvalidBlockSize(self.blk_sz.get()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn excess_raw_header_bytes(&self) -> u16 {
|
||||
self.file_hdr_sz.get() - mem::size_of::<RawHeader>() as u16
|
||||
}
|
||||
|
||||
fn excess_raw_chunk_bytes(&self) -> u16 {
|
||||
self.chunk_hdr_sz.get() - mem::size_of::<RawChunk>() as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw on-disk layout for the chunk header.
|
||||
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(packed)]
|
||||
struct RawChunk {
|
||||
/// Chunk type. Must be [`CHUNK_TYPE_RAW`], [`CHUNK_TYPE_FILL`],
|
||||
@@ -163,69 +214,58 @@ impl RawChunk {
|
||||
.chunk_sz
|
||||
.get()
|
||||
.checked_mul(header.blk_sz.get())
|
||||
.ok_or_else(|| {
|
||||
Error::Chunk(
|
||||
index,
|
||||
format!(
|
||||
"Chunk size overflow: {} * {}",
|
||||
self.chunk_sz.get(),
|
||||
header.blk_sz.get(),
|
||||
),
|
||||
)
|
||||
.ok_or_else(|| Error::ChunkSizeOverflow {
|
||||
index,
|
||||
chunk_size: self.chunk_sz.get(),
|
||||
block_size: header.blk_sz.get(),
|
||||
})?,
|
||||
CHUNK_TYPE_FILL | CHUNK_TYPE_CRC32 => 4,
|
||||
CHUNK_TYPE_DONT_CARE => 0,
|
||||
t => return Err(Error::Chunk(index, format!("Invalid chunk type: {t}"))),
|
||||
t => {
|
||||
return Err(Error::InvalidChunkType {
|
||||
index,
|
||||
chunk_type: t,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
data_size
|
||||
.checked_add(mem::size_of::<Self>() as u32)
|
||||
.ok_or_else(|| Error::Chunk(index, format!("Data size too large: {data_size}")))
|
||||
.checked_add(header.chunk_hdr_sz.into())
|
||||
.ok_or(Error::DataSizeTooLarge { index, data_size })
|
||||
}
|
||||
|
||||
fn validate(&self, index: u32, header: &RawHeader, start_block: u32) -> Result<()> {
|
||||
let end_block = start_block
|
||||
.checked_add(self.chunk_sz.get())
|
||||
.ok_or_else(|| {
|
||||
Error::Chunk(
|
||||
index,
|
||||
format!(
|
||||
"Block count overflow: {start_block} + {}",
|
||||
self.chunk_sz.get(),
|
||||
),
|
||||
)
|
||||
.ok_or_else(|| Error::BlockCountOverflow {
|
||||
index,
|
||||
start_block,
|
||||
chunk_size: self.chunk_sz.get(),
|
||||
})?;
|
||||
|
||||
if end_block > header.total_blks.get() {
|
||||
return Err(Error::Chunk(
|
||||
return Err(Error::EndBlockExceedsTotal {
|
||||
index,
|
||||
format!(
|
||||
"End block {end_block} exceeds total blocks {}",
|
||||
header.total_blks.get(),
|
||||
),
|
||||
))?;
|
||||
end_block,
|
||||
total_blocks: header.total_blks.get(),
|
||||
})?;
|
||||
}
|
||||
|
||||
if self.chunk_type.get() == CHUNK_TYPE_CRC32 && self.chunk_sz.get() != 0 {
|
||||
return Err(Error::Chunk(
|
||||
return Err(Error::Crc32ChunkNotEmpty {
|
||||
index,
|
||||
format!(
|
||||
"CRC32 chunk has non-zero blocks: {:?}",
|
||||
start_block..end_block,
|
||||
),
|
||||
));
|
||||
chunk_size: self.chunk_sz.get(),
|
||||
});
|
||||
}
|
||||
|
||||
let expected_size = self.expected_size(index, header)?;
|
||||
|
||||
if expected_size != self.total_sz.get() {
|
||||
return Err(Error::Chunk(
|
||||
return Err(Error::InvalidChunkSize {
|
||||
index,
|
||||
format!(
|
||||
"Expected total size {expected_size}, but have {}",
|
||||
self.total_sz.get(),
|
||||
),
|
||||
));
|
||||
expected_size,
|
||||
total_size: self.total_sz.get(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -656,11 +696,15 @@ impl<R: Read> SparseReader<R> {
|
||||
/// data chunks if they are not needed. If the underlying file is seekable
|
||||
/// and skipping chunks is needed, use [`Self::new_seekable`] instead.
|
||||
pub fn new(mut inner: R, crc_mode: CrcMode) -> Result<Self> {
|
||||
let mut header = RawHeader::new_zeroed();
|
||||
inner.read_exact(header.as_bytes_mut())?;
|
||||
let header =
|
||||
RawHeader::read_from_io(&mut inner).map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
header.validate()?;
|
||||
|
||||
inner
|
||||
.read_discard(header.excess_raw_header_bytes().into())
|
||||
.map_err(|e| Error::DataRead("header_excess", e))?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
seek: None,
|
||||
@@ -699,18 +743,14 @@ impl<R: Read> SparseReader<R> {
|
||||
if self.data_remain != 0 {
|
||||
if let Some(seek) = self.seek {
|
||||
if self.hasher.is_some() {
|
||||
return Err(Error::Reader(
|
||||
"Cannot skip data when CRC validation is enabled".into(),
|
||||
));
|
||||
return Err(Error::Crc32RandomRead);
|
||||
}
|
||||
|
||||
seek(&mut self.inner, SeekFrom::Current(self.data_remain.into()))?;
|
||||
seek(&mut self.inner, SeekFrom::Current(self.data_remain.into()))
|
||||
.map_err(|e| Error::DataRead("data_remain", e))?;
|
||||
self.data_remain = 0;
|
||||
} else {
|
||||
return Err(Error::Reader(format!(
|
||||
"Previous chunk still has {} bytes remaining",
|
||||
self.data_remain,
|
||||
)));
|
||||
return Err(Error::UnreadChunkData(self.data_remain));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,11 +758,15 @@ impl<R: Read> SparseReader<R> {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut raw_chunk = RawChunk::new_zeroed();
|
||||
self.inner.read_exact(raw_chunk.as_bytes_mut())?;
|
||||
let raw_chunk =
|
||||
RawChunk::read_from_io(&mut self.inner).map_err(|e| Error::DataRead("chunk", e))?;
|
||||
|
||||
raw_chunk.validate(self.chunk, &self.header, self.block)?;
|
||||
|
||||
self.inner
|
||||
.read_discard(self.header.excess_raw_chunk_bytes().into())
|
||||
.map_err(|e| Error::DataRead("chunk_excess", e))?;
|
||||
|
||||
let data: ChunkData;
|
||||
|
||||
match raw_chunk.chunk_type.get() {
|
||||
@@ -733,8 +777,8 @@ impl<R: Read> SparseReader<R> {
|
||||
data = ChunkData::Data;
|
||||
}
|
||||
CHUNK_TYPE_FILL => {
|
||||
let mut fill_value = little_endian::U32::new_zeroed();
|
||||
self.inner.read_exact(fill_value.as_bytes_mut())?;
|
||||
let fill_value = little_endian::U32::read_from_io(&mut self.inner)
|
||||
.map_err(|e| Error::DataRead("chunk_fill_value", e))?;
|
||||
|
||||
if let Some(hasher) = &mut self.hasher {
|
||||
hash_fill_chunk(&raw_chunk, fill_value, &self.header, hasher);
|
||||
@@ -750,16 +794,17 @@ impl<R: Read> SparseReader<R> {
|
||||
data = ChunkData::Hole;
|
||||
}
|
||||
CHUNK_TYPE_CRC32 => {
|
||||
let mut expected = little_endian::U32::new_zeroed();
|
||||
self.inner.read_exact(expected.as_bytes_mut())?;
|
||||
let expected = little_endian::U32::read_from_io(&mut self.inner)
|
||||
.map_err(|e| Error::DataRead("chunk_crc32", e))?;
|
||||
|
||||
if let Some(hasher) = &mut self.hasher {
|
||||
let actual = hasher.clone().finalize();
|
||||
|
||||
if actual != expected.get() {
|
||||
return Err(Error::Reader(format!(
|
||||
"Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}",
|
||||
)));
|
||||
return Err(Error::MismatchedCrc32Checkpoint {
|
||||
expected: expected.get(),
|
||||
actual,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,9 +835,7 @@ impl<R: Read> SparseReader<R> {
|
||||
let actual = hasher.finalize();
|
||||
|
||||
if actual != expected {
|
||||
return Err(Error::Reader(format!(
|
||||
"Expected final CRC32 {expected:08x}, but have {actual:08x}",
|
||||
)));
|
||||
return Err(Error::MismatchedCrc32Final { expected, actual });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -836,10 +879,7 @@ impl<W: Write> SparseWriter<W> {
|
||||
/// file to be seekable, so the [`Header`] must be fully known up front.
|
||||
pub fn new(mut inner: W, header: Header) -> Result<Self> {
|
||||
if header.minor_version != MINOR_VERSION {
|
||||
return Err(Error::Writer(format!(
|
||||
"Minor version not supported for writing: {}",
|
||||
header.minor_version,
|
||||
)));
|
||||
return Err(Error::UnsupportedMinorVersion(header.minor_version));
|
||||
}
|
||||
|
||||
let header = RawHeader {
|
||||
@@ -856,7 +896,9 @@ impl<W: Write> SparseWriter<W> {
|
||||
|
||||
header.validate()?;
|
||||
|
||||
inner.write_all(header.as_bytes())?;
|
||||
header
|
||||
.write_to_io(&mut inner)
|
||||
.map_err(|e| Error::DataWrite("header", e))?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
@@ -874,21 +916,18 @@ impl<W: Write> SparseWriter<W> {
|
||||
/// [`ChunkData::Data`], the data must be fully written first.
|
||||
pub fn start_chunk(&mut self, chunk: Chunk) -> Result<()> {
|
||||
if self.data_remain != 0 {
|
||||
return Err(Error::Writer(format!(
|
||||
"Previous chunk still has {} bytes remaining",
|
||||
self.data_remain,
|
||||
)));
|
||||
return Err(Error::UnwrittenChunkData(self.data_remain));
|
||||
}
|
||||
|
||||
if self.chunk == self.header.total_chunks.get() {
|
||||
return Err(Error::Writer("Already wrote all chunk headers".into()));
|
||||
return Err(Error::TooManyChunks);
|
||||
}
|
||||
|
||||
if chunk.bounds.start != self.block {
|
||||
return Err(Error::Writer(format!(
|
||||
"Gap between end of last chunk {} and start of new chunk {}",
|
||||
self.block, chunk.bounds.start,
|
||||
)));
|
||||
return Err(Error::GapBetweenChunks {
|
||||
prev_end: self.block,
|
||||
cur_start: chunk.bounds.start,
|
||||
});
|
||||
}
|
||||
|
||||
let mut raw_chunk = RawChunk {
|
||||
@@ -910,7 +949,9 @@ impl<W: Write> SparseWriter<W> {
|
||||
self.chunk += 1;
|
||||
self.block = chunk.bounds.end;
|
||||
|
||||
self.inner.write_all(raw_chunk.as_bytes())?;
|
||||
raw_chunk
|
||||
.write_to_io(&mut self.inner)
|
||||
.map_err(|e| Error::DataWrite("chunk", e))?;
|
||||
|
||||
match chunk.data {
|
||||
ChunkData::Data => {
|
||||
@@ -918,7 +959,9 @@ impl<W: Write> SparseWriter<W> {
|
||||
raw_chunk.total_sz.get() - u32::from(self.header.chunk_hdr_sz.get());
|
||||
}
|
||||
ChunkData::Fill(fill_value) => {
|
||||
self.inner.write_all(&fill_value.to_le_bytes())?;
|
||||
self.inner
|
||||
.write_all(&fill_value.to_le_bytes())
|
||||
.map_err(|e| Error::DataWrite("chunk_fill_value", e))?;
|
||||
|
||||
hash_fill_chunk(
|
||||
&raw_chunk,
|
||||
@@ -931,13 +974,13 @@ impl<W: Write> SparseWriter<W> {
|
||||
hash_fill_chunk(&raw_chunk, 0.into(), &self.header, &mut self.hasher);
|
||||
}
|
||||
ChunkData::Crc32(expected) => {
|
||||
self.inner.write_all(&expected.to_le_bytes())?;
|
||||
self.inner
|
||||
.write_all(&expected.to_le_bytes())
|
||||
.map_err(|e| Error::DataWrite("chunk_crc32", e))?;
|
||||
|
||||
let actual = self.hasher.clone().finalize();
|
||||
if actual != expected {
|
||||
return Err(Error::Reader(format!(
|
||||
"Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}",
|
||||
)));
|
||||
return Err(Error::MismatchedCrc32Checkpoint { expected, actual });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -952,9 +995,7 @@ impl<W: Write> SparseWriter<W> {
|
||||
let actual = self.hasher.finalize();
|
||||
|
||||
if actual != expected {
|
||||
return Err(Error::Reader(format!(
|
||||
"Expected final CRC32 {expected:08x}, but have {actual:08x}",
|
||||
)));
|
||||
return Err(Error::MismatchedCrc32Final { expected, actual });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
|
||||
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
|
||||
error!("{e:?}");
|
||||
|
||||
@@ -28,7 +28,7 @@ where
|
||||
{
|
||||
struct OctalStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
|
||||
impl<T> Visitor<'_> for OctalStrVisitor<T>
|
||||
where
|
||||
T: PrimInt,
|
||||
<T as Num>::FromStrRadixErr: fmt::Display,
|
||||
|
||||
+240
-154
@@ -4,12 +4,13 @@
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Write,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Cursor, Read, Seek},
|
||||
io::{self, BufRead, BufReader, Cursor, Read},
|
||||
num::ParseIntError,
|
||||
ops::Range,
|
||||
ops::{Range, RangeFrom},
|
||||
path::{Path, PathBuf},
|
||||
slice,
|
||||
slice, str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
@@ -41,8 +42,6 @@ use crate::{
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("No compatible boot image found for {0}")]
|
||||
NoTargets(&'static str),
|
||||
#[error("Boot image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash descriptor found in vbmeta header")]
|
||||
@@ -53,41 +52,80 @@ pub enum Error {
|
||||
ParseMagiskVersion(String, #[source] ParseIntError),
|
||||
#[error("Failed to determine Magisk version from: {0:?}")]
|
||||
FindMagiskVersion(PathBuf),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("Boot image error")]
|
||||
BootImage(#[from] bootimage::Error),
|
||||
#[error("Compression error")]
|
||||
Compression(#[from] compression::Error),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("CPIO error")]
|
||||
Cpio(#[from] cpio::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[source] ZipError),
|
||||
#[error("Zip error for entry name: {0:?}")]
|
||||
ZipEntryName(String, #[source] ZipError),
|
||||
#[error("Zip error for entry index #{0}")]
|
||||
ZipEntryIndex(usize, #[source] ZipError),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("File I/O error")]
|
||||
File(PathBuf, #[source] io::Error),
|
||||
#[error("Failed to load potentially compressed ramdisk")]
|
||||
RamdiskLoadCompression(#[source] compression::Error),
|
||||
#[error("Failed to save potentially compressed ramdisk")]
|
||||
RamdiskSaveCompression(#[source] compression::Error),
|
||||
#[error("Failed to finalize compressed ramdisk")]
|
||||
RamdiskSaveCompressionFinalize(#[source] io::Error),
|
||||
#[error("Failed to load ramdisk cpio entries")]
|
||||
RamdiskLoadCpio(#[source] cpio::Error),
|
||||
#[error("Failed to save ramdisk cpio entries")]
|
||||
RamdiskSaveCpio(#[source] cpio::Error),
|
||||
#[error("Failed to load potentially compressed kernel")]
|
||||
KernelLoadCompression(#[source] compression::Error),
|
||||
#[error("Failed to read kernel image")]
|
||||
KernelRead(#[source] io::Error),
|
||||
#[error("Failed to load boot image")]
|
||||
BootImageLoad(#[source] bootimage::Error),
|
||||
#[error("Failed to save boot image")]
|
||||
BootImageSave(#[source] bootimage::Error),
|
||||
#[error("Failed to seek boot image")]
|
||||
BootImageSeek(#[source] io::Error),
|
||||
#[error("Failed to encode public key in AVB binary format")]
|
||||
AvbEncodeKey(#[source] avb::Error),
|
||||
#[error("Failed to load AVB header from boot image")]
|
||||
AvbLoad(#[source] avb::Error),
|
||||
#[error("Failed to update AVB header for boot image")]
|
||||
AvbUpdate(#[source] avb::Error),
|
||||
#[error("Failed to load OTA certificate")]
|
||||
OtaCertLoad(#[source] crypto::Error),
|
||||
#[error("Failed to generate replacement otacerts zip")]
|
||||
OtaCertZip(#[source] otacert::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
#[error("Failed to XZ compress entry: {:?}", .0.as_bstr())]
|
||||
XzCompress(Vec<u8>, #[source] io::Error),
|
||||
#[error("Failed to open zip file: {0:?}")]
|
||||
ZipOpen(PathBuf, #[source] ZipError),
|
||||
#[error("Failed to open zip entry: {0:?}")]
|
||||
ZipEntryOpen(&'static str, #[source] ZipError),
|
||||
#[error("Failed to read zip entry: {0:?}")]
|
||||
ZipEntryRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to open zip entry #{0}")]
|
||||
ZipIndexOpen(usize, #[source] ZipError),
|
||||
#[error("Failed to open file: {0:?}")]
|
||||
FileOpen(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum TargetsError {
|
||||
#[error("No compatible boot image found for {0}")]
|
||||
NoTargets(&'static str),
|
||||
#[error("Targets validation error: {0}")]
|
||||
TargetValidation(String),
|
||||
#[error("Failed to open boot image: {0}")]
|
||||
Open(String, #[source] io::Error),
|
||||
#[error("Failed to load boot image: {0}")]
|
||||
Load(String, #[source] Error),
|
||||
#[error("Failed to save boot image: {0}")]
|
||||
Save(String, #[source] Error),
|
||||
#[error("Failed to patch boot image: {0}")]
|
||||
Patch(String, #[source] Error),
|
||||
}
|
||||
|
||||
type TargetsResult<T> = std::result::Result<T, TargetsError>;
|
||||
|
||||
fn load_ramdisk(
|
||||
data: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<CpioEntry>, CompressedFormat)> {
|
||||
let raw_reader = Cursor::new(data);
|
||||
let mut reader = CompressedReader::new(raw_reader, false)?;
|
||||
let entries = cpio::load(&mut reader, false, cancel_signal)?;
|
||||
let mut reader =
|
||||
CompressedReader::new(raw_reader, false).map_err(Error::RamdiskLoadCompression)?;
|
||||
let entries = cpio::load(&mut reader, false, cancel_signal).map_err(Error::RamdiskLoadCpio)?;
|
||||
|
||||
trace!(
|
||||
"Loaded {:?} ramdisk with {} entries",
|
||||
@@ -104,12 +142,15 @@ fn save_ramdisk(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let mut writer = CompressedWriter::new(raw_writer, format)?;
|
||||
cpio::save(&mut writer, entries, false, cancel_signal)?;
|
||||
let mut writer =
|
||||
CompressedWriter::new(raw_writer, format).map_err(Error::RamdiskSaveCompression)?;
|
||||
cpio::save(&mut writer, entries, false, cancel_signal).map_err(Error::RamdiskSaveCpio)?;
|
||||
|
||||
trace!("Wrote {format:?} ramdisk with {} entries", entries.len());
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let raw_writer = writer
|
||||
.finish()
|
||||
.map_err(Error::RamdiskSaveCompressionFinalize)?;
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
@@ -130,7 +171,7 @@ pub trait BootImagePatch {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>>;
|
||||
) -> TargetsResult<Vec<&'a str>>;
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()>;
|
||||
}
|
||||
@@ -151,13 +192,13 @@ impl MagiskRootPatcher {
|
||||
// RULESDEVICE config option, which stored the writable block device as an
|
||||
// rdev major/minor pair, which was not consistent across reboots and was
|
||||
// replaced by PREINITDEVICE
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..28100];
|
||||
const VER_PREINIT_DEVICE: Range<u32> =
|
||||
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
// - Versions newer than the latest supported version are assumed to support
|
||||
// the same features as the latest version
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..28200];
|
||||
const VER_PREINIT_DEVICE: RangeFrom<u32> = 25211..;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
const VER_PATCH_VBMETA: Range<u32> = Self::VERS_SUPPORTED[0].start..26202;
|
||||
const VER_XZ_BACKUP: Range<u32> =
|
||||
26403..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
const VER_XZ_BACKUP: RangeFrom<u32> = 26403..;
|
||||
|
||||
const ZIP_INIT_LD: &'static str = "lib/arm64-v8a/libinit-ld.so";
|
||||
const ZIP_LIBMAGISK: &'static str = "lib/arm64-v8a/libmagisk.so";
|
||||
@@ -216,18 +257,21 @@ impl MagiskRootPatcher {
|
||||
}
|
||||
|
||||
fn get_version(path: &Path) -> Result<u32> {
|
||||
let reader = File::open(path).map_err(|e| Error::File(path.to_owned(), e))?;
|
||||
let reader = BufReader::new(reader);
|
||||
let mut zip = ZipArchive::new(reader).map_err(Error::Zip)?;
|
||||
let reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(path.to_owned(), e))?;
|
||||
let mut zip = ZipArchive::new(reader).map_err(|e| Error::ZipOpen(path.to_owned(), e))?;
|
||||
let entry = zip
|
||||
.by_name(Self::ZIP_UTIL_FUNCTIONS)
|
||||
.map_err(|e| Error::ZipEntryName(Self::ZIP_UTIL_FUNCTIONS.to_owned(), e))?;
|
||||
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_UTIL_FUNCTIONS, e))?;
|
||||
let mut entry = BufReader::new(entry);
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
let n = entry.read_line(&mut line)?;
|
||||
let n = entry
|
||||
.read_line(&mut line)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_UTIL_FUNCTIONS, e))?;
|
||||
if n == 0 {
|
||||
return Err(Error::FindMagiskVersion(path.to_owned()));
|
||||
}
|
||||
@@ -243,14 +287,14 @@ impl MagiskRootPatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn xz_compress(reader: impl Read, cancel_signal: &AtomicBool) -> Result<Vec<u8>> {
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32)?;
|
||||
fn xz_compress(name: &[u8], reader: impl Read, cancel_signal: &AtomicBool) -> Result<Vec<u8>> {
|
||||
let stream = Stream::new_easy_encoder(9, Check::Crc32).map_err(Error::XzInit)?;
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = XzEncoder::new_stream(raw_writer, stream);
|
||||
|
||||
stream::copy(reader, &mut writer, cancel_signal)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let raw_writer = stream::copy(reader, &mut writer, cancel_signal)
|
||||
.and_then(|_| writer.finish())
|
||||
.map_err(|e| Error::XzCompress(name.to_owned(), e))?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
@@ -339,7 +383,7 @@ impl MagiskRootPatcher {
|
||||
new_path.extend(b".xz");
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
let buf = Self::xz_compress(&new_path, reader, cancel_signal)?;
|
||||
new_data = Some(CpioEntryData::Data(buf));
|
||||
}
|
||||
}
|
||||
@@ -381,7 +425,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
_cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let mut targets = vec![];
|
||||
|
||||
if boot_images.contains_key("init_boot") {
|
||||
@@ -394,9 +438,11 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
}
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let zip_reader =
|
||||
File::open(&self.apk_path).map_err(|e| Error::File(self.apk_path.clone(), e))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(zip_reader)).map_err(Error::Zip)?;
|
||||
let zip_reader = File::open(&self.apk_path)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(self.apk_path.clone(), e))?;
|
||||
let mut zip =
|
||||
ZipArchive::new(zip_reader).map_err(|e| Error::ZipOpen(self.apk_path.clone(), e))?;
|
||||
|
||||
// Load the first ramdisk. If it doesn't exist, we have to generate one
|
||||
// from scratch.
|
||||
@@ -427,9 +473,11 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
{
|
||||
let mut zip_entry = zip
|
||||
.by_name(Self::ZIP_MAGISKINIT)
|
||||
.map_err(|e| Error::ZipEntryName(Self::ZIP_MAGISKINIT.to_owned(), e))?;
|
||||
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_MAGISKINIT, e))?;
|
||||
let mut data = vec![];
|
||||
zip_entry.read_to_end(&mut data)?;
|
||||
zip_entry
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_MAGISKINIT, e))?;
|
||||
|
||||
entries.push(CpioEntry::new_file(
|
||||
b"init",
|
||||
@@ -471,8 +519,8 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
for (source, target) in xz_files {
|
||||
let reader = zip
|
||||
.by_name(source)
|
||||
.map_err(|e| Error::ZipEntryName(source.to_owned(), e))?;
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
.map_err(|e| Error::ZipEntryOpen(source, e))?;
|
||||
let buf = Self::xz_compress(source.as_bytes(), reader, cancel_signal)?;
|
||||
|
||||
entries.push(CpioEntry::new_file(target, 0o644, CpioEntryData::Data(buf)));
|
||||
}
|
||||
@@ -497,10 +545,9 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
magisk_config.push_str("RECOVERYMODE=false\n");
|
||||
|
||||
if Self::VER_PREINIT_DEVICE.contains(&self.version) {
|
||||
magisk_config.push_str(&format!(
|
||||
"PREINITDEVICE={}\n",
|
||||
self.preinit_device.as_ref().unwrap(),
|
||||
));
|
||||
if let Some(device) = &self.preinit_device {
|
||||
writeln!(&mut magisk_config, "PREINITDEVICE={device}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Magisk normally saves the original SHA1 digest in its config file. It
|
||||
@@ -510,7 +557,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
magisk_config.push_str("SHA1=0000000000000000000000000000000000000000\n");
|
||||
|
||||
if Self::VER_RANDOM_SEED.contains(&self.version) {
|
||||
magisk_config.push_str(&format!("RANDOMSEED={:#x}\n", self.random_seed));
|
||||
writeln!(&mut magisk_config, "RANDOMSEED={:#x}", self.random_seed).unwrap();
|
||||
}
|
||||
|
||||
trace!("Magisk config: {magisk_config:?}");
|
||||
@@ -523,7 +570,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
|
||||
// Repack ramdisk.
|
||||
cpio::sort(&mut entries);
|
||||
cpio::assign_inodes(&mut entries, false)?;
|
||||
cpio::assign_inodes(&mut entries, false).map_err(Error::RamdiskSaveCpio)?;
|
||||
let new_ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
|
||||
match boot_image {
|
||||
@@ -590,18 +637,22 @@ impl OtaCertPatcher {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(Error::Zip)?;
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(|e| {
|
||||
Error::ZipOpen(str::from_utf8(Self::OTACERTS_PATH).unwrap().into(), e)
|
||||
})?;
|
||||
|
||||
for index in 0..zip.len() {
|
||||
let zip_entry = zip
|
||||
.by_index(index)
|
||||
.map_err(|e| Error::ZipEntryIndex(index, e))?;
|
||||
.map_err(|e| Error::ZipIndexOpen(index, e))?;
|
||||
if !zip_entry.name().ends_with(".x509.pem") {
|
||||
debug!("Skipping invalid entry path: {}", zip_entry.name());
|
||||
continue;
|
||||
}
|
||||
|
||||
let certificate = crypto::read_pem_cert(zip_entry)?;
|
||||
let path = PathBuf::from(zip_entry.name());
|
||||
let certificate =
|
||||
crypto::read_pem_cert(&path, zip_entry).map_err(Error::OtaCertLoad)?;
|
||||
certificates.push(certificate);
|
||||
}
|
||||
}
|
||||
@@ -610,7 +661,6 @@ impl OtaCertPatcher {
|
||||
}
|
||||
|
||||
fn patch_ramdisk(
|
||||
&self,
|
||||
ramdisk: &mut Vec<u8>,
|
||||
zip: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -640,10 +690,10 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let mut targets = vec![];
|
||||
|
||||
'outer: for (name, info) in boot_images {
|
||||
'outer: for (&name, info) in boot_images {
|
||||
let ramdisks = match &info.boot_image {
|
||||
BootImage::V0Through2(b) => slice::from_ref(&b.ramdisk),
|
||||
BootImage::V3Through4(b) => slice::from_ref(&b.ramdisk),
|
||||
@@ -655,9 +705,10 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)
|
||||
.map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
if entries.iter().any(|e| e.path == Self::OTACERTS_PATH) {
|
||||
targets.push(*name);
|
||||
targets.push(name);
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
@@ -673,7 +724,8 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
BootImage::VendorV3Through4(b) => &mut b.ramdisks,
|
||||
};
|
||||
|
||||
let new_zip = otacert::create_zip(&self.cert, OtaCertBuildFlags::empty())?;
|
||||
let new_zip = otacert::create_zip(&self.cert, OtaCertBuildFlags::empty())
|
||||
.map_err(Error::OtaCertZip)?;
|
||||
trace!("Generated new {} byte otacerts.zip", new_zip.len());
|
||||
|
||||
for ramdisk in ramdisks {
|
||||
@@ -681,7 +733,7 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
if self.patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
|
||||
if Self::patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -719,7 +771,8 @@ impl DsuPubKeyPatcher {
|
||||
entries.push(CpioEntry::new_directory(Self::DSU_KEYS_PATH, 0o755));
|
||||
}
|
||||
|
||||
let data = CpioEntryData::Data(avb::encode_public_key(&self.key)?);
|
||||
let binary_key = avb::encode_public_key(&self.key).map_err(Error::AvbEncodeKey)?;
|
||||
let data = CpioEntryData::Data(binary_key);
|
||||
|
||||
if let Some(e) = entries
|
||||
.iter_mut()
|
||||
@@ -745,11 +798,11 @@ impl BootImagePatch for DsuPubKeyPatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let mut dsu_keys_targets = vec![];
|
||||
let mut first_stage_targets = vec![];
|
||||
|
||||
'outer: for (name, info) in boot_images {
|
||||
'outer: for (&name, info) in boot_images {
|
||||
let ramdisks = match &info.boot_image {
|
||||
BootImage::V0Through2(b) => slice::from_ref(&b.ramdisk),
|
||||
BootImage::V3Through4(b) => slice::from_ref(&b.ramdisk),
|
||||
@@ -761,15 +814,16 @@ impl BootImagePatch for DsuPubKeyPatcher {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)
|
||||
.map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
let mut found = false;
|
||||
|
||||
for entry in entries {
|
||||
if entry.path == Self::DSU_KEYS_PATH {
|
||||
dsu_keys_targets.push(*name);
|
||||
dsu_keys_targets.push(name);
|
||||
found = true;
|
||||
} else if entry.path == Self::FIRST_STAGE_PATH {
|
||||
first_stage_targets.push(*name);
|
||||
first_stage_targets.push(name);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
@@ -784,7 +838,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
|
||||
// Most builds trust as least one DSU key. For these builds, add the
|
||||
// user's key to the same directory.
|
||||
if dsu_keys_targets.len() > 1 {
|
||||
return Err(Error::Validation(format!(
|
||||
return Err(TargetsError::TargetValidation(format!(
|
||||
"DSU keys found in more than one boot image: {dsu_keys_targets:?}",
|
||||
)));
|
||||
}
|
||||
@@ -794,7 +848,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
|
||||
// For builds that don't trust any DSU keys, pick the first boot
|
||||
// image that contains a first stage ramdisk directory.
|
||||
if !first_stage_targets.is_empty() {
|
||||
first_stage_targets.sort();
|
||||
first_stage_targets.sort_unstable();
|
||||
first_stage_targets.resize(1, "");
|
||||
}
|
||||
|
||||
@@ -853,19 +907,22 @@ impl PrepatchedImagePatcher {
|
||||
}
|
||||
|
||||
fn load_prepatched_image(&self) -> Result<BootImage> {
|
||||
let raw_reader =
|
||||
File::open(&self.prepatched).map_err(|e| Error::File(self.prepatched.clone(), e))?;
|
||||
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))?;
|
||||
let reader = File::open(&self.prepatched)
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::FileOpen(self.prepatched.clone(), e))?;
|
||||
|
||||
Ok(boot_image)
|
||||
BootImage::from_reader(reader).map_err(Error::BootImageLoad)
|
||||
}
|
||||
|
||||
fn get_kmi_version(kernel: &[u8]) -> Result<Option<String>> {
|
||||
let mut decompressed = vec![];
|
||||
{
|
||||
let raw_reader = Cursor::new(kernel);
|
||||
let mut reader = CompressedReader::new(raw_reader, true)?;
|
||||
reader.read_to_end(&mut decompressed)?;
|
||||
let mut reader =
|
||||
CompressedReader::new(raw_reader, true).map_err(Error::KernelLoadCompression)?;
|
||||
reader
|
||||
.read_to_end(&mut decompressed)
|
||||
.map_err(Error::KernelRead)?;
|
||||
}
|
||||
|
||||
let regex = Regex::new(Self::VERSION_REGEX).unwrap();
|
||||
@@ -897,8 +954,10 @@ impl BootImagePatch for PrepatchedImagePatcher {
|
||||
&self,
|
||||
boot_images: &HashMap<&'a str, BootImageInfo>,
|
||||
_cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<&'a str>> {
|
||||
let prepatched_image = self.load_prepatched_image()?;
|
||||
) -> TargetsResult<Vec<&'a str>> {
|
||||
let prepatched_image = self
|
||||
.load_prepatched_image()
|
||||
.map_err(|e| TargetsError::Load("prepatched".to_owned(), e))?;
|
||||
|
||||
let has_kernel = match prepatched_image {
|
||||
BootImage::V0Through2(b) => !b.kernel.is_empty(),
|
||||
@@ -1085,36 +1144,90 @@ impl BootImagePatch for PrepatchedImagePatcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn load_boot_image(reader: &mut dyn ReadSeek) -> Result<BootImageInfo> {
|
||||
let (header, footer, image_size) = avb::load_image(&mut *reader).map_err(Error::AvbLoad)?;
|
||||
let Some(footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
|
||||
let section_reader =
|
||||
SectionReader::new(reader, 0, footer.original_image_size).map_err(Error::BootImageSeek)?;
|
||||
let boot_image = BootImage::from_reader(section_reader).map_err(Error::BootImageLoad)?;
|
||||
|
||||
let info = BootImageInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
boot_image,
|
||||
};
|
||||
|
||||
trace!("Loaded {image_size} byte boot image");
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn save_boot_image(
|
||||
writer: &mut dyn WriteSeek,
|
||||
info: &mut BootImageInfo,
|
||||
key: &RsaSigningKey,
|
||||
) -> Result<()> {
|
||||
let AppendedDescriptorMut::Hash(descriptor) = info
|
||||
.header
|
||||
.appended_descriptor_mut()
|
||||
.map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
return Err(Error::NoHashDescriptor);
|
||||
};
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
info.boot_image
|
||||
.to_writer(&mut hashing_writer)
|
||||
.map_err(Error::BootImageSave)?;
|
||||
let (writer, context) = hashing_writer.finish();
|
||||
|
||||
descriptor.image_size = writer.stream_position().map_err(Error::BootImageSeek)?;
|
||||
"sha256".clone_into(&mut descriptor.hash_algorithm);
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
|
||||
if !info.header.public_key.is_empty() {
|
||||
debug!("Signing boot image");
|
||||
info.header
|
||||
.set_algo_for_key(key)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
info.header.sign(key).map_err(Error::AvbUpdate)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(
|
||||
writer,
|
||||
&info.header,
|
||||
&mut info.footer,
|
||||
Some(info.image_size),
|
||||
)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_boot_images<'a>(
|
||||
names: &[&'a str],
|
||||
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
) -> Result<HashMap<&'a str, BootImageInfo>> {
|
||||
) -> TargetsResult<HashMap<&'a str, BootImageInfo>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
names
|
||||
.par_iter()
|
||||
.map(|name| {
|
||||
.map(|&name| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
let mut reader = open_input(name)?;
|
||||
let mut reader =
|
||||
open_input(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
|
||||
let (header, footer, image_size) = avb::load_image(&mut reader)?;
|
||||
let Some(footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let info =
|
||||
load_boot_image(&mut reader).map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
|
||||
let section_reader = SectionReader::new(reader, 0, footer.original_image_size)?;
|
||||
let boot_image = BootImage::from_reader(section_reader)?;
|
||||
|
||||
let info = BootImageInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
boot_image,
|
||||
};
|
||||
|
||||
trace!("Loaded {image_size} byte boot image: {name}");
|
||||
|
||||
Ok((*name, info))
|
||||
Ok((name, info))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1131,9 +1244,14 @@ pub fn patch_boot_images<'a>(
|
||||
key: &RsaSigningKey,
|
||||
patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<HashSet<&'a str>> {
|
||||
) -> TargetsResult<HashSet<&'a str>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
if patchers.is_empty() {
|
||||
debug!("Skip loading boot images; nothing to patch");
|
||||
return Ok(HashSet::new());
|
||||
}
|
||||
|
||||
// Preparse all images. Some patchers need to inspect every candidate.
|
||||
let mut images = load_boot_images(names, open_input)?;
|
||||
|
||||
@@ -1145,14 +1263,14 @@ pub fn patch_boot_images<'a>(
|
||||
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
|
||||
p.find_targets(&images, cancel_signal).and_then(|targets| {
|
||||
if targets.is_empty() {
|
||||
Err(Error::NoTargets(p.patcher_name()))
|
||||
Err(TargetsError::NoTargets(p.patcher_name()))
|
||||
} else {
|
||||
debug!("Found patcher targets: {targets:?}");
|
||||
Ok(targets)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
.collect::<TargetsResult<Vec<_>>>()?;
|
||||
|
||||
debug!("All patcher targets: {all_targets:?}");
|
||||
|
||||
@@ -1174,54 +1292,22 @@ pub fn patch_boot_images<'a>(
|
||||
// Apply all patches.
|
||||
groups
|
||||
.par_iter_mut()
|
||||
.try_for_each(|(_, (info, patchers))| -> Result<()> {
|
||||
.try_for_each(|(&name, (info, patchers))| -> TargetsResult<()> {
|
||||
patchers.iter().try_for_each(|p| {
|
||||
let _span =
|
||||
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
|
||||
p.patch(&mut info.boot_image, cancel_signal)
|
||||
.map_err(|e| TargetsError::Patch(name.to_owned(), e))
|
||||
})
|
||||
})?;
|
||||
|
||||
// Resign and write new images.
|
||||
groups
|
||||
.par_iter_mut()
|
||||
.map(|(name, (info, _))| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
groups.par_iter_mut().try_for_each(|(&name, (info, _))| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
let mut writer = open_output(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
|
||||
let AppendedDescriptorMut::Hash(descriptor) = info.header.appended_descriptor_mut()?
|
||||
else {
|
||||
return Err(Error::NoHashDescriptor);
|
||||
};
|
||||
save_boot_image(&mut writer, info, key).map_err(|e| TargetsError::Save(name.to_owned(), e))
|
||||
})?;
|
||||
|
||||
let writer = open_output(name)?;
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
info.boot_image.to_writer(&mut hashing_writer)?;
|
||||
let (mut writer, context) = hashing_writer.finish();
|
||||
|
||||
descriptor.image_size = writer.stream_position()?;
|
||||
"sha256".clone_into(&mut descriptor.hash_algorithm);
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
|
||||
if !info.header.public_key.is_empty() {
|
||||
debug!("Signing boot image");
|
||||
info.header.set_algo_for_key(key)?;
|
||||
info.header.sign(key)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(
|
||||
writer,
|
||||
&info.header,
|
||||
&mut info.footer,
|
||||
Some(info.image_size),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(groups.keys().cloned().collect())
|
||||
Ok(groups.keys().copied().collect())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
@@ -17,12 +17,10 @@ pub enum Error {
|
||||
ZipTooSmall(usize),
|
||||
#[error("New otacerts.zip is too large to fit in {0} bytes")]
|
||||
ZipTooLarge(usize),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("x509 DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("Failed to write otacerts zip")]
|
||||
ZipWrite(#[source] ZipError),
|
||||
#[error("Failed to write certificate to otacerts zip")]
|
||||
CertWrite(#[source] crypto::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -82,7 +80,8 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
};
|
||||
|
||||
let options = FileOptions::default().compression_method(compression_method);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
let name = "ota.x509.pem";
|
||||
writer.start_file(name, options).map_err(Error::ZipWrite)?;
|
||||
|
||||
let cert = if flags.is_empty() {
|
||||
Cow::Borrowed(cert)
|
||||
@@ -90,7 +89,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
let mut modified = cert.clone();
|
||||
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
|
||||
modified.signature = BitString::from_bytes(&[])?;
|
||||
// An empty ASN.1 bit string is always valid.
|
||||
modified.signature =
|
||||
BitString::from_bytes(&[]).expect("Empty ASN.1 bit string was invalid");
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
|
||||
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
|
||||
@@ -109,9 +110,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
Cow::Owned(modified)
|
||||
};
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &cert)?;
|
||||
crypto::write_pem_cert(Path::new(name), &mut writer, &cert).map_err(Error::CertWrite)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let raw_writer = writer.finish().map_err(Error::ZipWrite)?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
+47
-30
@@ -21,7 +21,7 @@ use crate::{
|
||||
ota,
|
||||
},
|
||||
patch::otacert,
|
||||
stream::{self, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
stream::{self, ReadFixedSizeExt, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -33,14 +33,16 @@ pub enum Error {
|
||||
NoFooter,
|
||||
#[error("No hash tree descriptor found in vbmeta header")]
|
||||
NoHashTreeDescriptor,
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("AVB error")]
|
||||
Avb(#[from] avb::Error),
|
||||
#[error("OTA certificate error")]
|
||||
OtaCert(#[from] otacert::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("{0:?} overflowed integer bounds during calculations")]
|
||||
IntOverflow(&'static str),
|
||||
#[error("Failed to update AVB header")]
|
||||
AvbUpdate(#[source] avb::Error),
|
||||
#[error("Failed to generate replacement otacerts zip")]
|
||||
OtaCertZip(#[source] otacert::Error),
|
||||
#[error("Failed to read image data")]
|
||||
ReadData(#[source] io::Error),
|
||||
#[error("Failed to write image data")]
|
||||
WriteData(#[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -119,11 +121,15 @@ pub fn patch_system_image(
|
||||
|
||||
let parent_span = Span::current();
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
|
||||
let (mut header, footer, image_size) =
|
||||
avb::load_image(input.reopen_boxed().map_err(Error::ReadData)?)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
let AppendedDescriptorMut::HashTree(descriptor) =
|
||||
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
@@ -133,17 +139,20 @@ pub fn patch_system_image(
|
||||
let modified_ranges = (0..num_chunks)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<Vec<Range<u64>>> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::ReadData)?;
|
||||
|
||||
let offset = chunk * CHUNK_SIZE;
|
||||
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(offset))?;
|
||||
reader.read_exact(&mut buf)?;
|
||||
let mut reader = input.reopen_boxed().map_err(Error::ReadData)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
.map_err(Error::ReadData)?;
|
||||
let buf = reader
|
||||
.read_vec_exact(size as usize)
|
||||
.map_err(Error::ReadData)?;
|
||||
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
let mut writer = output.reopen_boxed().map_err(Error::WriteData)?;
|
||||
let mut ranges = Vec::<Range<u64>>::new();
|
||||
|
||||
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
|
||||
@@ -155,14 +164,17 @@ pub fn patch_system_image(
|
||||
};
|
||||
|
||||
let zip_size = bounds_rel.end - bounds_rel.start;
|
||||
let new_zip = otacert::create_zip_with_size(certificate, zip_size)?;
|
||||
let new_zip = otacert::create_zip_with_size(certificate, zip_size)
|
||||
.map_err(Error::OtaCertZip)?;
|
||||
|
||||
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
stream::check_cancel(cancel_signal).map_err(Error::WriteData)?;
|
||||
|
||||
writer.seek(SeekFrom::Start(bounds.start))?;
|
||||
writer.write_all(&new_zip)?;
|
||||
writer
|
||||
.seek(SeekFrom::Start(bounds.start))
|
||||
.map_err(Error::WriteData)?;
|
||||
writer.write_all(&new_zip).map_err(Error::WriteData)?;
|
||||
|
||||
ranges.push(bounds);
|
||||
}
|
||||
@@ -196,18 +208,23 @@ pub fn patch_system_image(
|
||||
Some(modified_ranges.as_slice())
|
||||
};
|
||||
|
||||
descriptor.update(input, output, update_ranges, cancel_signal)?;
|
||||
descriptor
|
||||
.update(input, output, update_ranges, cancel_signal)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
debug!("Signing system image");
|
||||
header.set_algo_for_key(key)?;
|
||||
header.sign(key)?;
|
||||
header.set_algo_for_key(key).map_err(Error::AvbUpdate)?;
|
||||
header.sign(key).map_err(Error::AvbUpdate)?;
|
||||
}
|
||||
|
||||
let writer = output.reopen_boxed()?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))?;
|
||||
let writer = output.reopen_boxed().map_err(Error::WriteData)?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
let AppendedDescriptorMut::HashTree(descriptor) =
|
||||
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
|
||||
else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
};
|
||||
|
||||
@@ -215,15 +232,15 @@ pub fn patch_system_image(
|
||||
let hash_tree_end = descriptor
|
||||
.tree_offset
|
||||
.checked_add(descriptor.tree_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
|
||||
.ok_or(Error::IntOverflow("hash_tree_end"))?;
|
||||
let fec_data_end = descriptor
|
||||
.fec_offset
|
||||
.checked_add(descriptor.fec_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
|
||||
.ok_or(Error::IntOverflow("fec_data_end"))?;
|
||||
let header_end = footer
|
||||
.vbmeta_offset
|
||||
.checked_add(footer.vbmeta_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
|
||||
.ok_or(Error::IntOverflow("avb_end"))?;
|
||||
let footer_start = image_size - Footer::SIZE as u64;
|
||||
|
||||
let other_ranges = util::merge_overlapping(&[
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#![allow(clippy::nursery)]
|
||||
#![allow(clippy::pedantic)]
|
||||
|
||||
pub mod build {
|
||||
pub mod tools {
|
||||
pub mod releasetools {
|
||||
|
||||
+19
-91
@@ -10,7 +10,6 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
|
||||
@@ -122,69 +121,26 @@ impl<W: Write> WriteZerosExt for W {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for readers to read strings.
|
||||
pub trait ReadStringExt {
|
||||
/// Read exact sized string.
|
||||
fn read_string_exact(&mut self, size: usize) -> io::Result<String>;
|
||||
/// Extensions for readers to read fixed-size buffers.
|
||||
pub trait ReadFixedSizeExt {
|
||||
/// Read fixed-size array.
|
||||
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]>;
|
||||
|
||||
/// Read string with maximum size and trim trailing zeros.
|
||||
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String>;
|
||||
/// Read fixed-sized [`Vec`].
|
||||
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
impl<R: Read> ReadStringExt for R {
|
||||
fn read_string_exact(&mut self, size: usize) -> io::Result<String> {
|
||||
impl<R: Read> ReadFixedSizeExt for R {
|
||||
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]> {
|
||||
let mut buf = [0u8; N];
|
||||
self.read_exact(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>> {
|
||||
let mut buf = vec![0u8; size];
|
||||
self.read_exact(&mut buf)?;
|
||||
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String> {
|
||||
let mut buf = vec![0u8; max_size];
|
||||
self.read_exact(&mut buf)?;
|
||||
|
||||
let after_last_non_zero = buf
|
||||
.iter()
|
||||
.rev()
|
||||
.position(|&b| b != 0)
|
||||
.map_or(0, |i| buf.len() - i);
|
||||
buf.resize(after_last_non_zero, 0);
|
||||
buf.shrink_to_fit();
|
||||
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for writers to write strings.
|
||||
pub trait WriteStringExt {
|
||||
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl<W: Write> WriteStringExt for W {
|
||||
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()> {
|
||||
if data.len() > max_size {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("{data:?} exceeds maximum size of {max_size} bytes"),
|
||||
));
|
||||
}
|
||||
|
||||
self.write_all(data.as_bytes())?;
|
||||
|
||||
let num_zeros = (max_size - data.len()) as u64;
|
||||
self.write_zeros_exact(num_zeros)?;
|
||||
|
||||
Ok(())
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,13 +153,13 @@ pub trait Reopen: Sized {
|
||||
|
||||
impl<R: Read + Reopen> Reopen for BufReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufReader::new(self.get_ref().reopen()?))
|
||||
Ok(Self::new(self.get_ref().reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Reopen> Reopen for BufWriter<W> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(BufWriter::new(self.get_ref().reopen()?))
|
||||
Ok(Self::new(self.get_ref().reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -543,9 +499,7 @@ pub struct SharedCursor {
|
||||
|
||||
impl SharedCursor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,7 +641,7 @@ mod tests {
|
||||
|
||||
use super::{
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
|
||||
ReadStringExt, Reopen, SectionReader, SharedCursor, WriteStringExt, WriteZerosExt,
|
||||
Reopen, SectionReader, SharedCursor, WriteZerosExt,
|
||||
};
|
||||
|
||||
const FOOBAR_SHA256: [u8; 32] = [
|
||||
@@ -724,32 +678,6 @@ mod tests {
|
||||
assert_eq!(&writer.into_inner(), b"\0\0foo\0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_string() {
|
||||
let mut reader = Cursor::new(b"foo\0\0bar\0\0");
|
||||
|
||||
assert_eq!(reader.read_string_exact(3).unwrap(), "foo");
|
||||
assert_eq!(reader.read_string_exact(0).unwrap(), "");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
assert_eq!(reader.read_string_padded(3).unwrap(), "foo");
|
||||
|
||||
reader.rewind().unwrap();
|
||||
assert_eq!(reader.read_string_padded(10).unwrap(), "foo\0\0bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_string() {
|
||||
let mut writer = Cursor::new([0xffu8; 8]);
|
||||
|
||||
writer.write_string_padded("foobar", 8).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobar\0\0");
|
||||
|
||||
writer.rewind().unwrap();
|
||||
writer.write_string_padded("foobarhi", 8).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobarhi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn counting_reader() {
|
||||
let raw_reader = Cursor::new(b"foobar");
|
||||
|
||||
+330
-17
@@ -1,9 +1,17 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt, mem,
|
||||
ops::{
|
||||
Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
|
||||
},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use num_traits::PrimInt;
|
||||
use num_traits::{NumCast, PrimInt};
|
||||
use thiserror::Error;
|
||||
|
||||
pub const ZEROS: [u8; 16384] = [0u8; 16384];
|
||||
|
||||
@@ -21,6 +29,263 @@ impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores a precomputed [`Debug`] string.
|
||||
#[derive(Clone)]
|
||||
pub struct DebugString(String);
|
||||
|
||||
impl DebugString {
|
||||
pub fn new(value: impl fmt::Debug) -> Self {
|
||||
Self(format!("{value:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DebugString {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Hash, PartialEq, Eq)]
|
||||
pub enum AnyRange<T> {
|
||||
Range(Range<T>),
|
||||
RangeFrom(RangeFrom<T>),
|
||||
RangeFull(RangeFull),
|
||||
RangeInclusive(RangeInclusive<T>),
|
||||
RangeTo(RangeTo<T>),
|
||||
RangeToInclusive(RangeToInclusive<T>),
|
||||
}
|
||||
|
||||
impl<T> AnyRange<T> {
|
||||
pub fn with_bounds(start: Bound<T>, end: Bound<T>) -> Option<Self> {
|
||||
let result = match (start, end) {
|
||||
(Bound::Included(s), Bound::Excluded(e)) => Self::Range(s..e),
|
||||
(Bound::Included(s), Bound::Unbounded) => Self::RangeFrom(s..),
|
||||
(Bound::Unbounded, Bound::Unbounded) => Self::RangeFull(..),
|
||||
(Bound::Included(s), Bound::Included(e)) => Self::RangeInclusive(s..=e),
|
||||
(Bound::Unbounded, Bound::Excluded(e)) => Self::RangeTo(..e),
|
||||
(Bound::Unbounded, Bound::Included(e)) => Self::RangeToInclusive(..=e),
|
||||
(Bound::Excluded(_), _) => return None,
|
||||
};
|
||||
|
||||
Some(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialOrd<T>> AnyRange<T> {
|
||||
pub fn contains<U>(&self, item: &U) -> bool
|
||||
where
|
||||
T: PartialOrd<U>,
|
||||
U: ?Sized + PartialOrd<T>,
|
||||
{
|
||||
<Self as RangeBounds<T>>::contains(self, item)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for AnyRange<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Range(r) => r.fmt(f),
|
||||
Self::RangeFrom(r) => r.fmt(f),
|
||||
Self::RangeFull(r) => r.fmt(f),
|
||||
Self::RangeInclusive(r) => r.fmt(f),
|
||||
Self::RangeTo(r) => r.fmt(f),
|
||||
Self::RangeToInclusive(r) => r.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RangeBounds<T> for AnyRange<T> {
|
||||
fn start_bound(&self) -> Bound<&T> {
|
||||
match self {
|
||||
Self::Range(r) => r.start_bound(),
|
||||
Self::RangeFrom(r) => r.start_bound(),
|
||||
Self::RangeFull(r) => r.start_bound(),
|
||||
Self::RangeInclusive(r) => r.start_bound(),
|
||||
Self::RangeTo(r) => r.start_bound(),
|
||||
Self::RangeToInclusive(r) => r.start_bound(),
|
||||
}
|
||||
}
|
||||
|
||||
fn end_bound(&self) -> Bound<&T> {
|
||||
match self {
|
||||
Self::Range(r) => r.end_bound(),
|
||||
Self::RangeFrom(r) => r.end_bound(),
|
||||
Self::RangeFull(r) => r.end_bound(),
|
||||
Self::RangeInclusive(r) => r.end_bound(),
|
||||
Self::RangeTo(r) => r.end_bound(),
|
||||
Self::RangeToInclusive(r) => r.end_bound(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Range<T>> for AnyRange<T> {
|
||||
fn from(value: Range<T>) -> Self {
|
||||
Self::Range(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeFrom<T>> for AnyRange<T> {
|
||||
fn from(value: RangeFrom<T>) -> Self {
|
||||
Self::RangeFrom(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeFull> for AnyRange<T> {
|
||||
fn from(value: RangeFull) -> Self {
|
||||
Self::RangeFull(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeInclusive<T>> for AnyRange<T> {
|
||||
fn from(value: RangeInclusive<T>) -> Self {
|
||||
Self::RangeInclusive(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeTo<T>> for AnyRange<T> {
|
||||
fn from(value: RangeTo<T>) -> Self {
|
||||
Self::RangeTo(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<RangeToInclusive<T>> for AnyRange<T> {
|
||||
fn from(value: RangeToInclusive<T>) -> Self {
|
||||
Self::RangeToInclusive(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-generic type that can represent any 64-bit or smaller primitive
|
||||
/// integer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum LargeInt {
|
||||
Signed(i64),
|
||||
Unsigned(u64),
|
||||
}
|
||||
|
||||
impl fmt::Display for LargeInt {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Signed(n) => n.fmt(f),
|
||||
Self::Unsigned(n) => n.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-generic type that can represent any 64-bit or smaller primitive
|
||||
/// integer range.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum LargeIntRange {
|
||||
Signed(AnyRange<i64>),
|
||||
Unsigned(AnyRange<u64>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for LargeIntRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Signed(r) => r.fmt(f),
|
||||
Self::Unsigned(r) => r.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error returned when a value is not within a specific range.
|
||||
#[derive(Clone, Debug, Error)]
|
||||
#[error("Integer value {value} not in bounds: {range:?}")]
|
||||
pub struct OutOfBoundsError {
|
||||
value: LargeInt,
|
||||
range: LargeIntRange,
|
||||
}
|
||||
|
||||
/// Verify that `value` is within `bounds` and then return `value` if it is.
|
||||
pub fn check_bounds<T: PrimInt>(
|
||||
value: T,
|
||||
range: impl Into<AnyRange<T>>,
|
||||
) -> Result<T, OutOfBoundsError> {
|
||||
const {
|
||||
assert!(
|
||||
mem::size_of::<T>() <= 8,
|
||||
"Integer must be 64 bits or smaller"
|
||||
);
|
||||
}
|
||||
|
||||
let range = range.into();
|
||||
|
||||
if !range.contains(&value) {
|
||||
let value = if T::min_value() != T::zero() {
|
||||
LargeInt::Signed(NumCast::from(value).unwrap())
|
||||
} else {
|
||||
LargeInt::Unsigned(NumCast::from(value).unwrap())
|
||||
};
|
||||
|
||||
let range = if T::min_value() != T::zero() {
|
||||
let start = match range.start_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
let end = match range.end_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
LargeIntRange::Signed(AnyRange::with_bounds(start, end).unwrap())
|
||||
} else {
|
||||
let start = match range.start_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
let end = match range.end_bound() {
|
||||
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
|
||||
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
|
||||
LargeIntRange::Unsigned(AnyRange::with_bounds(start, end).unwrap())
|
||||
};
|
||||
|
||||
return Err(OutOfBoundsError { value, range });
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Try to cast `value` to primitive integer type `T`. If it does not fit, the
|
||||
/// error will indicate the valid range of values.
|
||||
pub fn try_cast<T: PrimInt, V: PrimInt>(value: V) -> Result<T, OutOfBoundsError> {
|
||||
const {
|
||||
assert!(
|
||||
mem::size_of::<T>() <= 8,
|
||||
"Integer must be 64 bits or smaller"
|
||||
);
|
||||
}
|
||||
|
||||
NumCast::from(value).ok_or_else(|| {
|
||||
let value = if V::min_value() != V::zero() {
|
||||
LargeInt::Signed(NumCast::from(value).unwrap())
|
||||
} else {
|
||||
LargeInt::Unsigned(NumCast::from(value).unwrap())
|
||||
};
|
||||
|
||||
let range = if T::min_value() != T::zero() {
|
||||
let min = NumCast::from(T::min_value()).unwrap();
|
||||
let max = NumCast::from(T::max_value()).unwrap();
|
||||
|
||||
LargeIntRange::Signed((min..=max).into())
|
||||
} else {
|
||||
let min = NumCast::from(T::min_value()).unwrap();
|
||||
let max = NumCast::from(T::max_value()).unwrap();
|
||||
|
||||
LargeIntRange::Unsigned((min..=max).into())
|
||||
};
|
||||
|
||||
OutOfBoundsError { value, range }
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a byte slice is all zeros.
|
||||
pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
while !buf.is_empty() {
|
||||
@@ -117,25 +382,73 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_any_range() {
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Excluded(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Unbounded).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..));
|
||||
|
||||
let range = AnyRange::<i32>::with_bounds(Bound::Unbounded, Bound::Unbounded).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Included(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(0..=1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Excluded(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..1));
|
||||
|
||||
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Included(1)).unwrap();
|
||||
assert_eq!(range, AnyRange::from(..=1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_bounds() {
|
||||
check_bounds(i64::MIN, ..).unwrap();
|
||||
check_bounds(i64::MAX, ..).unwrap();
|
||||
check_bounds(u64::MIN, ..).unwrap();
|
||||
check_bounds(u64::MAX, ..).unwrap();
|
||||
check_bounds(0, -1..=1).unwrap();
|
||||
|
||||
let err = check_bounds(i8::MAX, 0..=0).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Signed(127));
|
||||
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(0..=0)));
|
||||
|
||||
let err = check_bounds(u8::MAX, 0..=0).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Unsigned(255));
|
||||
assert_eq!(err.range, LargeIntRange::Unsigned(AnyRange::from(0..=0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_cast() {
|
||||
let value: u8 = try_cast(255u16).unwrap();
|
||||
assert_eq!(value, 255);
|
||||
|
||||
let err = try_cast::<i8, _>(256u16).unwrap_err();
|
||||
assert_eq!(err.value, LargeInt::Unsigned(256));
|
||||
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(-128..=127)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_overlaps() {
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
|
||||
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
|
||||
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
|
||||
assert!(!ranges_overlaps(&[0..4], &(0..0)));
|
||||
assert!(ranges_overlaps(&[0..4], &(0..4)));
|
||||
assert!(ranges_overlaps(&[0..4], &(1..4)));
|
||||
assert!(ranges_overlaps(&[0..4], &(0..3)));
|
||||
assert!(!ranges_overlaps(&[0..4], &(4..5)));
|
||||
assert!(ranges_overlaps(&[5..8], &(5..9)));
|
||||
assert!(ranges_overlaps(&[5..8], &(4..8)));
|
||||
assert!(ranges_overlaps(&[5..8], &(4..9)));
|
||||
assert!(ranges_overlaps(&[0..4, 5..8], &(4..5)));
|
||||
assert!(ranges_overlaps(&[0..4, 5..8], &(0..9)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ranges_contains() {
|
||||
assert_eq!(ranges_contains(&[0..4], &0), true);
|
||||
assert_eq!(ranges_contains(&[0..4], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
|
||||
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
|
||||
assert!(ranges_contains(&[0..4], &0));
|
||||
assert!(!ranges_contains(&[0..4], &4));
|
||||
assert!(!ranges_contains(&[0..4, 5..8], &4));
|
||||
assert!(ranges_contains(&[0..4, 5..8], &6));
|
||||
}
|
||||
}
|
||||
|
||||
+24
-24
@@ -118,7 +118,7 @@ fn round_trip_root_image() {
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
release_string: repeat_str("MaxLength", 47),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
@@ -136,11 +136,11 @@ fn round_trip_root_image() {
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xc4, 0xa5, 0xda, 0x3e, 0x09, 0xa2, 0xc8, 0x70, 0xcb, 0xf0, 0x96, 0x79, 0x0e, 0x1e,
|
||||
0x80, 0xae, 0x5e, 0x37, 0x81, 0x27, 0x24, 0xc3, 0x6c, 0xa9, 0x42, 0x9e, 0x2c, 0xb1,
|
||||
0x81, 0xad, 0xce, 0xee, 0x8d, 0x4f, 0x76, 0x45, 0x54, 0xc1, 0x31, 0x6a, 0xa7, 0x81,
|
||||
0x5c, 0x59, 0xa8, 0xe8, 0x76, 0xab, 0xed, 0x5b, 0x07, 0x07, 0x38, 0xdd, 0x09, 0x86,
|
||||
0x05, 0x39, 0x23, 0x2d, 0x7b, 0xcc, 0x57, 0x06,
|
||||
0x3b, 0x01, 0xf6, 0x04, 0x04, 0x6e, 0x6f, 0x60, 0x9c, 0xb0, 0x8b, 0x8a, 0x43, 0xf7,
|
||||
0x91, 0x2e, 0xc4, 0x1b, 0xc0, 0x7f, 0xa1, 0xe4, 0xe6, 0x59, 0x14, 0x08, 0xbe, 0x83,
|
||||
0xae, 0x0a, 0x0f, 0x0a, 0x4a, 0x15, 0x91, 0x0e, 0x4d, 0x18, 0x31, 0x48, 0x20, 0xe8,
|
||||
0x44, 0x62, 0x07, 0x98, 0x43, 0x30, 0xee, 0x2d, 0x20, 0x28, 0xc3, 0x94, 0xc6, 0x0e,
|
||||
0x86, 0xa3, 0xa7, 0x17, 0x36, 0xfd, 0x50, 0x7c,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -186,7 +186,7 @@ fn round_trip_appended_hash_image() {
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
release_string: repeat_str("MaxLength", 47),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
@@ -235,11 +235,11 @@ fn round_trip_appended_hash_image() {
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0x09, 0x98, 0x0c, 0x9d, 0x11, 0x50, 0xde, 0xb1, 0x55, 0x3b, 0x00, 0x76, 0xbe, 0x25,
|
||||
0xfd, 0xe6, 0x46, 0x22, 0xbd, 0x9a, 0x05, 0x86, 0xea, 0x07, 0x4d, 0x8f, 0x7b, 0x15,
|
||||
0x36, 0x20, 0x0d, 0xf0, 0x7e, 0x96, 0xd2, 0x58, 0xde, 0xf2, 0xa6, 0x91, 0x6d, 0x01,
|
||||
0x7b, 0x03, 0x96, 0x70, 0xf8, 0x3b, 0x76, 0x74, 0xf0, 0xbf, 0x47, 0xe0, 0xd2, 0xd4,
|
||||
0x5d, 0xbf, 0xb7, 0x9c, 0xf5, 0xf8, 0xaf, 0x3c,
|
||||
0x91, 0x38, 0x61, 0xc0, 0x68, 0x2a, 0x8b, 0xd8, 0x01, 0xa6, 0xe4, 0x4c, 0x1d, 0x27,
|
||||
0x93, 0x1b, 0xa4, 0x63, 0xd1, 0xbb, 0xf1, 0x64, 0x05, 0xf2, 0xa1, 0xa0, 0xb3, 0x35,
|
||||
0xe1, 0xc5, 0xac, 0x4f, 0x98, 0xb3, 0x0a, 0xed, 0xfc, 0xee, 0xa2, 0x6a, 0x77, 0xf4,
|
||||
0xe5, 0x69, 0xa0, 0xcd, 0x7a, 0xd1, 0xfe, 0x1d, 0x07, 0xd1, 0x25, 0xc6, 0x22, 0xe0,
|
||||
0x25, 0xcb, 0xe9, 0x75, 0x50, 0xe4, 0xae, 0x59,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -294,7 +294,7 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
release_string: repeat_str("MaxLength", 47),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
@@ -343,11 +343,11 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xb5, 0x56, 0x65, 0x81, 0x5a, 0x16, 0x65, 0xa9, 0xa6, 0xc6, 0x9e, 0x41, 0x89, 0x9f,
|
||||
0xe9, 0xbc, 0xea, 0x59, 0x4d, 0x14, 0x8a, 0x9e, 0x2b, 0x13, 0xa0, 0x3a, 0x8e, 0xd4,
|
||||
0x59, 0xcd, 0x74, 0xe7, 0x99, 0xbd, 0xa3, 0x58, 0x4b, 0x84, 0xf2, 0x04, 0xe2, 0x12,
|
||||
0x48, 0xfe, 0x4f, 0x67, 0x1f, 0x2a, 0xaa, 0x22, 0x51, 0x19, 0x83, 0x95, 0xa8, 0x03,
|
||||
0xf5, 0x87, 0x12, 0x05, 0x8e, 0x14, 0xd9, 0xbd
|
||||
0x92, 0xdd, 0x4d, 0xc5, 0xb0, 0x5b, 0x4f, 0x65, 0x97, 0x5a, 0x72, 0x66, 0xde, 0x82,
|
||||
0xc2, 0x2f, 0x33, 0x86, 0x8b, 0x65, 0x67, 0x80, 0x1d, 0xca, 0xd6, 0x2c, 0xfc, 0xca,
|
||||
0xaf, 0x4c, 0x56, 0x64, 0x3a, 0xd1, 0x06, 0x01, 0xda, 0x2e, 0x05, 0x67, 0xd1, 0x01,
|
||||
0xe3, 0xcb, 0x7b, 0x1e, 0xeb, 0x05, 0x89, 0xeb, 0x80, 0xcc, 0x17, 0x0c, 0x24, 0x73,
|
||||
0x0d, 0xcb, 0x36, 0xfa, 0x17, 0xbd, 0x20, 0x7e,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -401,7 +401,7 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
release_string: repeat_str("MaxLength", 47),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
@@ -450,11 +450,11 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xd6, 0x69, 0x19, 0x6a, 0x36, 0xc8, 0x1c, 0xe9, 0xc4, 0x85, 0xbe, 0xff, 0x43, 0xb1,
|
||||
0x9f, 0xd4, 0x1d, 0x6c, 0xf9, 0xd2, 0xf3, 0xa6, 0x5f, 0x66, 0x41, 0xd5, 0xf3, 0xfd,
|
||||
0x28, 0xdb, 0x14, 0x67, 0xc6, 0xa8, 0xef, 0xc4, 0xd4, 0x67, 0x6c, 0xb8, 0x66, 0xbb,
|
||||
0x56, 0x5a, 0x4a, 0xf5, 0xd8, 0x92, 0x7c, 0x42, 0xbc, 0x47, 0xdb, 0x94, 0x38, 0x15,
|
||||
0x4b, 0x2d, 0xd0, 0x28, 0x1f, 0xd1, 0x45, 0xa9,
|
||||
0xcf, 0x6b, 0x90, 0xcf, 0x77, 0x76, 0x62, 0x12, 0xc2, 0x22, 0xe6, 0xd5, 0x5b, 0xab,
|
||||
0x82, 0xd8, 0x6c, 0x93, 0xa3, 0x35, 0x5b, 0x77, 0xe0, 0x38, 0x12, 0x48, 0x90, 0x0c,
|
||||
0xee, 0xbf, 0x95, 0x31, 0xff, 0xc7, 0xf5, 0xb9, 0x4f, 0x18, 0x57, 0x46, 0x37, 0xbb,
|
||||
0xce, 0x7b, 0xa7, 0x26, 0x18, 0x5a, 0x3c, 0x41, 0xb2, 0x2e, 0xb7, 0x86, 0x51, 0xdc,
|
||||
0xf6, 0x26, 0x86, 0xf3, 0xc7, 0x96, 0x23, 0xed,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"CC0-1.0",
|
||||
"bzip2-1.0.6",
|
||||
"GPL-3.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-DFS-2016",
|
||||
"Unicode-3.0",
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
@@ -69,6 +69,6 @@ bypass = [
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/bzip2-rs",
|
||||
"https://github.com/chenxiaolong/zip",
|
||||
"https://github.com/trifectatechfoundation/bzip2-rs",
|
||||
]
|
||||
|
||||
@@ -32,3 +32,6 @@ default-features = false
|
||||
|
||||
[features]
|
||||
static = ["avbroot/static"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+3
-1
@@ -625,6 +625,7 @@ fn create_payload(
|
||||
version: None,
|
||||
merge_operations: vec![],
|
||||
estimate_cow_size: cow_estimate,
|
||||
estimate_op_count_max: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -648,6 +649,7 @@ fn create_payload(
|
||||
vabc_compression_param: profile.vabc_algo.map(|a| a.to_string()),
|
||||
cow_version: Some(2),
|
||||
vabc_feature_set: None,
|
||||
compression_factor: None,
|
||||
}),
|
||||
partial_update: None,
|
||||
apex_info: vec![],
|
||||
@@ -753,7 +755,7 @@ fn create_ota(
|
||||
|
||||
match path {
|
||||
ota::PATH_OTACERT => {
|
||||
crypto::write_pem_cert(&mut writer, cert_ota)
|
||||
crypto::write_pem_cert(Path::new(path), &mut writer, cert_ota)
|
||||
.with_context(|| format!("Failed to write entry: {path}"))?;
|
||||
}
|
||||
ota::PATH_PAYLOAD => {
|
||||
|
||||
@@ -16,3 +16,6 @@ publish = false
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
avbroot = { path = "../avbroot" }
|
||||
honggfuzz = "0.5.55"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -13,3 +13,6 @@ anyhow = "1.0.75"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
toml_edit = "0.22.9"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
Reference in New Issue
Block a user