mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 237f59fd9b | |||
| b0b00a4c54 | |||
| b9a70efb15 | |||
| aeef2936d8 | |||
| 3adab80893 | |||
| 4069429361 | |||
| 74193c1c9f | |||
| 0f8cef2daa | |||
| 8731df4504 | |||
| fda11b4262 | |||
| f0970aa80c | |||
| 4f0db5a7c4 | |||
| 57374e2e41 | |||
| f4e5c55fe9 | |||
| 850ce86388 | |||
| c16636a68c | |||
| 26812cd43a | |||
| 135cd320ce | |||
| 919f621833 | |||
| 2c1d86cd96 | |||
| 91074ae4c8 | |||
| a5c628b66f | |||
| 156c96d3d4 | |||
| 2460d1807d | |||
| 728d333951 | |||
| 39661a994d | |||
| d6b19b69f7 | |||
| 96806cba36 | |||
| 5c474a1e98 | |||
| 4cf546e5ca | |||
| 8f71b61b21 | |||
| fa8d9eb58c | |||
| 39afcf485f | |||
| a3eb8284c8 | |||
| e2708d39dd | |||
| a5f13826e7 | |||
| 963456c194 | |||
| 842a2feb88 | |||
| 8bb1f771af | |||
| ab51654dad | |||
| d75b87d3df | |||
| 73e0404662 | |||
| ebb4f18add | |||
| bee8d80df0 | |||
| fdb19e3b97 | |||
| cef29fd280 | |||
| 58155513af | |||
| 01cdd0b0d3 | |||
| e39d023855 | |||
| 05c86af298 | |||
| 759ed7fae5 | |||
| f6aac1c12f | |||
| e5b754c786 | |||
| d874921a69 | |||
| 390dce5f0c | |||
| 6d939bda25 | |||
| a40d6ea379 | |||
| 7d0bb378a6 | |||
| 3ed38b8d29 | |||
| 0717981d62 | |||
| 556f86e4df | |||
| c019ccad1c | |||
| 192e737dd3 | |||
| 339267149f | |||
| 21c2536759 | |||
| 185f02c209 | |||
| a8908d6d06 | |||
| 2683781737 | |||
| 44b90936bf | |||
| d7369e73e9 | |||
| bb5c97ea1b | |||
| ac95660e23 |
@@ -18,6 +18,8 @@ jobs:
|
||||
CARGO_TERM_COLOR: always
|
||||
# https://github.com/rust-lang/rust/issues/78210
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
# https://github.com/rust-lang/cargo/pull/15462
|
||||
RUSTDOCFLAGS: -C target-feature=+crt-static
|
||||
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
|
||||
ANDROID_API: ${{ matrix.artifact.android_api }}
|
||||
strategy:
|
||||
@@ -26,8 +28,12 @@ jobs:
|
||||
artifact:
|
||||
- os: ubuntu-latest
|
||||
name: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-24.04-arm
|
||||
name: aarch64-unknown-linux-gnu
|
||||
- os: windows-latest
|
||||
name: x86_64-pc-windows-msvc
|
||||
- os: windows-11-arm
|
||||
name: aarch64-pc-windows-msvc
|
||||
- os: macos-latest
|
||||
name: universal-apple-darwin
|
||||
targets:
|
||||
@@ -41,7 +47,7 @@ jobs:
|
||||
android_api: '31'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
@@ -83,7 +89,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
key: ${{ matrix.artifact.name }}
|
||||
|
||||
@@ -92,7 +98,7 @@ jobs:
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo android \
|
||||
clippy --release --workspace --features static \
|
||||
clippy --release --workspace \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -101,7 +107,7 @@ jobs:
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo android \
|
||||
build --release --workspace --features static \
|
||||
build --release --workspace \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -110,7 +116,7 @@ jobs:
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo android \
|
||||
test --release --workspace --features static \
|
||||
test --release --workspace \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -119,7 +125,7 @@ jobs:
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo android \
|
||||
run --release -p e2e --features static \
|
||||
run --release -p e2e \
|
||||
--target "${target}" \
|
||||
-- test -a -c e2e/e2e.toml
|
||||
done
|
||||
@@ -154,7 +160,7 @@ jobs:
|
||||
run: cp LICENSE README.md target/output/
|
||||
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
|
||||
path: |
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@30f817c6f72275c6d54dc744fbca09ebc958599f # v2.0.12
|
||||
uses: EmbarkStudios/cargo-deny-action@76cd80eb775d7bbbd2d80292136d74d39e1b4918 # v2.0.14
|
||||
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
|
||||
@@ -7,6 +7,47 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.23.3
|
||||
|
||||
* Add support for Magisk 30600 ([PR #529])
|
||||
* Update dependencies ([PR #530])
|
||||
|
||||
### Version 3.23.2
|
||||
|
||||
* Add prebuilt binaries for aarch64 GNU/Linux ([PR #523])
|
||||
* Add prebuilt binaries for aarch64 Windows ([PR #524])
|
||||
|
||||
### Version 3.23.1
|
||||
|
||||
* Add support for Magisk 30400 ([PR #521])
|
||||
|
||||
### Version 3.23.0
|
||||
|
||||
* Update dependencies and fix Rust 1.90 clippy lints ([PR #517], [PR #520])
|
||||
* Add support for Magisk 30300 ([PR #519])
|
||||
|
||||
### Version 3.22.0
|
||||
|
||||
* Fix CoW size estimates not being set when running `avbroot payload pack` ([PR #509])
|
||||
* Add support for only unpacking metadata in `avbroot {avb,boot,cpio,lp,payload} unpack` ([PR #510])
|
||||
* Specify minimum supported compiler version for building from source ([PR #511])
|
||||
* Update dependencies ([PR #512])
|
||||
|
||||
### Version 3.21.0
|
||||
|
||||
* Switch to using rawzip for zip parsing and writing ([PR #489])
|
||||
* Various code improvements ([PR #492], [PR #495], [PR #496], [PR #497], [PR #499], [PR #500], [PR #501], [PR #502], [PR #503], [PR #505])
|
||||
* Update dependencies ([PR #498])
|
||||
* Fix patching older OTAs with payloads using CoW v2 without the compression factor field ([Issue #493], [PR #504])
|
||||
|
||||
### Version 3.20.0
|
||||
|
||||
* Switch to using lzma-rust2 for XZ compression and decompression ([PR #483])
|
||||
* Remove cap-std and cap-tempfile dependencies ([PR #484])
|
||||
* Add new `avbroot avb verify-device` command to verify the signatures on the actual device ([Issue #482], [PR #485], [PR #486])
|
||||
* This is only available in the Android build of avbroot.
|
||||
* Update dependencies and fix new Rust 1.89 clippy lints ([PR #487])
|
||||
|
||||
### Version 3.19.0
|
||||
|
||||
* Allow verifying hashes of unsigned images with `avbroot avb verify` ([PR #477])
|
||||
@@ -392,6 +433,8 @@ Behind-the-scenes changes:
|
||||
[Issue #451]: https://github.com/chenxiaolong/avbroot/issues/451
|
||||
[Issue #469]: https://github.com/chenxiaolong/avbroot/issues/469
|
||||
[Issue #472]: https://github.com/chenxiaolong/avbroot/issues/472
|
||||
[Issue #482]: https://github.com/chenxiaolong/avbroot/issues/482
|
||||
[Issue #493]: https://github.com/chenxiaolong/avbroot/issues/493
|
||||
[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
|
||||
@@ -567,3 +610,33 @@ Behind-the-scenes changes:
|
||||
[PR #477]: https://github.com/chenxiaolong/avbroot/pull/477
|
||||
[PR #478]: https://github.com/chenxiaolong/avbroot/pull/478
|
||||
[PR #479]: https://github.com/chenxiaolong/avbroot/pull/479
|
||||
[PR #483]: https://github.com/chenxiaolong/avbroot/pull/483
|
||||
[PR #484]: https://github.com/chenxiaolong/avbroot/pull/484
|
||||
[PR #485]: https://github.com/chenxiaolong/avbroot/pull/485
|
||||
[PR #486]: https://github.com/chenxiaolong/avbroot/pull/486
|
||||
[PR #487]: https://github.com/chenxiaolong/avbroot/pull/487
|
||||
[PR #489]: https://github.com/chenxiaolong/avbroot/pull/489
|
||||
[PR #492]: https://github.com/chenxiaolong/avbroot/pull/492
|
||||
[PR #495]: https://github.com/chenxiaolong/avbroot/pull/495
|
||||
[PR #496]: https://github.com/chenxiaolong/avbroot/pull/496
|
||||
[PR #497]: https://github.com/chenxiaolong/avbroot/pull/497
|
||||
[PR #498]: https://github.com/chenxiaolong/avbroot/pull/498
|
||||
[PR #499]: https://github.com/chenxiaolong/avbroot/pull/499
|
||||
[PR #500]: https://github.com/chenxiaolong/avbroot/pull/500
|
||||
[PR #501]: https://github.com/chenxiaolong/avbroot/pull/501
|
||||
[PR #502]: https://github.com/chenxiaolong/avbroot/pull/502
|
||||
[PR #503]: https://github.com/chenxiaolong/avbroot/pull/503
|
||||
[PR #504]: https://github.com/chenxiaolong/avbroot/pull/504
|
||||
[PR #505]: https://github.com/chenxiaolong/avbroot/pull/505
|
||||
[PR #509]: https://github.com/chenxiaolong/avbroot/pull/509
|
||||
[PR #510]: https://github.com/chenxiaolong/avbroot/pull/510
|
||||
[PR #511]: https://github.com/chenxiaolong/avbroot/pull/511
|
||||
[PR #512]: https://github.com/chenxiaolong/avbroot/pull/512
|
||||
[PR #517]: https://github.com/chenxiaolong/avbroot/pull/517
|
||||
[PR #519]: https://github.com/chenxiaolong/avbroot/pull/519
|
||||
[PR #520]: https://github.com/chenxiaolong/avbroot/pull/520
|
||||
[PR #521]: https://github.com/chenxiaolong/avbroot/pull/521
|
||||
[PR #523]: https://github.com/chenxiaolong/avbroot/pull/523
|
||||
[PR #524]: https://github.com/chenxiaolong/avbroot/pull/524
|
||||
[PR #529]: https://github.com/chenxiaolong/avbroot/pull/529
|
||||
[PR #530]: https://github.com/chenxiaolong/avbroot/pull/530
|
||||
|
||||
Generated
+409
-606
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -4,10 +4,12 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.19.0"
|
||||
version = "3.23.3"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
publish = false
|
||||
rust-version = "1.88.0"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
cast_lossless = "deny"
|
||||
|
||||
+12
-1
@@ -58,12 +58,23 @@ This subcommand shows all of the vbmeta header and footer fields. `vbmeta` parti
|
||||
avbroot avb verify -i <root vbmeta image> -p <public key>
|
||||
```
|
||||
|
||||
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
|
||||
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All images are expected to be in the same directory as the vbmeta image being verified. Missing images are ignored by default because the vbmeta images in some OTAs reference partitions that only exist on a real device. `--fail-if-missing` can be used to override this.
|
||||
|
||||
If `-p` is omitted, the signatures and hashes are checked only for validity, not that they are trusted.
|
||||
|
||||
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`.
|
||||
|
||||
### Verifying AVB hashes and signatures on device
|
||||
|
||||
```bash
|
||||
# Run from a root adb shell:
|
||||
avbroot avb verify-device [-p <public key>]
|
||||
```
|
||||
|
||||
This subcommand is like `avbroot avb verify`, except that it verifies the actual partitions on the device instead of a directory of image files. This is only available in the Android build of avbroot.
|
||||
|
||||
If `-p` is omitted, the signatures are verified against the public key SHA-256 digest reported by the bootloader. This is the same digest shown on screen every time the device boots.
|
||||
|
||||
### Computing vbmeta digest
|
||||
|
||||
```bash
|
||||
|
||||
@@ -51,7 +51,7 @@ avbroot applies the following patches to the partition images:
|
||||
|
||||
3. Follow the steps to [generate signing keys](#generating-keys).
|
||||
|
||||
Skip this step if you're updating Android, Magisk, or KernelSU after you've performed an [initial setup](#initial-setup). [Updates](#updates) do not require signing keys since you have already generated them in the initial setup.
|
||||
Skip this step if you're updating Android, Magisk, or KernelSU after you've already performed an [initial setup](#initial-setup). There's no need to generate new signing keys for [updates](#updates): any further updates must use the keys that were created during the initial setup.
|
||||
|
||||
4. Patch the OTA zip. The base command is:
|
||||
|
||||
@@ -213,6 +213,8 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
Alternatively, the Android build of avbroot can also be used to [verify the partitions on the device](./README.extra.md#verifying-avb-hashes-and-signatures-on-device).
|
||||
|
||||
9. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
|
||||
|
||||
```bash
|
||||
@@ -239,9 +241,9 @@ Updates to Android, Magisk, and KernelSU are all done the same way: by patching
|
||||
|
||||
4. Sideload the patched OTA with `adb sideload`.
|
||||
|
||||
5. Restart your phone. Note: the phone will likely take a long time to startup after an OS update (a few minutes in some cases).
|
||||
5. Restart your device. Note that the device will likely take longer than usual to start on the first boot after an OS update (a few minutes in some cases).
|
||||
|
||||
**Warning**: Due to how virtual A/B works, there is a snapshot merge operation that Android runs invisibly in the background after installing an OTA and rebooting. During the snapshot merge process, it's not possible to sideload another OTA from recovery mode. Avoid doing anything that could result in a boot loop (eg. installing modules) until this process is complete because there is no way to recover, aside from unlocking the bootloader (and wiping) again.
|
||||
**WARNING**: Due to how virtual A/B works, there is a snapshot merge operation that Android runs invisibly in the background after installing an OTA and rebooting. During the snapshot merge process, it's not possible to sideload another OTA from recovery mode. Avoid doing anything that could result in a boot loop (eg. installing modules) until this process is complete because there is no way to recover, aside from unlocking the bootloader (and wiping) again.
|
||||
|
||||
The status can be found by running `adb logcat -v color -s update_engine`. Alternatively, if [Custota](https://github.com/chenxiaolong/Custota) is installed (even if it's not configured to point to a custom OTA server), it will show a notification until the snapshot merge operation completes.
|
||||
|
||||
@@ -588,8 +590,6 @@ The output binary is written to `target/release/avbroot`.
|
||||
|
||||
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
|
||||
|
||||
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
|
||||
|
||||
### Android cross-compilation
|
||||
|
||||
To cross-compile for Android, install [cargo-android](https://github.com/chenxiaolong/cargo-android) and use the `cargo android` wrapper. To make a release build for aarch64, run:
|
||||
|
||||
+24
-18
@@ -6,7 +6,7 @@ avbroot – это утилита для воспроизводимой моди
|
||||
|
||||
## Требования
|
||||
|
||||
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваш телефон необходимую схему разметки, откройте zip-архив OTA и проверьте:
|
||||
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваше устройство необходимую схему разметки, откройте zip-архив OTA и проверьте:
|
||||
|
||||
* наличие файла `payload.bin` (обычно находится в корне архива)
|
||||
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
|
||||
@@ -49,6 +49,8 @@ avbroot модифицирует следующие образы:
|
||||
|
||||
3. [Сгенерируйте ключи подписи.](#генерация-ключей)
|
||||
|
||||
Пропустите этот шаг, если вы обновляете Android, Magisk или KernelSU уже после выполнения [первоначальной настройки](#первоначальная-настройка). Повторная генерация ключей подписи для [обновлений](#обновления) не требуется: для всех последующих обновлений должны использоваться те ключи, что были созданы при первоначальной настройке.
|
||||
|
||||
4. Пропатчите ОТА-архив с помощью команды:
|
||||
|
||||
```bash
|
||||
@@ -105,7 +107,7 @@ avbroot модифицирует следующие образы:
|
||||
|
||||
Первые два компонента подписываются ключом AVB, а последние два – ключом OTA. Можно использовать один и тот же ключ, однако в следующих шагах описано, как сгенерировать два отдельных.
|
||||
|
||||
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
|
||||
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA.
|
||||
|
||||
1. Сгенерируйте ключи подписи для AVB и OTA.
|
||||
|
||||
@@ -209,6 +211,8 @@ avbroot совместим с любым стандартным 4096-битны
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
Как ещё один вариант, Android-версию avbroot также можно использовать для [проверки разделов на устройстве](./README.extra.md#verifying-avb-hashes-and-signatures-on-device).
|
||||
|
||||
9. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
|
||||
|
||||
```bash
|
||||
@@ -227,15 +231,19 @@ avbroot совместим с любым стандартным 4096-битны
|
||||
|
||||
Обновления Android, Magisk и KernelSU выполняются одинаково – исключительно путем обновления или репатчинга того же самого OTA.
|
||||
|
||||
1. Если Magisk или KernelSU обновились, сначала установите их новый `.apk`. Если вы случайно открыли приложение после обновления, убедитесь, что оно не начало прошивать загрузочный образ. Если появится предложение обновить сам загрузочный образ – отклоните его.
|
||||
1. Сгенерируйте новый пропатченный OTA согласно инструкции в разделе [использования.](#использование)
|
||||
|
||||
2. Следуйте инструкции в разделе [использования,](#использование) чтобы пропатчить OTA уже с новым .apk Magisk'а/предварительно пропатченным образом с Magisk или KernelSU.
|
||||
2. Если обновляется Magisk или KernelSU, сначала установите их новый `.apk`. Если вы случайно открыли приложение, убедитесь, что оно **не начало** прошивать загрузочный образ. Если в самом приложении появится предложение обновить загрузочный образ, отклоните его.
|
||||
|
||||
3. Перезагрузитесь в режим Recovery. Если устройство повисло на сплеше с сообщением "No command", удерживайте кнопку питания, а затем нажмите кнопку увеличения громкости один раз.
|
||||
|
||||
4. Обновитесь (Apply update from adb → `adb sideload <ota.zip.patched>`).
|
||||
|
||||
5. Готово!
|
||||
5. Перезагрузите устройство. Обратите внимание, что при первом запуске после обновления ОС устройство может загружаться дольше обычного (иногда до нескольких минут).
|
||||
|
||||
**ПРЕДУПРЕЖДЕНИЕ**: В силу специфики работы виртуального A/B в Android, сразу после установки OTA и перезагрузки, в фоновом режиме незаметно запускается операция слияния снапшотов. Во время этого процесса невозможно установить другой OTA через режим Recovery. Пока продолжается слияние снапшотов, избегайте любых действий, которые могут привести к бутлупу (например, установка модулей), поскольку в случае сбоя восстановить устройство получится только повторно разблокировав загрузчик, стирая все данные.
|
||||
|
||||
Узнать текущий статус процесса можно, выполнив команду: `adb logcat -v color -s update_engine`. Дополнительно, если установлено [Custota](https://github.com/chenxiaolong/Custota) (даже если оно не настроено на использование пользовательского OTA-сервера), приложение будет отображать соответствующее уведомление до завершения операции слияния снапшота.
|
||||
|
||||
## Возврат на заводскую прошивку
|
||||
|
||||
@@ -315,7 +323,7 @@ Magisk версии 25211 и новее требует наличие разде
|
||||
|
||||
Теперь, когда имя раздела известно, его нужно указать avbroot с помощью команды `--magisk-preinit-device <имя>`. Имя раздела стоит запомнить или сохранить где-нибудь на будущее, оно вряд ли изменится при обновлении Magisk.
|
||||
|
||||
Если запустить приложение Magisk на целевом устройстве невозможно (например, телефон не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
|
||||
Если запустить приложение Magisk на целевом устройстве невозможно (например, оно не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
|
||||
|
||||
## Проверка OTA
|
||||
|
||||
@@ -391,17 +399,17 @@ avbroot можно использовать для простого перепо
|
||||
Если вы используете аргумент `--skip-recovery-ota-cert`, потому что уже добавили сертификат OTA в загрузочный образ вручную, рекомендуетcя [проверить пропатченный OTA](#проверка-ota), дабы удостовериться, что замена произведена корректно. Процесс верификации проверяет только копию сертификатов OTA в загрузочном образе, не проверяя копию в образе системы.
|
||||
|
||||
### Пропуск всех патчей
|
||||
|
||||
|
||||
Чтобы внести самый минимум изменений, укажите аргументы:
|
||||
|
||||
|
||||
* `--skip-system-ota-cert`
|
||||
* `--skip-recovery-ota-cert`
|
||||
* `--rootless`
|
||||
* не используйте аргумент `--dsu`.
|
||||
|
||||
|
||||
Так, пользовательскими ключами будут переподписаны лишь образ `vbmeta` и OTA, остальные разделы останутся нетронутыми.
|
||||
|
||||
**Это следует использовать только для устранения неполадок.** Без патчей сертификатов, поверх полученного OTA не получится установить никакие обновления.
|
||||
|
||||
**Это следует использовать только для устранения неполадок.** Без патчей сертификатов, поверх полученного OTA не получится установить никакие обновления.
|
||||
|
||||
### Подмена образов
|
||||
|
||||
@@ -422,15 +430,15 @@ Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
|
||||
|
||||
### Изменение алгоритма CoW сжатия для вирутального A/B
|
||||
|
||||
|
||||
Алгоритм CoW (copy-on-write) сжатия для виртуального A/B можно изменить, используя аргумент `--vabc-algo <алгоритм>`, указав `gz` или `lz4`. Как правило, по умолчанию OTA использует алгоритм, который совместим с изначальной версией Android, на которой поставлялось устройство.
|
||||
|
||||
|
||||
* Девайсы, поставляемые с Android 12, поддерживают `gz` и `brotli` (последний не поддерживается avbroot)
|
||||
* Девайсы, поставляемые с Android 14, поддерживают `lz4`
|
||||
* Девайсы, поставляемые с Android 15, поддерживают `zstd` (не поддерживается avbroot)
|
||||
|
||||
|
||||
Выбор быстрого алгоритма, такого как lz4, может значительно ускорить установку OTA из-под системы (при использованием стороннего приложения для OTA-обновлений). Однако, при установке OTA в режиме Recovery, разницы в скорости не будет.
|
||||
|
||||
|
||||
Обратите внимание, что текущая используемая версия Android должна поддерживать выбранный алгоритм сжатия. В противном случае установка завершится ошибкой. Например, попытка установить OTA-обновление с Android 14, использующее алгоритм lz4, приведет к ошибке, если установка производится из-под Android 13.
|
||||
|
||||
### Использование в неинтерактивном режиме
|
||||
@@ -515,7 +523,7 @@ avbroot поддерживает делегирование всех опера
|
||||
|
||||
На современных устройствах с Android 16 и выше, в настройках для разработчиков может появиться опция переключения на ядро с размером страницы 16 КБ. Однако, эта функция не будет работать в системе, пропатченной с помощью avbroot, поскольку переключение данной настройки осуществляется путём установки инкрементальной OTA:
|
||||
|
||||
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
|
||||
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
|
||||
* `/vendor/boot_otas/boot_ota_4k.zip` — используется для переключения на ядро с размером страницы 4 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 16K)
|
||||
|
||||
Эти файлы (в `boot_otas`) невозможно прошить на системе, пропатченной avbroot, потому что `payload.bin` внутри них подписан ключом производителя. Кроме того, это неполноценные OTA-файлы: у них нет метаданных, характерных для OTA, а сам zip-файл не подписан. Это просто обычный архив, который содержит подписанный `payload.bin`.
|
||||
@@ -570,8 +578,6 @@ cargo build --release
|
||||
|
||||
Дебаг-сборки тоже работают, но они будут работать значительно медленнее (в вычислениях sha256), потому что оптимизации компилятора отключены.
|
||||
|
||||
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
|
||||
|
||||
### Кросс-компиляция на Android
|
||||
|
||||
Чтобы использовать кросс-компиляцию на Android, установите [cargo-android](https://github.com/chenxiaolong/cargo-android) и воспользуйтесь оболочкой `cargo android`. Чтобы создать релизную сборку для aarch64, выполните:
|
||||
|
||||
+10
-18
@@ -4,7 +4,8 @@ version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
publish.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -14,8 +15,6 @@ base64 = "0.22.1"
|
||||
bitflags = { version = "2.4.1", features = ["serde"] }
|
||||
bstr = "1.6.2"
|
||||
bzip2 = "0.6.0"
|
||||
cap-std = "3.0.0"
|
||||
cap-tempfile = "3.0.0"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
clap_complete = "4.4.0"
|
||||
cms = { version = "0.2.2", features = ["std"] }
|
||||
@@ -28,17 +27,19 @@ dlv-list = "0.6.0"
|
||||
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
liblzma = "0.4.1"
|
||||
lz4_flex = "0.11.1"
|
||||
lz4_flex = "0.12.0"
|
||||
lzma-rust2 = "0.15.2"
|
||||
memchr = "2.6.0"
|
||||
# We can't upgrade to 0.9.0 until rsa updates its dependency.
|
||||
num-bigint-dig = "0.8.4"
|
||||
num-traits = "0.2.16"
|
||||
passterm = "2.0.3"
|
||||
phf = { version = "0.12.1", features = ["macros"] }
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
|
||||
prost = "0.14.1"
|
||||
# We can't upgrade to 0.9.0 until rsa updates its rand_core dependency.
|
||||
rand = "0.8.5"
|
||||
rawzip = "0.4.0"
|
||||
rayon = "1.7.0"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
# We use ring instead of sha2 for sha256 digest computation of large files
|
||||
@@ -60,19 +61,13 @@ x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
zerocopy = { version = "0.8.10", features = ["std"] }
|
||||
zerocopy-derive = "0.8.5"
|
||||
|
||||
# https://github.com/zip-rs/zip2/pull/367
|
||||
# https://github.com/zip-rs/zip2/pull/368
|
||||
# For getting the data offset when writing new zip entries.
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip2"
|
||||
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
|
||||
default-features = false
|
||||
features = ["deflate"]
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2.158"
|
||||
rustix = { version = "1.0.3", default-features = false, features = ["process"] }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
system-properties = { git = "https://github.com/chenxiaolong/system-properties", tag = "v0.2.1" }
|
||||
|
||||
[build-dependencies]
|
||||
constcat = "0.6.0"
|
||||
prost-build = "0.14.1"
|
||||
@@ -81,8 +76,5 @@ protox = "0.9.0"
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
[features]
|
||||
static = ["liblzma/static"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -40,19 +40,14 @@ pub enum Command {
|
||||
MagiskInfo(boot::MagiskInfoCli),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
|
||||
pub enum LogFormat {
|
||||
#[default]
|
||||
Short,
|
||||
Medium,
|
||||
Long,
|
||||
}
|
||||
|
||||
impl Default for LogFormat {
|
||||
fn default() -> Self {
|
||||
Self::Short
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LogFormat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
|
||||
|
||||
+267
-121
@@ -1,24 +1,22 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File},
|
||||
ffi::OsString,
|
||||
fmt,
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use cap_std::{
|
||||
ambient_authority,
|
||||
fs::{Dir, OpenOptions},
|
||||
};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use rsa::RsaPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use tracing::{Span, debug_span, info, warn};
|
||||
|
||||
use crate::{
|
||||
@@ -27,7 +25,7 @@ use crate::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
},
|
||||
stream::{self, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter, check_cancel},
|
||||
stream::{self, ReadFixedSizeExt, ToWriter, UserPosFile, check_cancel},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -54,7 +52,7 @@ fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
|
||||
Ok((info, reader))
|
||||
}
|
||||
|
||||
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo, recompute_size: bool) -> Result<()> {
|
||||
fn write_avb_image(file: &File, info: &mut AvbInfo, recompute_size: bool) -> Result<()> {
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
info.image_size = if let Some(f) = &mut info.footer {
|
||||
@@ -102,14 +100,13 @@ fn write_raw(
|
||||
reader: &mut BufReader<File>,
|
||||
size: u64,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
) -> Result<File> {
|
||||
let file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open raw image for writing: {path:?}"))?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
@@ -134,7 +131,7 @@ fn write_raw_and_verify(
|
||||
info: &AvbInfo,
|
||||
ignore_invalid: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
) -> Result<File> {
|
||||
let f = info.footer.as_ref().expect("Not an appended image");
|
||||
|
||||
let descriptor = info.header.appended_descriptor()?;
|
||||
@@ -148,7 +145,7 @@ fn write_raw_and_verify(
|
||||
|
||||
let raw_file = write_raw(path, reader, copy_size, cancel_signal)?;
|
||||
|
||||
let result = verify_and_repair(None, raw_file.reopen()?, descriptor, true, cancel_signal);
|
||||
let result = verify_and_repair(None, &raw_file, descriptor, true, cancel_signal);
|
||||
|
||||
// Chop off the old hash tree and FEC data.
|
||||
raw_file.set_len(f.original_image_size)?;
|
||||
@@ -171,7 +168,7 @@ fn write_raw_and_update(
|
||||
reader: &mut BufReader<File>,
|
||||
info: &mut AvbInfo,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
) -> Result<File> {
|
||||
assert!(info.footer.is_some(), "Not an appended image");
|
||||
|
||||
let image_size = reader
|
||||
@@ -183,7 +180,7 @@ fn write_raw_and_update(
|
||||
match info.header.appended_descriptor_mut()? {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.image_size = image_size;
|
||||
d.update(&raw_file, &raw_file, None, cancel_signal)
|
||||
d.update(&raw_file, None, cancel_signal)
|
||||
.context("Failed to update hash tree descriptor")?;
|
||||
}
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
@@ -298,14 +295,13 @@ fn update_dm_verity_cmdline(info: &mut AvbInfo) -> Result<bool> {
|
||||
};
|
||||
|
||||
for d in &mut info.header.descriptors {
|
||||
if let Descriptor::KernelCmdline(d) = d {
|
||||
if d.flags & KernelCmdlineDescriptor::FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED != 0
|
||||
&& d.cmdline.starts_with("dm=")
|
||||
&& d.cmdline != new_cmdline
|
||||
{
|
||||
d.cmdline = new_cmdline;
|
||||
return Ok(true);
|
||||
}
|
||||
if let Descriptor::KernelCmdline(d) = d
|
||||
&& d.flags & KernelCmdlineDescriptor::FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED != 0
|
||||
&& d.cmdline.starts_with("dm=")
|
||||
&& d.cmdline != new_cmdline
|
||||
{
|
||||
d.cmdline = new_cmdline;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,22 +406,82 @@ fn display_info(display: &DisplayGroup, info: &AvbInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that the partition name won't cause directory traversals.
|
||||
fn ensure_name_is_safe(name: &str) -> Result<()> {
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
bail!("Unsafe partition name: {name}");
|
||||
#[derive(Debug, Clone)]
|
||||
struct SearchPath {
|
||||
dir: PathBuf,
|
||||
suffix: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for SearchPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?} (suffix: {:?})", self.dir, self.suffix)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ImageOpener {
|
||||
search: Vec<SearchPath>,
|
||||
}
|
||||
|
||||
impl ImageOpener {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub fn with_dir(dir: impl Into<PathBuf>) -> Self {
|
||||
let mut result = Self::new();
|
||||
result.add_dir(dir, ".img");
|
||||
result
|
||||
}
|
||||
|
||||
pub fn add_dir(&mut self, dir: impl Into<PathBuf>, suffix: impl Into<String>) {
|
||||
self.search.push(SearchPath {
|
||||
dir: dir.into(),
|
||||
suffix: suffix.into(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open(&self, name: &str, options: &OpenOptions) -> io::Result<(PathBuf, File)> {
|
||||
for search in &self.search {
|
||||
let path = util::path_join_single(&search.dir, format!("{name}{}", search.suffix))
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
|
||||
|
||||
match options.open(&path) {
|
||||
Ok(f) => return Ok((path, f)),
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
|
||||
Err(e) => {
|
||||
return Err(io::Error::new(
|
||||
e.kind(),
|
||||
format!("Failed to open for reading: {path:?}: {e}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!(
|
||||
"Failed to find {name:?} image in: {}",
|
||||
util::join(&self.search, ", "),
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TrustMethod {
|
||||
Key(RsaPublicKey),
|
||||
KeyDigest([u8; 32]),
|
||||
Anything,
|
||||
}
|
||||
|
||||
/// Recursively verify an image's vbmeta header and all of the chained images.
|
||||
/// `seen` is used to prevent cycles. `descriptors` will contain all of the hash
|
||||
/// and hash tree descriptors that need to be verified.
|
||||
pub fn verify_headers(
|
||||
directory: &Dir,
|
||||
opener: &ImageOpener,
|
||||
name: &str,
|
||||
expected_key: Option<&RsaPublicKey>,
|
||||
trust_method: &TrustMethod,
|
||||
seen: &mut HashSet<String>,
|
||||
descriptors: &mut HashMap<String, Descriptor>,
|
||||
) -> Result<()> {
|
||||
@@ -433,12 +489,7 @@ pub fn verify_headers(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ensure_name_is_safe(name)?;
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let raw_reader = directory
|
||||
.open(&path)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let (path, raw_reader) = opener.open(name, OpenOptions::new().read(true))?;
|
||||
let (header, _, _) = avb::load_image(BufReader::new(raw_reader))
|
||||
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
|
||||
|
||||
@@ -450,14 +501,27 @@ pub fn verify_headers(
|
||||
if let Some(k) = &public_key {
|
||||
let prefix = format!("{name} has a signed vbmeta header");
|
||||
|
||||
if let Some(e) = expected_key {
|
||||
if k == e {
|
||||
info!("{prefix}");
|
||||
} else {
|
||||
bail!("{prefix}, but is signed by an untrusted key");
|
||||
match trust_method {
|
||||
TrustMethod::Key(expected) => {
|
||||
if k == expected {
|
||||
info!("{prefix}");
|
||||
} else {
|
||||
bail!("{prefix}, but is signed by an untrusted key");
|
||||
}
|
||||
}
|
||||
TrustMethod::KeyDigest(expected_sha256) => {
|
||||
let encoded = avb::encode_public_key(k)?;
|
||||
let digest = Sha256::digest(&encoded);
|
||||
|
||||
if digest.as_slice() == expected_sha256 {
|
||||
info!("{prefix}");
|
||||
} else {
|
||||
bail!("{prefix}, but is signed by an untrusted key");
|
||||
}
|
||||
}
|
||||
TrustMethod::Anything => {
|
||||
warn!("{prefix}, but parent does not list a trusted key");
|
||||
}
|
||||
} else {
|
||||
warn!("{prefix}, but parent does not list a trusted key");
|
||||
}
|
||||
} else {
|
||||
info!("{name} has an unsigned vbmeta header");
|
||||
@@ -486,8 +550,9 @@ pub fn verify_headers(
|
||||
let target_key = avb::decode_public_key(&d.public_key).with_context(|| {
|
||||
format!("Failed to decode chained public key for: {target_name}")
|
||||
})?;
|
||||
let target_trust = TrustMethod::Key(target_key);
|
||||
|
||||
verify_headers(directory, target_name, Some(&target_key), seen, descriptors)?;
|
||||
verify_headers(opener, target_name, &target_trust, seen, descriptors)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -502,7 +567,7 @@ pub fn verify_headers(
|
||||
/// work.
|
||||
fn verify_and_repair(
|
||||
name: Option<&str>,
|
||||
mut file: PSeekFile,
|
||||
file: &File,
|
||||
descriptor: AppendedDescriptorRef,
|
||||
repair: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -519,7 +584,7 @@ fn verify_and_repair(
|
||||
warn!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warn!("Attempting to repair using FEC data{suffix}");
|
||||
|
||||
d.repair(&file, &file, cancel_signal)
|
||||
d.repair(&file, cancel_signal)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
|
||||
d.verify(&file, cancel_signal).inspect(|()| {
|
||||
@@ -533,8 +598,7 @@ fn verify_and_repair(
|
||||
AppendedDescriptorRef::Hash(d) => {
|
||||
info!("Verifying hash descriptor{suffix}");
|
||||
|
||||
file.rewind()?;
|
||||
d.verify(file, cancel_signal)
|
||||
d.verify(UserPosFile::new(file), cancel_signal)
|
||||
.with_context(|| format!("Failed to verify hash descriptor{suffix}"))?;
|
||||
}
|
||||
}
|
||||
@@ -545,49 +609,44 @@ fn verify_and_repair(
|
||||
/// Verify hash and hash tree descriptor digests and FEC data against their
|
||||
/// corresponding input files.
|
||||
pub fn verify_descriptors(
|
||||
directory: &Dir,
|
||||
opener: &ImageOpener,
|
||||
descriptors: &HashMap<String, Descriptor>,
|
||||
repair: bool,
|
||||
allow_missing: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
descriptors
|
||||
.par_iter()
|
||||
.map(|(name, descriptor)| {
|
||||
let _span = parent_span.enter();
|
||||
let mut options = OpenOptions::new();
|
||||
options.read(true);
|
||||
options.write(repair);
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let file = match directory
|
||||
.open_with(&path, OpenOptions::new().read(true).write(repair))
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
{
|
||||
Ok(f) => f,
|
||||
// Some devices, like bluejay, have vbmeta descriptors that
|
||||
// refer to partitions that exist on the device, but not in the
|
||||
// OTA.
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
warn!("Partition image does not exist: {path:?}");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?
|
||||
}
|
||||
};
|
||||
descriptors.par_iter().try_for_each(|(name, descriptor)| {
|
||||
let _span = parent_span.enter();
|
||||
|
||||
verify_and_repair(
|
||||
Some(name),
|
||||
file,
|
||||
descriptor.try_into()?,
|
||||
repair,
|
||||
cancel_signal,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
let file = match opener.open(name, &options) {
|
||||
Ok((_, f)) => f,
|
||||
// Some devices, like bluejay, have vbmeta descriptors that refer to
|
||||
// partitions that exist on the device, but not in the OTA.
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound && allow_missing => {
|
||||
warn!("{e}");
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
verify_and_repair(
|
||||
Some(name),
|
||||
&file,
|
||||
descriptor.try_into()?,
|
||||
repair,
|
||||
cancel_signal,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_digest_recursive(
|
||||
directory: &Dir,
|
||||
directory: &Path,
|
||||
name: &str,
|
||||
context: &mut ring::digest::Context,
|
||||
max_depth: u8,
|
||||
@@ -602,11 +661,8 @@ fn compute_digest_recursive(
|
||||
|
||||
seen.insert(name.to_owned());
|
||||
|
||||
ensure_name_is_safe(name)?;
|
||||
|
||||
let path = format!("{name}.img");
|
||||
let mut raw_reader = directory
|
||||
.open(&path)
|
||||
let path = util::path_join_single(directory, format!("{name}.img"))?;
|
||||
let mut raw_reader = File::open(&path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let (header, footer, _) = avb::load_image(&mut raw_reader)
|
||||
@@ -658,7 +714,11 @@ fn compute_digest_recursive(
|
||||
/// 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]> {
|
||||
pub fn compute_digest(
|
||||
directory: &Path,
|
||||
name: &str,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<[u8; 32]> {
|
||||
let mut seen = HashSet::<String>::new();
|
||||
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
|
||||
@@ -675,7 +735,7 @@ fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
|
||||
write_info(&cli.output_info, &info)?;
|
||||
|
||||
if info.footer.is_some() {
|
||||
if info.footer.is_some() && !cli.no_output_raw {
|
||||
write_raw_and_verify(
|
||||
&cli.output_raw,
|
||||
&mut reader,
|
||||
@@ -706,13 +766,12 @@ fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
file
|
||||
} else {
|
||||
File::create(&cli.output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open output for writing: {:?}", cli.output))?
|
||||
};
|
||||
|
||||
sign_or_clear(&mut info, &orig_header, &cli.key)?;
|
||||
|
||||
write_avb_image(file, &mut info, cli.recompute_size)?;
|
||||
write_avb_image(&file, &mut info, cli.recompute_size)?;
|
||||
|
||||
// We display the info at the very end after both the header and footer are
|
||||
// updated so that incorrect/incomplete information isn't shown.
|
||||
@@ -735,7 +794,7 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
// Write new hash tree and FEC data instead of copying the original.
|
||||
// There could have been errors in the original FEC data itself.
|
||||
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
|
||||
d.update(&file, &file, None, cancel_signal)?;
|
||||
d.update(&file, None, cancel_signal)?;
|
||||
}
|
||||
|
||||
update_dm_verity_cmdline(&mut info)?;
|
||||
@@ -743,13 +802,12 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
file
|
||||
} else {
|
||||
File::create(&cli.output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for writing: {:?}", cli.output))?
|
||||
};
|
||||
|
||||
sign_or_clear(&mut info, &orig_header, &cli.key)?;
|
||||
|
||||
write_avb_image(file, &mut info, false)?;
|
||||
write_avb_image(&file, &mut info, false)?;
|
||||
|
||||
// We display the info at the very end after both the header and footer are
|
||||
// updated so that incorrect/incomplete information isn't shown.
|
||||
@@ -765,50 +823,40 @@ fn info_subcommand(cli: &InfoCli) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let public_key = if let Some(p) = &cli.public_key {
|
||||
fn verify_internal(
|
||||
public_key_path: Option<&Path>,
|
||||
public_key_digest: Option<[u8; 32]>,
|
||||
opener: &ImageOpener,
|
||||
name: &str,
|
||||
repair: bool,
|
||||
allow_missing: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let trust_method = if let Some(p) = public_key_path {
|
||||
let data = fs::read(p).with_context(|| format!("Failed to read file: {p:?}"))?;
|
||||
let key = avb::decode_public_key(&data)
|
||||
.with_context(|| format!("Failed to decode public key: {p:?}"))?;
|
||||
|
||||
Some(key)
|
||||
TrustMethod::Key(key)
|
||||
} else if let Some(d) = public_key_digest {
|
||||
TrustMethod::KeyDigest(d)
|
||||
} else {
|
||||
None
|
||||
TrustMethod::Anything
|
||||
};
|
||||
|
||||
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 mut seen = HashSet::<String>::new();
|
||||
let mut descriptors = HashMap::<String, Descriptor>::new();
|
||||
|
||||
verify_headers(
|
||||
&directory,
|
||||
name,
|
||||
public_key.as_ref(),
|
||||
&mut seen,
|
||||
&mut descriptors,
|
||||
)?;
|
||||
verify_descriptors(&directory, &descriptors, cli.repair, cancel_signal)?;
|
||||
verify_headers(opener, name, &trust_method, &mut seen, &mut descriptors)?;
|
||||
verify_descriptors(opener, &descriptors, repair, allow_missing, cancel_signal)?;
|
||||
|
||||
info!("Successfully verified all vbmeta signatures and hashes");
|
||||
|
||||
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:?}"))?;
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let directory = util::parent_path(&cli.input);
|
||||
let name = cli
|
||||
.input
|
||||
.file_stem()
|
||||
@@ -816,7 +864,71 @@ fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
|
||||
|
||||
let digest = compute_digest(&directory, name, cancel_signal)?;
|
||||
let opener = ImageOpener::with_dir(directory);
|
||||
|
||||
verify_internal(
|
||||
cli.public_key.as_deref(),
|
||||
None,
|
||||
&opener,
|
||||
name,
|
||||
cli.repair,
|
||||
!cli.fail_if_missing,
|
||||
cancel_signal,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn get_required_property(name: &str) -> Result<String> {
|
||||
system_properties::read(name)
|
||||
.with_context(|| format!("Failed to query property: {name}"))?
|
||||
.ok_or_else(|| anyhow!("Property is not set: {name}"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
fn verify_device_subcommand(cli: &VerifyDeviceCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let slot_suffix = get_required_property("ro.boot.slot_suffix")?;
|
||||
|
||||
// Use the bootloader's public key digest if no key is specified. This is
|
||||
// what the user flashed for avb_custom_key.
|
||||
let public_key_digest = if cli.public_key.is_none() {
|
||||
let hex_digest = get_required_property("ro.boot.vbmeta.public_key_digest")?;
|
||||
let mut digest = [0u8; 32];
|
||||
|
||||
hex::decode_to_slice(&hex_digest, &mut digest)
|
||||
.with_context(|| format!("Invalid public key digest: {hex_digest}"))?;
|
||||
|
||||
info!("Verifying against bootloader public key digest: {hex_digest}");
|
||||
|
||||
Some(digest)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut opener = ImageOpener::new();
|
||||
opener.add_dir("/dev/block/by-name", &slot_suffix);
|
||||
opener.add_dir("/dev/block/mapper", &slot_suffix);
|
||||
|
||||
verify_internal(
|
||||
cli.public_key.as_deref(),
|
||||
public_key_digest,
|
||||
&opener,
|
||||
&cli.partition,
|
||||
false,
|
||||
false,
|
||||
cancel_signal,
|
||||
)
|
||||
}
|
||||
|
||||
fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let directory = util::parent_path(&cli.input);
|
||||
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));
|
||||
|
||||
@@ -830,6 +942,8 @@ 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),
|
||||
#[cfg(target_os = "android")]
|
||||
AvbCommand::VerifyDevice(c) => verify_device_subcommand(c, cancel_signal),
|
||||
AvbCommand::Digest(c) => digest_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
@@ -911,6 +1025,10 @@ struct UnpackCli {
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
|
||||
output_raw: PathBuf,
|
||||
|
||||
/// Do not output raw image.
|
||||
#[arg(long, conflicts_with = "output_raw")]
|
||||
no_output_raw: bool,
|
||||
|
||||
/// Ignore invalid digests or FEC data.
|
||||
#[arg(long)]
|
||||
ignore_invalid: bool,
|
||||
@@ -1033,6 +1151,32 @@ struct VerifyCli {
|
||||
/// Only images with hash tree descriptors can contain FEC data.
|
||||
#[arg(short, long)]
|
||||
repair: bool,
|
||||
|
||||
/// Fail if a referenced image is missing.
|
||||
///
|
||||
/// Missing images are ignored by default because some OTAs contain vbmeta
|
||||
/// images referencing partitions that only exist on the real device.
|
||||
#[arg(long)]
|
||||
fail_if_missing: bool,
|
||||
}
|
||||
|
||||
/// Verify vbmeta signatures for the currently booted system.
|
||||
///
|
||||
/// This behaves like the `verify` subcommand, except that it checks the actual
|
||||
/// partitions that this device is currently booted from.
|
||||
#[cfg(target_os = "android")]
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyDeviceCli {
|
||||
/// Path to public key in AVB binary format.
|
||||
///
|
||||
/// If this is not specified, the signatures can only be checked for
|
||||
/// validity, not whether they are trusted.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
public_key: Option<PathBuf>,
|
||||
|
||||
/// Partition to recursively verify.
|
||||
#[arg(short = 'P', long, value_name = "NAME", default_value = "vbmeta")]
|
||||
partition: String,
|
||||
}
|
||||
|
||||
/// Compute the vbmeta digest.
|
||||
@@ -1054,6 +1198,8 @@ enum AvbCommand {
|
||||
#[command(alias = "dump")]
|
||||
Info(InfoCli),
|
||||
Verify(VerifyCli),
|
||||
#[cfg(target_os = "android")]
|
||||
VerifyDevice(VerifyDeviceCli),
|
||||
Digest(DigestCli),
|
||||
}
|
||||
|
||||
|
||||
+53
-11
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -164,30 +164,44 @@ fn unpack_subcommand(boot_cli: &BootCli, cli: &UnpackCli) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(data) = kernel {
|
||||
if let Some(data) = kernel
|
||||
&& !cli.no_output_kernel
|
||||
{
|
||||
write_data_if_not_empty(&cli.output_kernel, data)?;
|
||||
}
|
||||
if let Some(data) = second {
|
||||
if let Some(data) = second
|
||||
&& !cli.no_output_second
|
||||
{
|
||||
write_data_if_not_empty(&cli.output_second, data)?;
|
||||
}
|
||||
if let Some(data) = recovery_dtbo {
|
||||
if let Some(data) = recovery_dtbo
|
||||
&& !cli.no_output_recovery_dtbo
|
||||
{
|
||||
write_data_if_not_empty(&cli.output_recovery_dtbo, data)?;
|
||||
}
|
||||
if let Some(data) = dtb {
|
||||
if let Some(data) = dtb
|
||||
&& !cli.no_output_dtb
|
||||
{
|
||||
write_data_if_not_empty(&cli.output_dtb, data)?;
|
||||
}
|
||||
if let Some(header) = vts_signature {
|
||||
if let Some(header) = vts_signature
|
||||
&& !cli.no_output_vts_signature
|
||||
{
|
||||
write_avb_header(&cli.output_vts_signature, header)?;
|
||||
}
|
||||
if let Some(text) = bootconfig {
|
||||
if let Some(text) = bootconfig
|
||||
&& !cli.no_output_bootconfig
|
||||
{
|
||||
write_text_if_not_empty(&cli.output_bootconfig, text)?;
|
||||
}
|
||||
|
||||
for (i, data) in ramdisks.iter().enumerate() {
|
||||
let mut path = cli.output_ramdisk_prefix.as_os_str().to_owned();
|
||||
path.push(i.to_string());
|
||||
if !cli.no_output_ramdisk {
|
||||
for (i, data) in ramdisks.iter().enumerate() {
|
||||
let mut path = cli.output_ramdisk_prefix.as_os_str().to_owned();
|
||||
path.push(i.to_string());
|
||||
|
||||
write_data_if_not_empty(Path::new(&path), data)?;
|
||||
write_data_if_not_empty(Path::new(&path), data)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -339,6 +353,10 @@ struct UnpackCli {
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "kernel.img")]
|
||||
output_kernel: PathBuf,
|
||||
|
||||
/// Do not output kernel image.
|
||||
#[arg(long, conflicts_with = "output_kernel")]
|
||||
no_output_kernel: bool,
|
||||
|
||||
/// Path prefix for output ramdisk images.
|
||||
#[arg(
|
||||
long,
|
||||
@@ -348,10 +366,18 @@ struct UnpackCli {
|
||||
)]
|
||||
output_ramdisk_prefix: PathBuf,
|
||||
|
||||
/// Do not output ramdisk images.
|
||||
#[arg(long, conflicts_with = "output_ramdisk_prefix")]
|
||||
no_output_ramdisk: bool,
|
||||
|
||||
/// Path to output second stage bootloader image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "second.img")]
|
||||
output_second: PathBuf,
|
||||
|
||||
/// Do not output second stage bootloader image.
|
||||
#[arg(long, conflicts_with = "output_second")]
|
||||
no_output_second: bool,
|
||||
|
||||
/// Path to output recovery dtbo/acpio image.
|
||||
#[arg(
|
||||
long,
|
||||
@@ -361,10 +387,18 @@ struct UnpackCli {
|
||||
)]
|
||||
output_recovery_dtbo: PathBuf,
|
||||
|
||||
/// Do not output recovery dtbo/acpio image.
|
||||
#[arg(long, conflicts_with = "output_recovery_dtbo")]
|
||||
no_output_recovery_dtbo: bool,
|
||||
|
||||
/// Path to output device tree blob image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "dtb.img")]
|
||||
output_dtb: PathBuf,
|
||||
|
||||
/// Do not output device tree blob image.
|
||||
#[arg(long, conflicts_with = "output_dtb")]
|
||||
no_output_dtb: bool,
|
||||
|
||||
/// Path to output VTS signature.
|
||||
#[arg(
|
||||
long,
|
||||
@@ -374,6 +408,10 @@ struct UnpackCli {
|
||||
)]
|
||||
output_vts_signature: PathBuf,
|
||||
|
||||
/// Do not output VTS signature.
|
||||
#[arg(long, conflicts_with = "output_vts_signature")]
|
||||
no_output_vts_signature: bool,
|
||||
|
||||
/// Path to output bootconfig text.
|
||||
#[arg(
|
||||
long,
|
||||
@@ -382,6 +420,10 @@ struct UnpackCli {
|
||||
default_value = "bootconfig.txt"
|
||||
)]
|
||||
output_bootconfig: PathBuf,
|
||||
|
||||
/// Do not output bootconfig text.
|
||||
#[arg(long, conflicts_with = "output_bootconfig")]
|
||||
no_output_bootconfig: bool,
|
||||
}
|
||||
|
||||
/// Pack a boot image.
|
||||
|
||||
+31
-33
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -11,7 +11,6 @@ use std::{
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use bstr::ByteSlice;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{Parser, Subcommand};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -91,17 +90,17 @@ fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
|
||||
|
||||
/// Open reader to the corresponding file inside the tree if the entry is a
|
||||
/// regular file. Unsafe paths will result in an error.
|
||||
fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
|
||||
fn open_tree_file(tree: &Path, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
|
||||
if entry.file_type == CpioEntryType::Regular {
|
||||
let path = entry
|
||||
let sub_path = entry
|
||||
.path
|
||||
.as_bstr()
|
||||
.to_path()
|
||||
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
|
||||
let path = util::path_join(tree, sub_path)?;
|
||||
|
||||
let mut reader = tree
|
||||
.open(path)
|
||||
.map(|f| BufReader::new(f.into_std()))
|
||||
let mut reader = File::open(&path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
|
||||
let file_size = reader
|
||||
@@ -122,21 +121,21 @@ fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<Fil
|
||||
/// Open writer to the corresponding file inside the tree if the entry is a
|
||||
/// regular file. Intermediate directories are automatically created as needed.
|
||||
/// Unsafe paths will result in an error.
|
||||
fn create_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
|
||||
fn create_tree_file(tree: &Path, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
|
||||
if entry.file_type == CpioEntryType::Regular {
|
||||
let path = entry
|
||||
let sub_path = entry
|
||||
.path
|
||||
.as_bstr()
|
||||
.to_path()
|
||||
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
|
||||
let parent = util::parent_path(path);
|
||||
let path = util::path_join(tree, sub_path)?;
|
||||
let parent = util::parent_path(&path);
|
||||
|
||||
tree.create_dir_all(parent)
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
|
||||
|
||||
let writer = tree
|
||||
.create(path)
|
||||
.map(|f| BufWriter::new(f.into_std()))
|
||||
let writer = File::create(&path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
|
||||
Ok(Some(writer))
|
||||
@@ -171,25 +170,24 @@ fn unpack_subcommand(
|
||||
|
||||
display_format(cpio_cli, format);
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.output_tree, authority)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
|
||||
let tree = Dir::open_ambient_dir(&cli.output_tree, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.output_tree))?;
|
||||
if !cli.no_output_tree {
|
||||
fs::create_dir_all(&cli.output_tree)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
|
||||
|
||||
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
|
||||
display_entry(cpio_cli, &entry);
|
||||
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
|
||||
display_entry(cpio_cli, &entry);
|
||||
|
||||
if let Some(mut writer) = create_tree_file(&tree, &entry)? {
|
||||
let file_size = entry.data.size()?;
|
||||
if let Some(mut writer) = create_tree_file(&cli.output_tree, &entry)? {
|
||||
let file_size = entry.data.size()?;
|
||||
|
||||
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
|
||||
.context("Failed to copy data")?;
|
||||
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
|
||||
.context("Failed to copy data")?;
|
||||
|
||||
writer.into_inner().context("Failed to flush data")?;
|
||||
writer.into_inner().context("Failed to flush data")?;
|
||||
}
|
||||
|
||||
info.entries.push(entry);
|
||||
}
|
||||
|
||||
info.entries.push(entry);
|
||||
}
|
||||
|
||||
write_info(&cli.output_info, &info)?;
|
||||
@@ -209,12 +207,8 @@ fn pack_subcommand(cpio_cli: &CpioCli, cli: &PackCli, cancel_signal: &AtomicBool
|
||||
|
||||
cpio::assign_inodes(&mut info.entries, true)?;
|
||||
|
||||
let authority = ambient_authority();
|
||||
let tree = Dir::open_ambient_dir(&cli.input_tree, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.input_tree))?;
|
||||
|
||||
for entry in &mut info.entries {
|
||||
let out = open_tree_file(&tree, entry)?;
|
||||
let out = open_tree_file(&cli.input_tree, entry)?;
|
||||
|
||||
if let Some((_, file_size)) = &out {
|
||||
entry.data = CpioEntryData::Size(*file_size);
|
||||
@@ -308,6 +302,10 @@ struct UnpackCli {
|
||||
/// Path to output files directory.
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
|
||||
output_tree: PathBuf,
|
||||
|
||||
/// Do not output files.
|
||||
#[arg(long, conflicts_with = "output_tree")]
|
||||
no_output_tree: bool,
|
||||
}
|
||||
|
||||
/// Pack a cpio archive.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -13,15 +13,14 @@ use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::fec::FecImage,
|
||||
stream::{FromReader, PSeekFile, ToWriter},
|
||||
stream::{FromReader, ToWriter},
|
||||
};
|
||||
|
||||
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
|
||||
fn open_input(path: &Path, rw: bool) -> Result<File> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(rw)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))
|
||||
}
|
||||
|
||||
@@ -91,10 +90,7 @@ fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
let input = open_input(&cli.input, true)?;
|
||||
let fec = read_fec(&cli.fec)?;
|
||||
|
||||
// The separate buffered readers and writers are safe because the function
|
||||
// guarantees that every thread touches disjoint offsets and every offset is
|
||||
// read and written at most once.
|
||||
fec.repair(&input, &input, cancel_signal)
|
||||
fec.repair(&input, cancel_signal)
|
||||
.context("Failed to repair file")?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -13,15 +13,14 @@ use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::hashtree::HashTreeImage,
|
||||
stream::{FromReader, PSeekFile, ToWriter},
|
||||
stream::{FromReader, ToWriter},
|
||||
};
|
||||
|
||||
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
|
||||
fn open_input(path: &Path, rw: bool) -> Result<File> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(rw)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))
|
||||
}
|
||||
|
||||
|
||||
+33
-67
@@ -1,8 +1,7 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::{Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
@@ -10,7 +9,6 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use rayon::iter::{
|
||||
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator,
|
||||
@@ -18,36 +16,33 @@ use rayon::iter::{
|
||||
|
||||
use crate::{
|
||||
format::lp::{Extent, ExtentType, ImageType, Metadata, SECTOR_SIZE},
|
||||
stream::{self, FromReader, PSeekFile, Reopen, ToWriter},
|
||||
stream::{self, FromReader, ToWriter, UserPosFile},
|
||||
util,
|
||||
};
|
||||
|
||||
fn open_lp_inputs(paths: &[impl AsRef<Path>]) -> Result<(Vec<PSeekFile>, Metadata)> {
|
||||
let mut inputs = paths
|
||||
fn open_lp_inputs(paths: &[impl AsRef<Path>]) -> Result<(Vec<File>, Metadata)> {
|
||||
let inputs = paths
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let p = p.as_ref();
|
||||
|
||||
File::open(p)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
|
||||
File::open(p).with_context(|| format!("Failed to open LP image for reading: {p:?}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let metadata = Metadata::from_reader(&mut inputs[0])
|
||||
let metadata = Metadata::from_reader(&inputs[0])
|
||||
.with_context(|| format!("Failed to parse LP image metadata: {:?}", paths[0].as_ref()))?;
|
||||
|
||||
Ok((inputs, metadata))
|
||||
}
|
||||
|
||||
fn open_lp_outputs(paths: &[impl AsRef<Path>]) -> Result<Vec<PSeekFile>> {
|
||||
fn open_lp_outputs(paths: &[impl AsRef<Path>]) -> Result<Vec<File>> {
|
||||
paths
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let p = p.as_ref();
|
||||
|
||||
File::create(p)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open LP image for writing: {p:?}"))
|
||||
File::create(p).with_context(|| format!("Failed to open LP image for writing: {p:?}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
}
|
||||
@@ -166,17 +161,15 @@ fn fill_slots(metadata: &mut Metadata) {
|
||||
}
|
||||
|
||||
fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let mut inputs = cli
|
||||
let inputs = cli
|
||||
.input
|
||||
.iter()
|
||||
.map(|p| {
|
||||
File::open(p)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
|
||||
File::open(p).with_context(|| format!("Failed to open LP image for reading: {p:?}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let mut metadata = Metadata::from_reader(&mut inputs[0])
|
||||
let mut metadata = Metadata::from_reader(&inputs[0])
|
||||
.with_context(|| format!("Failed to read LP image metadata: {:?}", cli.input[0]))?;
|
||||
|
||||
// Display and write only the selected slot.
|
||||
@@ -186,15 +179,12 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
|
||||
write_info(&cli.output_info, &metadata)?;
|
||||
|
||||
// For empty images, there's no data to unpack.
|
||||
if metadata.image_type == ImageType::Empty {
|
||||
if metadata.image_type == ImageType::Empty || cli.no_output_images {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.output_images, authority)
|
||||
fs::create_dir_all(&cli.output_images)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
|
||||
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
|
||||
|
||||
let slot = &metadata.slots[0];
|
||||
|
||||
@@ -216,11 +206,10 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
|
||||
|
||||
for partition in &group.partitions {
|
||||
// A partition name with unsafe characters fails during parsing.
|
||||
let path = format!("{}.img", partition.name);
|
||||
let path =
|
||||
util::path_join_single(&cli.output_images, format!("{}.img", partition.name))?;
|
||||
|
||||
let file = directory
|
||||
.create(&path)
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
let file = File::create(&path)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
|
||||
file.set_len(partition.size()?)
|
||||
@@ -251,10 +240,9 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
|
||||
.into_par_iter()
|
||||
.map(move |e| (g_index, p_index, e))
|
||||
})
|
||||
.map(|(g_index, p_index, extent)| {
|
||||
// Never fails for PSeekFiles.
|
||||
let mut reader = inputs[extent.device_index].reopen()?;
|
||||
let mut writer = files[g_index][p_index].reopen()?;
|
||||
.try_for_each(|(g_index, p_index, extent)| {
|
||||
let mut reader = UserPosFile::new(&inputs[extent.device_index]);
|
||||
let mut writer = UserPosFile::new(&files[g_index][p_index]);
|
||||
|
||||
let r_path = &cli.input[extent.device_index];
|
||||
let w_path = &paths[g_index][p_index];
|
||||
@@ -271,9 +259,6 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
@@ -303,35 +288,20 @@ fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) ->
|
||||
}
|
||||
}
|
||||
|
||||
for group in &slot.groups {
|
||||
for partition in &group.partitions {
|
||||
let name = &partition.name;
|
||||
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
bail!("Unsafe partition name: {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Preopen all image input files.
|
||||
let mut paths = vec![];
|
||||
let mut files = vec![];
|
||||
|
||||
if metadata.image_type == ImageType::Normal {
|
||||
let authority = ambient_authority();
|
||||
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
|
||||
|
||||
for group in &mut slot.groups {
|
||||
let mut group_paths = vec![];
|
||||
let mut group_files = vec![];
|
||||
|
||||
for partition in &mut group.partitions {
|
||||
let path = format!("{}.img", partition.name);
|
||||
let path =
|
||||
util::path_join_single(&cli.input_images, format!("{}.img", partition.name))?;
|
||||
|
||||
let mut file = directory
|
||||
.open(&path)
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
let mut file = File::open(&path)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
|
||||
let size = file
|
||||
@@ -398,10 +368,9 @@ fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) ->
|
||||
.into_par_iter()
|
||||
.map(move |e| (g_index, p_index, e))
|
||||
})
|
||||
.map(|(g_index, p_index, extent)| {
|
||||
// Never fails for PSeekFiles.
|
||||
let mut reader = files[g_index][p_index].reopen()?;
|
||||
let mut writer = outputs[extent.device_index].reopen()?;
|
||||
.try_for_each(|(g_index, p_index, extent)| {
|
||||
let mut reader = UserPosFile::new(&files[g_index][p_index]);
|
||||
let mut writer = UserPosFile::new(&outputs[extent.device_index]);
|
||||
|
||||
let r_path = &paths[g_index][p_index];
|
||||
let w_path = &cli.output[extent.device_index];
|
||||
@@ -418,9 +387,6 @@ fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) ->
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
@@ -503,10 +469,9 @@ fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool
|
||||
// Flatten extents in all partitions and split them to smaller chunks
|
||||
// for better parallelism.
|
||||
.flat_map(|partition| split_extents(&partition.extents))
|
||||
.map(|extent| {
|
||||
// Never fails for PSeekFiles.
|
||||
let mut reader = inputs[extent.device_index].reopen()?;
|
||||
let mut writer = outputs[extent.device_index].reopen()?;
|
||||
.try_for_each(|extent| {
|
||||
let mut reader = UserPosFile::new(&inputs[extent.device_index]);
|
||||
let mut writer = UserPosFile::new(&outputs[extent.device_index]);
|
||||
|
||||
let r_path = &cli.input[extent.device_index];
|
||||
let w_path = &cli.output[extent.device_index];
|
||||
@@ -523,9 +488,6 @@ fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(lp_cli: &LpCli, cli: &InfoCli) -> Result<()> {
|
||||
@@ -573,6 +535,10 @@ struct UnpackCli {
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")]
|
||||
output_images: PathBuf,
|
||||
|
||||
/// Do not output images.
|
||||
#[arg(long, conflicts_with = "output_images")]
|
||||
no_output_images: bool,
|
||||
|
||||
/// The LP metadata slot to use.
|
||||
///
|
||||
/// This slot is the only slot where data extents are copied from. Any data
|
||||
|
||||
+356
-291
File diff suppressed because it is too large
Load Diff
+52
-41
@@ -1,17 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::{OsStr, OsString},
|
||||
ffi::OsString,
|
||||
fs::{self, File},
|
||||
io::{BufReader, BufWriter, Seek, SeekFrom},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use tracing::info;
|
||||
|
||||
@@ -19,7 +18,8 @@ use crate::{
|
||||
cli::ota,
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::payload::{PayloadHeader, PayloadWriter},
|
||||
stream::{self, FromReader, PSeekFile},
|
||||
stream::{self, FromReader},
|
||||
util,
|
||||
};
|
||||
|
||||
fn open_reader(path: &Path, allow_delta: bool) -> Result<(BufReader<File>, PayloadHeader)> {
|
||||
@@ -113,27 +113,26 @@ fn unpack_subcommand(
|
||||
|
||||
write_info(&cli.output_info, &header)?;
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.output_images, authority)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
|
||||
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
|
||||
if !cli.no_output_images {
|
||||
fs::create_dir_all(&cli.output_images)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
|
||||
|
||||
ota::extract_payload(
|
||||
&PSeekFile::new(reader.into_inner()),
|
||||
&directory,
|
||||
0,
|
||||
payload_size,
|
||||
&header,
|
||||
&header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| &p.partition_name)
|
||||
.cloned()
|
||||
.collect(),
|
||||
cancel_signal,
|
||||
)?;
|
||||
ota::extract_payload(
|
||||
&reader.into_inner(),
|
||||
&cli.output_images,
|
||||
0,
|
||||
payload_size,
|
||||
&header,
|
||||
&header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| &p.partition_name)
|
||||
.cloned()
|
||||
.collect(),
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -147,34 +146,42 @@ fn pack_subcommand(
|
||||
|
||||
let mut header = read_info(&cli.input_info)?;
|
||||
|
||||
let authority = ambient_authority();
|
||||
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
|
||||
|
||||
for p in &header.manifest.partitions {
|
||||
let name = &p.partition_name;
|
||||
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
bail!("Unsafe partition name: {name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-open all of the image files.
|
||||
let input_files = header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let path = format!("{}.img", p.partition_name);
|
||||
let file = directory
|
||||
.open(&path)
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
let path =
|
||||
util::path_join_single(&cli.input_images, format!("{}.img", p.partition_name))?;
|
||||
let file = File::open(&path)
|
||||
.map(Arc::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))?;
|
||||
|
||||
Ok((p.partition_name.clone(), file))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>>>()?;
|
||||
|
||||
for (name, input_file) in &input_files {
|
||||
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !dpm.groups.iter().any(|g| g.partition_names.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter_mut()
|
||||
.find(|p| p.partition_name == *name)
|
||||
.unwrap()
|
||||
.estimate_cow_size = Some(0);
|
||||
|
||||
ota::recow_image(name, input_file, &mut header, cancel_signal)?;
|
||||
}
|
||||
|
||||
// Compress the images and compute the list of install operations for
|
||||
// insertion into the payload header. The compressed data is stored in new
|
||||
// temp files and the original input files are dropped.
|
||||
@@ -381,6 +388,10 @@ struct UnpackCli {
|
||||
default_value = "payload_images"
|
||||
)]
|
||||
output_images: PathBuf,
|
||||
|
||||
/// Do not output images.
|
||||
#[arg(long, conflicts_with = "output_images")]
|
||||
no_output_images: bool,
|
||||
}
|
||||
|
||||
/// Pack a payload binary.
|
||||
|
||||
@@ -321,7 +321,7 @@ fn unpack_subcommand(
|
||||
let to_skip = i64::from(chunk.bounds.len()) * i64::from(metadata.header.block_size);
|
||||
|
||||
writer
|
||||
.seek(SeekFrom::Current(to_skip))
|
||||
.seek_relative(to_skip)
|
||||
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
|
||||
}
|
||||
ChunkData::Crc32(_) => {}
|
||||
|
||||
@@ -160,10 +160,10 @@ impl PassphraseSource {
|
||||
Err(e) => {
|
||||
#[cfg(unix)]
|
||||
if let PromptError::IOError(io_e) = e {
|
||||
if let Some(errno) = io_e.raw_os_error() {
|
||||
if errno == libc::ENXIO || errno == libc::ENOTTY {
|
||||
return Err(Error::NotInteractive(io_e));
|
||||
}
|
||||
if let Some(errno) = io_e.raw_os_error()
|
||||
&& (errno == libc::ENXIO || errno == libc::ENOTTY)
|
||||
{
|
||||
return Err(Error::NotInteractive(io_e));
|
||||
}
|
||||
|
||||
return Err(Error::PassphrasePrompt(PromptError::IOError(io_e)));
|
||||
|
||||
+45
-65
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -29,8 +29,8 @@ use crate::{
|
||||
padding::{self, ZeroPadding},
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, CountingWriter, FromReader, ReadFixedSizeExt, ReadSeekReopen,
|
||||
ToWriter, WriteSeekReopen, WriteZerosExt,
|
||||
self, CountingReader, CountingWriter, FromReader, ReadAt, ReadFixedSizeExt, ReadWriteAt,
|
||||
ToWriter, UserPosFile, WriteZerosExt,
|
||||
},
|
||||
util::{self, OutOfBoundsError},
|
||||
};
|
||||
@@ -149,10 +149,6 @@ pub enum Error {
|
||||
FecVerify(#[source] fec::Error),
|
||||
#[error("Failed to repair file with FEC data")]
|
||||
FecRepair(#[source] fec::Error),
|
||||
#[error("Failed to reopen input file")]
|
||||
InputReopen(#[source] io::Error),
|
||||
#[error("Failed to reopen output file")]
|
||||
OutputReopen(#[source] io::Error),
|
||||
#[error("Failed to compute hash of input file")]
|
||||
InputDigest(#[source] io::Error),
|
||||
#[error("Failed to read AVB data: {0}")]
|
||||
@@ -510,10 +506,7 @@ impl HashTreeDescriptor {
|
||||
|
||||
/// Update the root hash, hash tree, and FEC data. The hash tree and FEC
|
||||
/// data will be written immediately following the image data at offset
|
||||
/// [`Self::image_size`]. Both `open_input` and `open_output` may be called
|
||||
/// from multiple threads and must return independently seekable handles to
|
||||
/// the same file. It is guaranteed that every thread will read and write
|
||||
/// disjoint file offsets.
|
||||
/// [`Self::image_size`].
|
||||
///
|
||||
/// If `ranges` is [`Option::None`], then the hash tree and FEC data are
|
||||
/// updated for the whole while. Due to the nature of the file access
|
||||
@@ -525,42 +518,37 @@ impl HashTreeDescriptor {
|
||||
/// that what is specified in order to perform the computations.
|
||||
///
|
||||
/// The fields in this instance are updated atomically. No fields are
|
||||
/// updated if an error occurs. The input file can be restored back to its
|
||||
/// updated if an error occurs. The file can be restored back to its
|
||||
/// original state by truncating it to [`Self::image_size`].
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
file: &(dyn ReadWriteAt + Sync),
|
||||
ranges: Option<&[Range<u64>]>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let mut pos_file = UserPosFile::new(file);
|
||||
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm)?;
|
||||
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
|
||||
let tree_offset = self.image_size;
|
||||
let (root_digest, hash_tree_data) = match ranges {
|
||||
Some(r) => {
|
||||
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(self.tree_offset))
|
||||
pos_file
|
||||
.seek(SeekFrom::Start(tree_offset))
|
||||
.map_err(|e| Error::DataRead("HashTree::tree_data", e))?;
|
||||
|
||||
let mut hash_tree_data = reader
|
||||
let mut hash_tree_data = pos_file
|
||||
.read_vec_exact(self.tree_size as usize)
|
||||
.map_err(|e| Error::DataRead("HashTree::tree_data", e))?;
|
||||
|
||||
let root_digest = hash_tree
|
||||
.update(
|
||||
input,
|
||||
self.image_size,
|
||||
r,
|
||||
&mut hash_tree_data,
|
||||
cancel_signal,
|
||||
)
|
||||
.update(file, self.image_size, r, &mut hash_tree_data, cancel_signal)
|
||||
.map_err(Error::HashTreeUpdate)?;
|
||||
|
||||
(root_digest, hash_tree_data)
|
||||
}
|
||||
None => hash_tree
|
||||
.generate(input, self.image_size, cancel_signal)
|
||||
.generate(file, self.image_size, cancel_signal)
|
||||
.map_err(Error::HashTreeGenerate)?,
|
||||
};
|
||||
|
||||
@@ -569,11 +557,10 @@ impl HashTreeDescriptor {
|
||||
|
||||
let tree_size = hash_tree_data.len() as u64;
|
||||
|
||||
let mut writer = output.reopen_boxed().map_err(Error::OutputReopen)?;
|
||||
writer
|
||||
.seek(SeekFrom::Start(self.image_size))
|
||||
pos_file
|
||||
.seek(SeekFrom::Start(tree_offset))
|
||||
.map_err(|e| Error::DataWrite("HashTree::tree_data", e))?;
|
||||
writer
|
||||
pos_file
|
||||
.write_all(&hash_tree_data)
|
||||
.map_err(|e| Error::DataWrite("HashTree::tree_data", e))?;
|
||||
|
||||
@@ -588,58 +575,54 @@ impl HashTreeDescriptor {
|
||||
|
||||
let parity: u8 = util::try_cast(self.fec_num_roots)
|
||||
.map_err(|e| Error::IntOutOfBounds("HashTree::fec_num_roots", e))?;
|
||||
let fec_offset = tree_offset + tree_size;
|
||||
|
||||
let fec_data = if let Some(r) = ranges {
|
||||
let mut r_with_hash_tree = r.to_vec();
|
||||
r_with_hash_tree.push(self.tree_offset..self.tree_offset + tree_size);
|
||||
r_with_hash_tree.push(tree_offset..tree_offset + tree_size);
|
||||
|
||||
let (fec, fec_size) = self.get_fec()?;
|
||||
|
||||
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(self.fec_offset))
|
||||
pos_file
|
||||
.seek(SeekFrom::Start(fec_offset))
|
||||
.map_err(|e| Error::DataRead("HashTree::fec_data", e))?;
|
||||
|
||||
let mut fec_data = reader
|
||||
let mut fec_data = pos_file
|
||||
.read_vec_exact(fec_size)
|
||||
.map_err(|e| Error::DataRead("HashTree::fec_data", e))?;
|
||||
|
||||
fec.update(input, &r_with_hash_tree, &mut fec_data, cancel_signal)
|
||||
fec.update(file, &r_with_hash_tree, &mut fec_data, cancel_signal)
|
||||
.map_err(Error::FecUpdate)?;
|
||||
|
||||
fec_data
|
||||
} else {
|
||||
// The FEC covers the hash tree as well.
|
||||
let fec = Fec::new(self.image_size + tree_size, self.data_block_size, parity)
|
||||
.map_err(Error::FecInit)?;
|
||||
fec.generate(input, cancel_signal)
|
||||
let fec =
|
||||
Fec::new(fec_offset, self.data_block_size, parity).map_err(Error::FecInit)?;
|
||||
fec.generate(file, cancel_signal)
|
||||
.map_err(Error::FecGenerate)?
|
||||
};
|
||||
|
||||
// Already seeked to FEC.
|
||||
writer
|
||||
pos_file
|
||||
.seek(SeekFrom::Start(fec_offset))
|
||||
.map_err(|e| Error::DataWrite("HashTree::fec_data", e))?;
|
||||
pos_file
|
||||
.write_all(&fec_data)
|
||||
.map_err(|e| Error::DataWrite("HashTree::fec_data", e))?;
|
||||
|
||||
self.fec_offset = self.image_size + tree_size;
|
||||
self.fec_offset = fec_offset;
|
||||
self.fec_size = fec_data.len() as u64;
|
||||
}
|
||||
|
||||
self.tree_offset = self.image_size;
|
||||
self.tree_offset = tree_offset;
|
||||
self.tree_size = tree_size;
|
||||
self.root_digest = root_digest;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the root hash, hash tree, and FEC data. `open_input` will be
|
||||
/// called from multiple threads and must return independently seekable
|
||||
/// handles to the same file.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
/// Verify the root hash, hash tree, and FEC data.
|
||||
pub fn verify(&self, input: &(dyn ReadAt + Sync), cancel_signal: &AtomicBool) -> Result<()> {
|
||||
self.check_offsets()?;
|
||||
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm)?;
|
||||
@@ -647,7 +630,7 @@ impl HashTreeDescriptor {
|
||||
util::check_bounds(self.tree_size, ..=HASH_TREE_MAX_SIZE)
|
||||
.map_err(|e| Error::IntOutOfBounds("HashTree::tree_size", e))?;
|
||||
|
||||
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let mut reader = UserPosFile::new(input);
|
||||
reader
|
||||
.seek(SeekFrom::Start(self.tree_offset))
|
||||
.map_err(|e| Error::DataRead("HashTree::tree_data", e))?;
|
||||
@@ -684,9 +667,7 @@ impl HashTreeDescriptor {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to repair errors in the input file using the FEC data. Both
|
||||
/// `open_input` and `open_output` may be called from multiple threads and
|
||||
/// must return independently seekable handles to the same file.
|
||||
/// Try to repair errors in the input file using the FEC data.
|
||||
///
|
||||
/// Due to the nature of FEC, when there are too many errors, it's possible
|
||||
/// for the data to be miscorrected to a "valid" state. [`Self::verify()`]
|
||||
@@ -694,8 +675,7 @@ impl HashTreeDescriptor {
|
||||
/// actually valid.
|
||||
pub fn repair(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
file: &(dyn ReadWriteAt + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
self.check_offsets()?;
|
||||
@@ -705,7 +685,7 @@ impl HashTreeDescriptor {
|
||||
return Err(Error::FecMissing);
|
||||
}
|
||||
|
||||
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let mut reader = UserPosFile::new(file);
|
||||
reader
|
||||
.seek(SeekFrom::Start(self.fec_offset))
|
||||
.map_err(|e| Error::DataRead("HashTree::fec_data", e))?;
|
||||
@@ -717,7 +697,7 @@ impl HashTreeDescriptor {
|
||||
.read_vec_exact(fec_size)
|
||||
.map_err(|e| Error::DataRead("HashTree::fec_data", e))?;
|
||||
|
||||
fec.repair(input, output, &fec_data, cancel_signal)
|
||||
fec.repair(file, &fec_data, cancel_signal)
|
||||
.map_err(Error::FecRepair)?;
|
||||
|
||||
Ok(())
|
||||
@@ -1657,7 +1637,7 @@ impl Header {
|
||||
|
||||
/// Get the first hash or hash tree descriptor if there is only one. This is
|
||||
/// the case for appended AVB images.
|
||||
pub fn appended_descriptor(&self) -> Result<AppendedDescriptorRef> {
|
||||
pub fn appended_descriptor(&self) -> Result<AppendedDescriptorRef<'_>> {
|
||||
let mut result = None;
|
||||
|
||||
for descriptor in &self.descriptors {
|
||||
@@ -1683,7 +1663,7 @@ impl Header {
|
||||
|
||||
/// Get the first hash or hash tree descriptor if there is only one. This is
|
||||
/// the case for appended AVB images.
|
||||
pub fn appended_descriptor_mut(&mut self) -> Result<AppendedDescriptorMut> {
|
||||
pub fn appended_descriptor_mut(&mut self) -> Result<AppendedDescriptorMut<'_>> {
|
||||
let mut result = None;
|
||||
|
||||
for descriptor in &mut self.descriptors {
|
||||
@@ -2178,10 +2158,10 @@ pub fn write_appended_image(
|
||||
.and_then(|s| s.checked_add(header_padding))
|
||||
.ok_or(Error::IntOverflow("Appended::header_end_padded"))?;
|
||||
|
||||
if let Some(s) = image_size {
|
||||
if header_end_padded > s {
|
||||
return Err(Error::TooSmallForHeader(s));
|
||||
}
|
||||
if let Some(s) = image_size
|
||||
&& header_end_padded > s
|
||||
{
|
||||
return Err(Error::TooSmallForHeader(s));
|
||||
}
|
||||
|
||||
writer
|
||||
|
||||
@@ -387,14 +387,13 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(v1) = &v1_data {
|
||||
if reader
|
||||
if let Some(v1) = &v1_data
|
||||
&& reader
|
||||
.stream_position()
|
||||
.map_err(|e| Error::DataRead("Boot::V1::header_size", e))?
|
||||
!= u64::from(v1.header_size)
|
||||
{
|
||||
return Err(Error::InvalidHeaderSize(v1.header_size));
|
||||
}
|
||||
{
|
||||
return Err(Error::InvalidHeaderSize(v1.header_size));
|
||||
}
|
||||
|
||||
padding::read_discard(&mut reader, page_size.into())
|
||||
@@ -870,14 +869,12 @@ impl BootImageV3Through4 {
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())
|
||||
.map_err(|e| Error::DataWrite("Boot::V3::ramdisk_padding", e))?;
|
||||
|
||||
if !skip_v4_sig {
|
||||
if let Some(sig) = v4_signature {
|
||||
writer
|
||||
.write_all(&sig)
|
||||
.map_err(|e| Error::DataWrite("Boot::V4::signature", e))?;
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())
|
||||
.map_err(|e| Error::DataWrite("Boot::V4::signature_padding", e))?;
|
||||
}
|
||||
if !skip_v4_sig && let Some(sig) = v4_signature {
|
||||
writer
|
||||
.write_all(&sig)
|
||||
.map_err(|e| Error::DataWrite("Boot::V4::signature", e))?;
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())
|
||||
.map_err(|e| Error::DataWrite("Boot::V4::signature_padding", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
|
||||
use liblzma::{
|
||||
read::XzDecoder,
|
||||
stream::{Check, Stream},
|
||||
write::XzEncoder,
|
||||
use flate2::{
|
||||
Compression,
|
||||
read::{DeflateDecoder, GzDecoder},
|
||||
write::{DeflateEncoder, GzEncoder},
|
||||
};
|
||||
use lz4_flex::frame::FrameDecoder;
|
||||
use lzma_rust2::{CheckType, XzOptions, XzReader, XzWriter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -28,7 +28,7 @@ pub enum Error {
|
||||
#[error("Failed to initialize legacy LZ4 encoder")]
|
||||
Lz4Init(#[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
XzInit(#[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -109,6 +109,7 @@ impl<W: Write> Write for Lz4LegacyEncoder<W> {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum CompressedFormat {
|
||||
None,
|
||||
Deflate,
|
||||
Gzip,
|
||||
Lz4Legacy,
|
||||
Xz,
|
||||
@@ -116,9 +117,44 @@ pub enum CompressedFormat {
|
||||
|
||||
pub enum CompressedReader<R: Read> {
|
||||
None(R),
|
||||
/// Not autodetected.
|
||||
Deflate(DeflateDecoder<R>),
|
||||
Gzip(GzDecoder<R>),
|
||||
Lz4(FrameDecoder<R>),
|
||||
Xz(XzDecoder<R>),
|
||||
/// Boxed because the [`XzReader`] is nearly 4 KiB.
|
||||
Xz(Box<XzReader<R>>),
|
||||
}
|
||||
|
||||
impl<R: Read> CompressedReader<R> {
|
||||
pub fn with_format(reader: R, format: CompressedFormat) -> Self {
|
||||
match format {
|
||||
CompressedFormat::None => Self::None(reader),
|
||||
CompressedFormat::Deflate => Self::Deflate(DeflateDecoder::new(reader)),
|
||||
CompressedFormat::Gzip => Self::Gzip(GzDecoder::new(reader)),
|
||||
CompressedFormat::Lz4Legacy => Self::Lz4(FrameDecoder::new(reader)),
|
||||
CompressedFormat::Xz => Self::Xz(Box::new(XzReader::new(reader, false))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self) -> CompressedFormat {
|
||||
match self {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Deflate(_) => CompressedFormat::Deflate,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> R {
|
||||
match self {
|
||||
Self::None(r) => r,
|
||||
Self::Deflate(r) => r.into_inner(),
|
||||
Self::Gzip(r) => r.into_inner(),
|
||||
Self::Lz4(r) => r.into_inner(),
|
||||
Self::Xz(r) => r.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> CompressedReader<R> {
|
||||
@@ -132,37 +168,20 @@ impl<R: Read + Seek> CompressedReader<R> {
|
||||
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
|
||||
Ok(Self::Lz4(FrameDecoder::new(reader)))
|
||||
} else if &magic == XZ_MAGIC {
|
||||
Ok(Self::Xz(XzDecoder::new(reader)))
|
||||
Ok(Self::Xz(Box::new(XzReader::new(reader, false))))
|
||||
} else if raw_if_unknown {
|
||||
Ok(Self::None(reader))
|
||||
} else {
|
||||
Err(Error::UnknownFormat)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format(&self) -> CompressedFormat {
|
||||
match self {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> R {
|
||||
match self {
|
||||
Self::None(r) => r,
|
||||
Self::Gzip(r) => r.into_inner(),
|
||||
Self::Lz4(r) => r.into_inner(),
|
||||
Self::Xz(r) => r.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for CompressedReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::None(r) => r.read(buf),
|
||||
Self::Deflate(r) => r.read(buf),
|
||||
Self::Gzip(r) => r.read(buf),
|
||||
Self::Lz4(r) => r.read(buf),
|
||||
Self::Xz(r) => r.read(buf),
|
||||
@@ -170,17 +189,23 @@ impl<R: Read> Read for CompressedReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum CompressedWriter<W: Write> {
|
||||
None(W),
|
||||
Deflate(DeflateEncoder<W>),
|
||||
Gzip(GzEncoder<W>),
|
||||
Lz4Legacy(Lz4LegacyEncoder<W>),
|
||||
Xz(XzEncoder<W>),
|
||||
Xz(XzWriter<W>),
|
||||
}
|
||||
|
||||
impl<W: Write> CompressedWriter<W> {
|
||||
pub fn new(writer: W, format: CompressedFormat) -> Result<Self> {
|
||||
match format {
|
||||
CompressedFormat::None => Ok(Self::None(writer)),
|
||||
CompressedFormat::Deflate => Ok(Self::Deflate(DeflateEncoder::new(
|
||||
writer,
|
||||
Compression::default(),
|
||||
))),
|
||||
CompressedFormat::Gzip => {
|
||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||
}
|
||||
@@ -190,8 +215,11 @@ impl<W: Write> CompressedWriter<W> {
|
||||
}
|
||||
CompressedFormat::Xz => {
|
||||
// Some kernels are compiled without support for the default CRC64.
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32).map_err(Error::XzInit)?;
|
||||
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
|
||||
let mut options = XzOptions::with_preset(6);
|
||||
options.set_check_sum_type(CheckType::Crc32);
|
||||
|
||||
let xz_writer = XzWriter::new(writer, options).map_err(Error::XzInit)?;
|
||||
Ok(Self::Xz(xz_writer))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,6 +227,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
pub fn format(&self) -> CompressedFormat {
|
||||
match self {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Deflate(_) => CompressedFormat::Deflate,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
@@ -208,6 +237,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
pub fn finish(self) -> io::Result<W> {
|
||||
match self {
|
||||
Self::None(w) => Ok(w),
|
||||
Self::Deflate(w) => w.finish(),
|
||||
Self::Gzip(w) => w.finish(),
|
||||
Self::Lz4Legacy(w) => w.finish(),
|
||||
Self::Xz(w) => w.finish(),
|
||||
@@ -219,6 +249,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::None(w) => w.write(buf),
|
||||
Self::Deflate(w) => w.write(buf),
|
||||
Self::Gzip(w) => w.write(buf),
|
||||
Self::Lz4Legacy(w) => w.write(buf),
|
||||
Self::Xz(w) => w.write(buf),
|
||||
@@ -228,6 +259,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::None(w) => w.flush(),
|
||||
Self::Deflate(w) => w.flush(),
|
||||
Self::Gzip(w) => w.flush(),
|
||||
Self::Lz4Legacy(w) => w.flush(),
|
||||
Self::Xz(w) => w.flush(),
|
||||
|
||||
@@ -89,12 +89,10 @@ impl fmt::Debug for RawHexU32 {
|
||||
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;
|
||||
for c in buf.iter_mut().rev() {
|
||||
*c = char::from_digit(value & 0xf, 16).unwrap() as u8;
|
||||
value >>= 4;
|
||||
index -= 1;
|
||||
}
|
||||
|
||||
Self(buf)
|
||||
|
||||
+61
-86
@@ -1,10 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt,
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
io::{self, Read, SeekFrom, Write},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
@@ -21,7 +21,10 @@ use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::verityrs,
|
||||
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
|
||||
stream::{
|
||||
self, FromReader, ReadAt, ReadSeek, ReadWriteAt, ReadWriteSeek, ToWriter, UserPosFile,
|
||||
WriteSeek, WriteZerosExt,
|
||||
},
|
||||
util::{self, NumBytes, OutOfBoundsError},
|
||||
};
|
||||
|
||||
@@ -68,10 +71,8 @@ pub enum 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 get input file size")]
|
||||
InputSize(#[source] io::Error),
|
||||
#[error("Failed to read FEC data: {0}")]
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write FEC data: {0}")]
|
||||
@@ -248,7 +249,7 @@ impl Fec {
|
||||
/// slice in the file offset grid.
|
||||
fn read_seq_block(
|
||||
&self,
|
||||
mut reader: impl Read + Seek,
|
||||
reader: &mut dyn ReadSeek,
|
||||
offset: u64,
|
||||
buf: &mut [u8],
|
||||
) -> io::Result<()> {
|
||||
@@ -276,7 +277,7 @@ impl Fec {
|
||||
/// slice in the file offset grid.
|
||||
fn write_seq_block(
|
||||
&self,
|
||||
mut writer: impl Write + Seek,
|
||||
writer: &mut dyn WriteSeek,
|
||||
offset: u64,
|
||||
buf: &[u8],
|
||||
) -> io::Result<()> {
|
||||
@@ -299,7 +300,7 @@ impl Fec {
|
||||
|
||||
/// Read the nth round from the file. The data is laid out sequentially
|
||||
/// (row-by-row).
|
||||
fn read_round(&self, mut reader: impl Read + Seek, round: u64) -> io::Result<Vec<u8>> {
|
||||
fn read_round(&self, reader: &mut dyn ReadSeek, round: u64) -> io::Result<Vec<u8>> {
|
||||
let mut grid = vec![0u8; usize::from(self.rs_k) * self.block_size as usize];
|
||||
|
||||
for row in 0..self.rs_k {
|
||||
@@ -309,7 +310,7 @@ impl Fec {
|
||||
let row_end = row_start + self.block_size as usize;
|
||||
let row_slice = &mut grid[row_start..row_end];
|
||||
|
||||
self.read_seq_block(&mut reader, interleaved_offset, row_slice)?;
|
||||
self.read_seq_block(reader, interleaved_offset, row_slice)?;
|
||||
}
|
||||
|
||||
Ok(grid)
|
||||
@@ -317,12 +318,7 @@ impl Fec {
|
||||
|
||||
/// Write the nth round to the file. The data is expected to be laid out
|
||||
/// sequentially (row-by-row).
|
||||
fn write_round(
|
||||
&self,
|
||||
mut writer: impl Write + Seek,
|
||||
round: u64,
|
||||
grid: &[u8],
|
||||
) -> io::Result<()> {
|
||||
fn write_round(&self, writer: &mut dyn WriteSeek, round: u64, grid: &[u8]) -> io::Result<()> {
|
||||
for row in 0..self.rs_k {
|
||||
let interleaved_offset =
|
||||
round * u64::from(self.rs_k) * u64::from(self.block_size) + u64::from(row);
|
||||
@@ -330,7 +326,7 @@ impl Fec {
|
||||
let row_end = row_start + self.block_size as usize;
|
||||
let row_slice = &grid[row_start..row_end];
|
||||
|
||||
self.write_seq_block(&mut writer, interleaved_offset, row_slice)?;
|
||||
self.write_seq_block(writer, interleaved_offset, row_slice)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -360,7 +356,7 @@ impl Fec {
|
||||
/// Generate FEC data for a single round.
|
||||
fn generate_one_round(
|
||||
&self,
|
||||
reader: impl Read + Seek,
|
||||
reader: &mut dyn ReadSeek,
|
||||
round: u64,
|
||||
fec: &mut [u8],
|
||||
) -> Result<()> {
|
||||
@@ -386,7 +382,7 @@ impl Fec {
|
||||
}
|
||||
|
||||
/// Verify file data for a single round.
|
||||
fn verify_one_round(&self, reader: impl Read + Seek, round: u64, fec: &[u8]) -> Result<()> {
|
||||
fn verify_one_round(&self, reader: &mut dyn ReadSeek, round: u64, fec: &[u8]) -> Result<()> {
|
||||
assert_eq!(
|
||||
fec.len(),
|
||||
usize::from(self.parity()) * self.block_size as usize,
|
||||
@@ -414,8 +410,7 @@ impl Fec {
|
||||
/// Repair file data for a single round.
|
||||
fn repair_one_round(
|
||||
&self,
|
||||
reader: impl Read + Seek,
|
||||
writer: impl Write + Seek,
|
||||
file: &mut dyn ReadWriteSeek,
|
||||
round: u64,
|
||||
fec: &[u8],
|
||||
) -> Result<u64> {
|
||||
@@ -426,7 +421,7 @@ impl Fec {
|
||||
);
|
||||
|
||||
let mut grid = self
|
||||
.read_round(reader, round)
|
||||
.read_round(file, round)
|
||||
.map_err(|e| Error::DataRead("round", e))?;
|
||||
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
@@ -445,7 +440,7 @@ impl Fec {
|
||||
}
|
||||
|
||||
if num_corrected > 0 {
|
||||
self.write_round(writer, round, &grid)
|
||||
self.write_round(file, round, &grid)
|
||||
.map_err(|e| Error::DataWrite("round", e))?;
|
||||
}
|
||||
|
||||
@@ -458,7 +453,7 @@ impl Fec {
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn generate(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let fec_size = self.fec_size();
|
||||
@@ -466,13 +461,12 @@ impl Fec {
|
||||
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
.try_for_each(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
|
||||
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
let mut reader = UserPosFile::new(input);
|
||||
self.generate_one_round(&mut reader, round as u64, buf)
|
||||
})?;
|
||||
|
||||
Ok(fec)
|
||||
}
|
||||
@@ -482,7 +476,7 @@ impl Fec {
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn update(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
fec: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -501,15 +495,12 @@ impl Fec {
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
.try_for_each(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
|
||||
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
let mut reader = UserPosFile::new(input);
|
||||
self.generate_one_round(&mut reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify that the file contains no errors. This is significantly faster
|
||||
@@ -519,7 +510,7 @@ impl Fec {
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
fec: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -534,15 +525,12 @@ impl Fec {
|
||||
|
||||
fec.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
.try_for_each(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
|
||||
|
||||
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
self.verify_one_round(reader, round as u64, buf)
|
||||
let mut reader = UserPosFile::new(input);
|
||||
self.verify_one_round(&mut reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repair the file. Up to `parity / 2` bytes per codeword can be repaired.
|
||||
@@ -558,8 +546,7 @@ impl Fec {
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn repair(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
file: &(dyn ReadWriteAt + Sync),
|
||||
fec: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<u64> {
|
||||
@@ -576,15 +563,12 @@ impl Fec {
|
||||
.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<u64> {
|
||||
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
|
||||
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
|
||||
|
||||
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)
|
||||
let mut file = UserPosFile::new(file);
|
||||
self.repair_one_round(&mut file, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<Vec<u64>>>()?
|
||||
.into_iter()
|
||||
.sum();
|
||||
.try_reduce(|| 0, |prev, cur| Ok(prev + cur))?;
|
||||
|
||||
Ok(num_corrected)
|
||||
}
|
||||
@@ -636,14 +620,11 @@ impl FecImage {
|
||||
/// Generate FEC data for a file. `parity` is the number of parity bytes per
|
||||
/// 255-byte Reed-Solomon codeword.
|
||||
pub fn generate(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
parity: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let data_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let data_size = input.file_len().map_err(Error::InputSize)?;
|
||||
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
|
||||
let fec_data = fec.generate(input, cancel_signal)?;
|
||||
|
||||
@@ -657,7 +638,7 @@ impl FecImage {
|
||||
/// Update FEC data coreesponding to the specified file ranges.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -667,11 +648,7 @@ impl FecImage {
|
||||
|
||||
/// Check that a file contains no errors. This is significantly faster than
|
||||
/// [`Self::repair()`] if performing a repair is not necessary.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
pub fn verify(&self, input: &(dyn ReadAt + Sync), cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.verify(input, &self.fec, cancel_signal)
|
||||
}
|
||||
@@ -687,18 +664,13 @@ impl FecImage {
|
||||
/// possible for there to be a false positive where the corrupted codeword
|
||||
/// is "corrected" into an incorrect value. FEC error detection is not a
|
||||
/// replacement for cryptographically secure digests.
|
||||
///
|
||||
/// The inputs and outputs should point to the same underlying file because
|
||||
/// only regions where errors are corrected are written. It is guaranteed
|
||||
/// that multiple threads will always read and write disjoint file offsets.
|
||||
pub fn repair(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
file: &(dyn ReadWriteAt + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<u64> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.repair(input, output, &self.fec, cancel_signal)
|
||||
fec.repair(file, &self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// Build one instance of the FEC header. The caller is responsible for
|
||||
@@ -833,7 +805,7 @@ mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use rand::RngCore;
|
||||
|
||||
use crate::stream::SharedCursor;
|
||||
use crate::stream::MutexFile;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -862,7 +834,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn corrupt_byte(file: &mut SharedCursor, offset: u64) {
|
||||
fn corrupt_byte(file: &mut UserPosFile<&MutexFile<Cursor<Vec<u8>>>>, offset: u64) {
|
||||
let mut buf = [0u8; 1];
|
||||
|
||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||
@@ -881,11 +853,13 @@ mod tests {
|
||||
// Generate data big enough to span multiple rounds, but don't fill the
|
||||
// offset grid to ensure that the out-of-bounds-is-0 behavior works.
|
||||
let size = usize::from(rs_k) * block_size as usize * 3 - block_size as usize;
|
||||
let mut file = SharedCursor::default();
|
||||
let file = MutexFile::new(Cursor::new(Vec::new()));
|
||||
let mut pos_file = UserPosFile::new(&file);
|
||||
|
||||
let orig_digest = {
|
||||
let mut buf = vec![0u8; size];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
file.write_all(&buf).unwrap();
|
||||
pos_file.write_all(&buf).unwrap();
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
};
|
||||
|
||||
@@ -901,7 +875,7 @@ mod tests {
|
||||
fec.verify(&file, &fec_data, &cancel_signal).unwrap();
|
||||
|
||||
// Verify that errors are detected.
|
||||
corrupt_byte(&mut file, 0);
|
||||
corrupt_byte(&mut pos_file, 0);
|
||||
assert_matches!(
|
||||
fec.verify(&file, &fec_data, &cancel_signal),
|
||||
Err(Error::HasErrors)
|
||||
@@ -909,24 +883,24 @@ mod tests {
|
||||
|
||||
// Corrupt one byte in every single codeword.
|
||||
for offset in 1..num_codewords {
|
||||
corrupt_byte(&mut file, offset as u64);
|
||||
corrupt_byte(&mut pos_file, offset as u64);
|
||||
}
|
||||
|
||||
// Verify that all the single-byte errors can be fixed. We don't test
|
||||
// for Error::TooManyErrors because of the chance of false positives due
|
||||
// to the nature of RS.
|
||||
fec.repair(&file, &file, &fec_data, &cancel_signal).unwrap();
|
||||
fec.repair(&file, &fec_data, &cancel_signal).unwrap();
|
||||
|
||||
let repaired_digest = {
|
||||
let mut buf = Vec::new();
|
||||
file.rewind().unwrap();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
pos_file.rewind().unwrap();
|
||||
pos_file.read_to_end(&mut buf).unwrap();
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
};
|
||||
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
|
||||
|
||||
// Intentionally update some data.
|
||||
corrupt_byte(&mut file, 0);
|
||||
corrupt_byte(&mut pos_file, 0);
|
||||
let mut fec_data_updated = fec_data.clone();
|
||||
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
|
||||
fec.update(&file, &[0..1], &mut fec_data_updated, &cancel_signal)
|
||||
@@ -948,11 +922,12 @@ mod tests {
|
||||
fn round_trip_image() {
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut file = SharedCursor::default();
|
||||
let file = MutexFile::new(Cursor::new(Vec::new()));
|
||||
|
||||
{
|
||||
let mut buf = [0u8; FEC_BLOCK_SIZE];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
file.write_all(&buf).unwrap();
|
||||
UserPosFile::new(&file).write_all(&buf).unwrap();
|
||||
}
|
||||
|
||||
let image = FecImage::generate(&file, 2, &cancel_signal).unwrap();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, SeekFrom, Write},
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
avb,
|
||||
padding::{self, ZeroPadding},
|
||||
},
|
||||
stream::{self, FromReader, ReadFixedSizeExt, ReadSeekReopen, ToWriter},
|
||||
stream::{self, FromReader, ReadAt, ReadFixedSizeExt, ToWriter, UserPosFile},
|
||||
util::{self, NumBytes, OutOfBoundsError},
|
||||
};
|
||||
|
||||
@@ -50,8 +50,8 @@ pub enum 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 get input file size")]
|
||||
InputSize(#[source] io::Error),
|
||||
#[error("Failed to compute hash tree of input file")]
|
||||
InputDigest(#[source] io::Error),
|
||||
#[error("Failed to read hash tree data: {0}")]
|
||||
@@ -189,7 +189,7 @@ impl HashTree {
|
||||
/// Hash one full level in parallel.
|
||||
fn hash_one_level_parallel(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
size: u64,
|
||||
level_data: &mut [u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -207,26 +207,23 @@ impl HashTree {
|
||||
level_data
|
||||
.par_chunks_mut(digest_size * multiplier as usize)
|
||||
.enumerate()
|
||||
.map(|(chunk, out_data)| -> io::Result<()> {
|
||||
.try_for_each(|(chunk, out_data)| -> io::Result<()> {
|
||||
let digests = out_data.len() / digest_size;
|
||||
let in_start = (chunk as u64) * multiplier * u64::from(self.block_size);
|
||||
let in_size = ((digests as u64) * u64::from(self.block_size)).min(size - in_start);
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut reader = UserPosFile::new(input);
|
||||
reader.seek(SeekFrom::Start(in_start))?;
|
||||
|
||||
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
|
||||
})
|
||||
.collect::<io::Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update parts of the hash tree level corresponding to the specified
|
||||
/// blocks.
|
||||
fn hash_partial_level_parallel(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
size: u64,
|
||||
block_ranges: &[Range<u64>],
|
||||
level_data: &mut [u8],
|
||||
@@ -239,18 +236,15 @@ impl HashTree {
|
||||
.par_chunks_exact_mut(digest_size)
|
||||
.enumerate()
|
||||
.filter(|(chunk, _)| util::ranges_contains(block_ranges, &(*chunk as u64)))
|
||||
.map(|(chunk, out_data)| -> io::Result<()> {
|
||||
.try_for_each(|(chunk, out_data)| -> io::Result<()> {
|
||||
let in_start = (chunk as u64) * u64::from(self.block_size);
|
||||
let in_size = u64::from(self.block_size).min(size - in_start);
|
||||
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut reader = UserPosFile::new(input);
|
||||
reader.seek(SeekFrom::Start(in_start))?;
|
||||
|
||||
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
|
||||
})
|
||||
.collect::<io::Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the hash tree and return the root digest. If `ranges` is
|
||||
@@ -261,7 +255,7 @@ impl HashTree {
|
||||
/// offset of the leaf layer of the tree must equal `hash_tree_data`'s size.
|
||||
fn calculate(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
image_size: u64,
|
||||
ranges: Option<&[Range<u64>]>,
|
||||
level_offsets: &[Range<usize>],
|
||||
@@ -270,7 +264,7 @@ impl HashTree {
|
||||
) -> Result<Vec<u8>> {
|
||||
// Small files are hashed directly.
|
||||
if image_size <= u64::from(self.block_size) {
|
||||
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
|
||||
let mut reader = UserPosFile::new(input);
|
||||
let buf = reader
|
||||
.read_vec_exact(image_size as usize)
|
||||
.map_err(Error::InputDigest)?;
|
||||
@@ -334,7 +328,7 @@ impl HashTree {
|
||||
/// hash tree data.
|
||||
pub fn generate(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
image_size: u64,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<u8>, Vec<u8>)> {
|
||||
@@ -358,7 +352,7 @@ impl HashTree {
|
||||
/// Returns the new root digest.
|
||||
pub fn update(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
image_size: u64,
|
||||
ranges: &[Range<u64>],
|
||||
hash_tree_data: &mut [u8],
|
||||
@@ -387,7 +381,7 @@ impl HashTree {
|
||||
/// Verify that the file contains no errors.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
image_size: u64,
|
||||
root_digest: &[u8],
|
||||
hash_tree_data: &[u8],
|
||||
@@ -499,16 +493,13 @@ impl HashTreeImage {
|
||||
|
||||
/// Generate hash tree data for a file.
|
||||
pub fn generate(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
block_size: u32,
|
||||
algorithm: &str,
|
||||
salt: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let image_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut f| f.seek(SeekFrom::End(0)))
|
||||
.map_err(Error::InputReopen)?;
|
||||
let image_size = input.file_len().map_err(Error::InputSize)?;
|
||||
let digest_algorithm = Self::digest_algorithm(algorithm)?;
|
||||
let hash_tree = HashTree::new(block_size, digest_algorithm, salt);
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
|
||||
@@ -526,7 +517,7 @@ impl HashTreeImage {
|
||||
/// Update hash tree data coreesponding to the specified file ranges.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
ranges: &[Range<u64>],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
@@ -545,11 +536,7 @@ impl HashTreeImage {
|
||||
}
|
||||
|
||||
/// Check that a file contains no errors.
|
||||
pub fn verify(
|
||||
&self,
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
pub fn verify(&self, input: &(dyn ReadAt + Sync), cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
|
||||
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
|
||||
|
||||
@@ -656,7 +643,7 @@ mod tests {
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::stream::SharedCursor;
|
||||
use crate::stream::MutexFile;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -697,7 +684,8 @@ mod tests {
|
||||
fn generate_update_verify() {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
|
||||
let mut input = SharedCursor::new();
|
||||
let input = MutexFile::new(Cursor::new(Vec::new()));
|
||||
let mut pos_input = UserPosFile::new(&input);
|
||||
|
||||
// Try input smaller than one block.
|
||||
let (root_digest, hash_tree_data) = hash_tree.generate(&input, 0, &cancel_signal).unwrap();
|
||||
@@ -714,7 +702,7 @@ mod tests {
|
||||
|
||||
// Try larger input that spans multiple blocks are results in an actual
|
||||
// hash tree being created.
|
||||
input.write_all(&b"Data".repeat(25)).unwrap();
|
||||
pos_input.write_all(&b"Data".repeat(25)).unwrap();
|
||||
|
||||
let (root_digest, mut hash_tree_data) =
|
||||
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
|
||||
@@ -738,8 +726,8 @@ mod tests {
|
||||
);
|
||||
|
||||
// Change some data and update the hash tree.
|
||||
input.rewind().unwrap();
|
||||
input.write_all(b"Changed").unwrap();
|
||||
pos_input.rewind().unwrap();
|
||||
pos_input.write_all(b"Changed").unwrap();
|
||||
|
||||
let root_digest = hash_tree
|
||||
.update(&input, 100, &[0..7], &mut hash_tree_data, &cancel_signal)
|
||||
@@ -775,8 +763,8 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// But not if the data is corrupted.
|
||||
input.rewind().unwrap();
|
||||
input.write_all(b"Bad").unwrap();
|
||||
pos_input.rewind().unwrap();
|
||||
pos_input.write_all(b"Bad").unwrap();
|
||||
|
||||
hash_tree
|
||||
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
|
||||
|
||||
+131
-99
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
collections::BTreeMap,
|
||||
fmt::{self, Write as _},
|
||||
@@ -17,16 +18,16 @@ use cms::signed_data::SignedData;
|
||||
use const_oid::{ObjectIdentifier, db::rfc5912};
|
||||
use memchr::memmem;
|
||||
use prost::Message;
|
||||
use rawzip::{CompressionMethod, RECOMMENDED_BUFFER_SIZE, ZipArchive, ZipArchiveWriter};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
use x509_cert::{Certificate, der::Encode};
|
||||
use zip::{CompressionMethod, DateTime, ZipArchive, result::ZipError, write::SimpleFileOptions};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
format::{
|
||||
payload::{self, PayloadHeader},
|
||||
zip::ZipWriterWrapper,
|
||||
zip::{self, ZipEntriesSafeExt, ZipFileHeaderRecordExt},
|
||||
},
|
||||
protobuf::build::tools::releasetools::{OtaMetadata, ota_metadata::OtaType},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter, ReadFixedSizeExt},
|
||||
@@ -91,17 +92,21 @@ pub enum Error {
|
||||
#[error("Failed to decode OTA metadata protobuf message")]
|
||||
MetadataDecode(#[source] prost::DecodeError),
|
||||
#[error("Failed to open zip file")]
|
||||
ZipOpen(#[source] ZipError),
|
||||
ZipOpen(#[source] rawzip::Error),
|
||||
#[error("Failed to list zip entries")]
|
||||
ZipEntryList(#[source] rawzip::Error),
|
||||
#[error("Missing zip entry: {0:?}")]
|
||||
ZipEntryMissing(Cow<'static, str>),
|
||||
#[error("Failed to open zip entry: {0:?}")]
|
||||
ZipEntryOpen(&'static str, #[source] ZipError),
|
||||
ZipEntryOpen(Cow<'static, str>, #[source] rawzip::Error),
|
||||
#[error("Failed to start new zip entry: {0:?}")]
|
||||
ZipEntryStart(&'static str, #[source] ZipError),
|
||||
ZipEntryStart(Cow<'static, str>, #[source] rawzip::Error),
|
||||
#[error("Failed to read zip entry: {0:?}")]
|
||||
ZipEntryRead(&'static str, #[source] io::Error),
|
||||
ZipEntryRead(Cow<'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),
|
||||
ZipEntryWrite(Cow<'static, str>, #[source] io::Error),
|
||||
#[error("Failed to finalize zip entry: {0:?}")]
|
||||
ZipEntryFinish(Cow<'static, str>, #[source] rawzip::Error),
|
||||
#[error("Failed to load OTA certificate")]
|
||||
OtaCertLoad(#[source] crypto::Error),
|
||||
#[error("Failed to extract public key from OTA certificate")]
|
||||
@@ -120,6 +125,8 @@ pub enum Error {
|
||||
DataRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to write OTA data: {0}")]
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
#[error("Failed to convert to non-streaming zip")]
|
||||
MakeNonStreaming(#[source] rawzip::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -500,16 +507,36 @@ impl fmt::Display for ZipMode {
|
||||
/// directory would start.
|
||||
pub fn add_metadata(
|
||||
zip_entries: &[ZipEntry],
|
||||
zip_writer: &mut ZipWriterWrapper<impl Write>,
|
||||
zip_writer: &mut ZipArchiveWriter<impl Write>,
|
||||
next_offset: u64,
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
zip_mode: ZipMode,
|
||||
) -> Result<OtaMetadata> {
|
||||
fn write_entry(
|
||||
archive: &mut ZipArchiveWriter<impl Write>,
|
||||
path: &'static str,
|
||||
data: &[u8],
|
||||
) -> Result<(u64, u64)> {
|
||||
let (entry_writer, data_config) = archive
|
||||
.new_file(path)
|
||||
.start()
|
||||
.map_err(|e| Error::ZipEntryStart(path.into(), e))?;
|
||||
let data_offset = entry_writer.stream_offset();
|
||||
let mut data_writer = data_config.wrap(entry_writer);
|
||||
|
||||
data_writer
|
||||
.write_all(data)
|
||||
.map_err(|e| Error::ZipEntryWrite(path.into(), e))?;
|
||||
|
||||
let data_size = data_writer
|
||||
.finish()
|
||||
.and_then(|(w, d)| w.finish(d))
|
||||
.map_err(|e| Error::ZipEntryFinish(path.into(), e))?;
|
||||
|
||||
Ok((data_offset, data_size))
|
||||
}
|
||||
|
||||
let mut metadata = metadata.clone();
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(CompressionMethod::Stored);
|
||||
|
||||
let mut prop_entries = zip_entries.iter().map(PropEntry::from).collect();
|
||||
add_payload_metadata_entry(&mut prop_entries, payload_metadata_size)?;
|
||||
@@ -528,34 +555,25 @@ pub fn add_metadata(
|
||||
let (temp_legacy_offset, temp_modern_offset) = {
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = match zip_mode {
|
||||
ZipMode::Streaming => ZipWriterWrapper::new_streaming(raw_writer),
|
||||
ZipMode::Seekable => ZipWriterWrapper::new_seekable(raw_writer),
|
||||
};
|
||||
// Note that we don't need to worry about the offsets changing based on
|
||||
// the zip writing mode (streaming vs. seekable). Currently, we always
|
||||
// include data descriptors and do post-processing to copy the fields
|
||||
// into the local header without shifting the data.
|
||||
let mut writer = ZipArchiveWriter::new(raw_writer);
|
||||
|
||||
let legacy_offset = writer
|
||||
.start_file(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
writer
|
||||
.write_all(legacy_raw.as_bytes())
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
|
||||
|
||||
let modern_offset = writer
|
||||
.start_file(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
writer
|
||||
.write_all(&modern_raw)
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
|
||||
let (legacy_offset, legacy_size) =
|
||||
write_entry(&mut writer, PATH_METADATA, legacy_raw.as_bytes())?;
|
||||
let (modern_offset, modern_size) = write_entry(&mut writer, PATH_METADATA_PB, &modern_raw)?;
|
||||
|
||||
prop_entries.push(PropEntry::new(
|
||||
PATH_METADATA,
|
||||
next_offset + legacy_offset,
|
||||
legacy_raw.len() as u64,
|
||||
legacy_size,
|
||||
));
|
||||
prop_entries.push(PropEntry::new(
|
||||
PATH_METADATA_PB,
|
||||
next_offset + modern_offset,
|
||||
modern_raw.len() as u64,
|
||||
modern_size,
|
||||
));
|
||||
|
||||
(next_offset + legacy_offset, next_offset + modern_offset)
|
||||
@@ -570,19 +588,8 @@ pub fn add_metadata(
|
||||
{
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
|
||||
|
||||
let legacy_offset = zip_writer
|
||||
.start_file(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
zip_writer
|
||||
.write_all(legacy_raw.as_bytes())
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
|
||||
|
||||
let modern_offset = zip_writer
|
||||
.start_file(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
zip_writer
|
||||
.write_all(&modern_raw)
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
|
||||
let (legacy_offset, _) = write_entry(zip_writer, PATH_METADATA, legacy_raw.as_bytes())?;
|
||||
let (modern_offset, _) = write_entry(zip_writer, PATH_METADATA_PB, &modern_raw)?;
|
||||
|
||||
assert_eq!(legacy_offset, temp_legacy_offset);
|
||||
assert_eq!(modern_offset, temp_modern_offset);
|
||||
@@ -597,23 +604,24 @@ pub fn verify_metadata(
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<()> {
|
||||
let mut zip_reader = ZipArchive::new(reader).map_err(Error::ZipOpen)?;
|
||||
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
|
||||
let archive = ZipArchive::from_seekable(reader, &mut buffer).map_err(Error::ZipOpen)?;
|
||||
let mut zip_entries = vec![];
|
||||
|
||||
for i in 0..zip_reader.len() {
|
||||
let entry = zip_reader
|
||||
.by_index(i)
|
||||
.map_err(|e| Error::ZipIndexOpen(i, e))?;
|
||||
let mut entries = archive.entries_safe(&mut buffer);
|
||||
|
||||
if entry.compression() != CompressionMethod::Stored {
|
||||
while let Some((cd_entry, entry)) = entries.next_entry().map_err(Error::ZipEntryList)? {
|
||||
if cd_entry.compression_method() != CompressionMethod::Store {
|
||||
continue;
|
||||
}
|
||||
|
||||
zip_entries.push(PropEntry::new(
|
||||
entry.name(),
|
||||
entry.data_start(),
|
||||
entry.size(),
|
||||
));
|
||||
let Ok(path) = cd_entry.file_path_utf8() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let range = entry.compressed_data_range();
|
||||
|
||||
zip_entries.push(PropEntry::new(path, range.0, range.1 - range.0));
|
||||
}
|
||||
|
||||
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
|
||||
@@ -808,7 +816,7 @@ fn parse_raw_ota_sig(mut reader: impl Read + Seek) -> Result<RawOtaSignature> {
|
||||
.map_err(|e| Error::DataRead("file_size", e))?;
|
||||
|
||||
reader
|
||||
.seek(SeekFrom::Current(-6))
|
||||
.seek_relative(-6)
|
||||
.map_err(|e| Error::DataRead("footer", e))?;
|
||||
let footer = reader
|
||||
.read_array_exact::<6>()
|
||||
@@ -870,54 +878,73 @@ pub fn parse_ota_sig(reader: impl Read + Seek) -> Result<OtaSignature> {
|
||||
pub fn parse_zip_ota_info(
|
||||
reader: impl Read + Seek,
|
||||
) -> Result<(OtaMetadata, Certificate, PayloadHeader, String)> {
|
||||
let mut zip = ZipArchive::new(reader).map_err(Error::ZipOpen)?;
|
||||
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
|
||||
let archive = ZipArchive::from_seekable(reader, &mut buffer).map_err(Error::ZipOpen)?;
|
||||
|
||||
let metadata = match zip.by_name(PATH_METADATA_PB) {
|
||||
Ok(mut entry) => {
|
||||
let mut metadata_modern = None;
|
||||
let mut metadata_legacy = None;
|
||||
let mut certificate = None;
|
||||
let mut header = None;
|
||||
let mut properties = None;
|
||||
|
||||
let mut entries = archive.entries_safe(&mut buffer);
|
||||
|
||||
while let Some((cd_entry, entry)) = entries.next_entry().map_err(Error::ZipEntryList)? {
|
||||
let path = cd_entry.file_path_utf8().map_err(Error::ZipEntryList)?;
|
||||
|
||||
if path == PATH_METADATA_PB {
|
||||
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_METADATA_PB.into(), e))?;
|
||||
let mut buf = Vec::new();
|
||||
entry
|
||||
|
||||
reader
|
||||
.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))?;
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_METADATA_PB.into(), e))?;
|
||||
|
||||
metadata_modern = Some(parse_protobuf_metadata(&buf)?);
|
||||
} else if path == PATH_METADATA {
|
||||
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_METADATA.into(), e))?;
|
||||
let mut buf = String::new();
|
||||
entry
|
||||
|
||||
reader
|
||||
.read_to_string(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_METADATA, e))?;
|
||||
parse_legacy_metadata(&buf)?
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_METADATA.into(), e))?;
|
||||
|
||||
metadata_legacy = Some(parse_legacy_metadata(&buf)?);
|
||||
} else if path == PATH_OTACERT {
|
||||
let reader = zip::verifying_reader(&entry, cd_entry.compression_method())
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_OTACERT.into(), e))?;
|
||||
|
||||
certificate = Some(
|
||||
crypto::read_pem_cert(Path::new(PATH_OTACERT), reader)
|
||||
.map_err(Error::OtaCertLoad)?,
|
||||
);
|
||||
} else if path == PATH_PAYLOAD {
|
||||
// No CRC validation because we only read the header.
|
||||
let reader = zip::compressed_reader(&entry, cd_entry.compression_method())
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_PAYLOAD.into(), e))?;
|
||||
|
||||
header = Some(PayloadHeader::from_reader(reader).map_err(Error::PayloadLoad)?);
|
||||
} else if path == PATH_PROPERTIES {
|
||||
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_PROPERTIES.into(), e))?;
|
||||
let mut buf = String::new();
|
||||
|
||||
reader
|
||||
.read_to_string(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_PROPERTIES.into(), e))?;
|
||||
|
||||
properties = Some(buf);
|
||||
}
|
||||
Err(e) => return Err(Error::ZipEntryOpen(PATH_METADATA_PB, e)),
|
||||
};
|
||||
}
|
||||
|
||||
let certificate = {
|
||||
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)
|
||||
.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)
|
||||
.map_err(|e| Error::ZipEntryOpen(PATH_PROPERTIES, e))?;
|
||||
let mut buf = String::new();
|
||||
entry
|
||||
.read_to_string(&mut buf)
|
||||
.map_err(|e| Error::ZipEntryRead(PATH_PROPERTIES, e))?;
|
||||
buf
|
||||
};
|
||||
let metadata = metadata_modern
|
||||
.or(metadata_legacy)
|
||||
.ok_or_else(|| Error::ZipEntryMissing(PATH_METADATA_PB.into()))?;
|
||||
let certificate = certificate.ok_or_else(|| Error::ZipEntryMissing(PATH_OTACERT.into()))?;
|
||||
let header = header.ok_or_else(|| Error::ZipEntryMissing(PATH_PAYLOAD.into()))?;
|
||||
let properties = properties.ok_or_else(|| Error::ZipEntryMissing(PATH_PROPERTIES.into()))?;
|
||||
|
||||
Ok((metadata, certificate, header, properties))
|
||||
}
|
||||
@@ -1077,6 +1104,11 @@ impl<W: Read + Write + Seek> SeekableSigningWriter<W> {
|
||||
cert: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<W> {
|
||||
// We always write a streaming zip because that is what rawzip supports.
|
||||
// Convert it to not be streaming. This will leave the data descriptors
|
||||
// behind, but that is fine.
|
||||
zip::make_non_streaming(&mut self.inner).map_err(Error::MakeNonStreaming)?;
|
||||
|
||||
let file_size = self
|
||||
.seek(SeekFrom::End(0))
|
||||
.map_err(|e| Error::DataRead("file_size", e))?;
|
||||
|
||||
+97
-111
@@ -15,11 +15,7 @@ use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use bzip2::write::BzDecoder;
|
||||
use flate2::{Compression, write::GzEncoder};
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
write::XzDecoder,
|
||||
write::XzEncoder,
|
||||
};
|
||||
use lzma_rust2::{CheckType, XzOptions, XzReader, XzWriter};
|
||||
use num_traits::CheckedAdd;
|
||||
use prost::Message;
|
||||
use rayon::{
|
||||
@@ -40,8 +36,8 @@ use crate::{
|
||||
install_operation::Type, signatures::Signature,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadFixedSizeExt,
|
||||
ReadSeekReopen, WriteSeek, WriteSeekReopen,
|
||||
self, CountingReader, FromReader, HashingReader, HashingWriter, ReadAt, ReadDiscardExt,
|
||||
ReadFixedSizeExt, ReadSeek, UserPosFile, WriteAt, WriteSeek,
|
||||
},
|
||||
util::{self, OutOfBoundsError},
|
||||
};
|
||||
@@ -130,16 +126,12 @@ pub enum Error {
|
||||
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 get input file size for partition: {0}")]
|
||||
InputSize(String, #[source] io::Error),
|
||||
#[error("Failed to GZ compress partition image chunk")]
|
||||
GzCompress(#[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
XzInit(#[source] io::Error),
|
||||
#[error("Failed to XZ compress partition image chunk")]
|
||||
XzCompress(#[source] io::Error),
|
||||
#[error("Failed to read uncompressed input partition image chunk")]
|
||||
@@ -596,12 +588,12 @@ impl<W: Write> Write for PayloadWriter<W> {
|
||||
/// Verify the payload signatures using the specified certificate and check that
|
||||
/// the digests in `payload_properties.txt` are correct.
|
||||
pub fn verify_payload(
|
||||
mut reader: impl Read + Seek,
|
||||
reader: &mut dyn ReadSeek,
|
||||
cert: &Certificate,
|
||||
properties_raw: &str,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let header = PayloadHeader::from_reader(&mut reader)?;
|
||||
let header = PayloadHeader::from_reader(&mut *reader)?;
|
||||
reader.rewind().map_err(|e| Error::DataRead("header", e))?;
|
||||
|
||||
let payload_signatures_offset = header
|
||||
@@ -621,7 +613,7 @@ pub fn verify_payload(
|
||||
// Read from the beginning to the metadata signature.
|
||||
let metadata_size = header.blob_offset - u64::from(header.metadata_signature_size);
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
&mut *reader,
|
||||
io::sink(),
|
||||
metadata_size,
|
||||
|data| {
|
||||
@@ -638,7 +630,7 @@ pub fn verify_payload(
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
&mut *reader,
|
||||
&mut writer,
|
||||
header.metadata_signature_size.into(),
|
||||
|data| h_full.update(data),
|
||||
@@ -668,7 +660,7 @@ pub fn verify_payload(
|
||||
|
||||
// Read (and discard) all the payload blobs.
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
&mut *reader,
|
||||
io::sink(),
|
||||
payload_signatures_offset,
|
||||
|data| {
|
||||
@@ -696,7 +688,7 @@ pub fn verify_payload(
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
&mut *reader,
|
||||
&mut writer,
|
||||
payload_signatures_size,
|
||||
|data| h_full.update(data),
|
||||
@@ -741,8 +733,8 @@ pub fn verify_payload(
|
||||
|
||||
/// Apply a partition operation from `reader` to `writer`.
|
||||
pub fn apply_operation(
|
||||
mut reader: impl Read + Seek,
|
||||
mut writer: impl Write + Seek,
|
||||
reader: &mut dyn ReadSeek,
|
||||
writer: &mut dyn WriteSeek,
|
||||
block_size: u32,
|
||||
blob_offset: u64,
|
||||
op: &InstallOperation,
|
||||
@@ -778,7 +770,7 @@ pub fn apply_operation(
|
||||
Type::Zero | Type::Discard => {
|
||||
stream::copy_n_inspect(
|
||||
io::repeat(0),
|
||||
&mut writer,
|
||||
&mut *writer,
|
||||
out_data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
@@ -797,8 +789,8 @@ pub fn apply_operation(
|
||||
match other {
|
||||
Type::Replace => {
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
&mut *reader,
|
||||
&mut *writer,
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
@@ -806,9 +798,9 @@ pub fn apply_operation(
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
Type::ReplaceBz => {
|
||||
let mut decoder = BzDecoder::new(&mut writer);
|
||||
let mut decoder = BzDecoder::new(&mut *writer);
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
&mut *reader,
|
||||
&mut decoder,
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
@@ -818,16 +810,15 @@ pub fn apply_operation(
|
||||
.map_err(error_fn)?;
|
||||
}
|
||||
Type::ReplaceXz => {
|
||||
let mut decoder = XzDecoder::new(&mut writer);
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
&mut decoder,
|
||||
data_length,
|
||||
|data| hasher.update(data),
|
||||
cancel_signal,
|
||||
)
|
||||
.and_then(|()| decoder.finish())
|
||||
.map_err(error_fn)?;
|
||||
// lzma_rust2 does not have a Write API, so we limit the
|
||||
// reader and read till EOF.
|
||||
let limited_reader = (&mut *reader).take(data_length);
|
||||
let hashing_reader = HashingReader::new(limited_reader, hasher);
|
||||
let mut decoder = XzReader::new(hashing_reader, false);
|
||||
stream::copy(&mut decoder, &mut *writer, cancel_signal)
|
||||
.map_err(error_fn)?;
|
||||
|
||||
(_, hasher) = decoder.into_inner().finish();
|
||||
}
|
||||
_ => return Err(Error::UnsupportedOperation(op.r#type())),
|
||||
}
|
||||
@@ -849,11 +840,10 @@ pub fn apply_operation(
|
||||
}
|
||||
|
||||
/// Extract the specified image from the payload. This is done multithreaded and
|
||||
/// uses rayon's global thread pool. Both the `payload` and `output` streams
|
||||
/// will be reopened from multiple threads.
|
||||
/// uses rayon's global thread pool.
|
||||
pub fn extract_image(
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
payload: &(dyn ReadAt + Sync),
|
||||
output: &(dyn WriteAt + Sync),
|
||||
header: &PayloadHeader,
|
||||
partition_name: &str,
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -865,40 +855,28 @@ pub fn extract_image(
|
||||
.find(|p| p.partition_name == partition_name)
|
||||
.ok_or_else(|| Error::MissingPartition(partition_name.to_owned()))?;
|
||||
|
||||
partition
|
||||
.operations
|
||||
.par_iter()
|
||||
.map(|op| -> Result<()> {
|
||||
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,
|
||||
writer,
|
||||
header.manifest.block_size(),
|
||||
header.blob_offset,
|
||||
op,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.collect::<Result<_>>()
|
||||
partition.operations.par_iter().try_for_each(|op| {
|
||||
apply_operation(
|
||||
&mut UserPosFile::new(payload),
|
||||
&mut UserPosFile::new(output),
|
||||
header.manifest.block_size(),
|
||||
header.blob_offset,
|
||||
op,
|
||||
cancel_signal,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Extract the specified partition images from the payload into writers. This
|
||||
/// is done multithreaded and uses rayon's global thread pool. `open_payload`
|
||||
/// and `open_output` will be called from multiple threads.
|
||||
pub fn extract_images<'a>(
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
/// is done multithreaded and uses rayon's global thread pool.
|
||||
pub fn extract_images<'name, 'file>(
|
||||
payload: &(dyn ReadAt + Sync),
|
||||
outputs: impl IntoIterator<Item = (&'name str, &'file (dyn WriteAt + Sync))>,
|
||||
header: &PayloadHeader,
|
||||
partition_names: impl IntoIterator<Item = &'a str>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let mut remaining = partition_names.into_iter().collect::<HashSet<_>>();
|
||||
let outputs = outputs.into_iter().collect::<HashMap<_, _>>();
|
||||
let mut remaining = outputs.keys().copied().collect::<HashSet<_>>();
|
||||
// We parallelize at the operation level or else one thread might get stuck
|
||||
// processing a giant image.
|
||||
let mut operations = vec![];
|
||||
@@ -918,22 +896,19 @@ pub fn extract_images<'a>(
|
||||
|
||||
operations
|
||||
.into_par_iter()
|
||||
.map(|(name, op)| -> Result<()> {
|
||||
let reader = payload.reopen_boxed().map_err(Error::PayloadReopen)?;
|
||||
let writer = open_output(name).map_err(|e| Error::OutputOpen(name.to_owned(), e))?;
|
||||
.try_for_each(|(name, op)| -> Result<()> {
|
||||
let mut reader = UserPosFile::new(payload);
|
||||
let mut writer = UserPosFile::new(&outputs[name]);
|
||||
|
||||
apply_operation(
|
||||
reader,
|
||||
writer,
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
header.manifest.block_size(),
|
||||
header.blob_offset,
|
||||
op,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compress raw data into a chunk to be used with a [`Type::ReplaceXz`]
|
||||
@@ -947,8 +922,9 @@ 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).map_err(Error::XzInit)?;
|
||||
let mut xz_writer = XzEncoder::new_stream(hashing_writer, stream);
|
||||
let mut options = XzOptions::with_preset(0);
|
||||
options.set_check_sum_type(CheckType::None);
|
||||
let mut xz_writer = XzWriter::new(hashing_writer, options).map_err(Error::XzInit)?;
|
||||
|
||||
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)
|
||||
.map_err(Error::XzCompress)?;
|
||||
@@ -964,10 +940,20 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum CowVersion {
|
||||
V2,
|
||||
V3,
|
||||
V3 {
|
||||
/// The maximum number of bytes to compress at a time.
|
||||
compression_factor: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl CowVersion {
|
||||
fn compression_factor(self) -> Option<u32> {
|
||||
match self {
|
||||
Self::V2 => None,
|
||||
Self::V3 { compression_factor } => Some(compression_factor),
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the size overhead required to store the headers and footers
|
||||
/// needed for this version of the on-disk CoW format.
|
||||
fn size_overhead(self, cow_replace_ops: u64, payload_install_ops: u64) -> u64 {
|
||||
@@ -1017,7 +1003,7 @@ impl CowVersion {
|
||||
// AOSP: CowWriterV2::GetCowSizeInfo()
|
||||
overhead += SIZEOF_COW_FOOTER_V2;
|
||||
}
|
||||
Self::V3 => {
|
||||
Self::V3 { .. } => {
|
||||
// AOSP: CowWriterV3::OpenForWrite() -> GetDataOffset()
|
||||
overhead += SIZEOF_COW_HEADER_V3;
|
||||
overhead += BUFFER_REGION_DEFAULT_SIZE;
|
||||
@@ -1234,8 +1220,6 @@ pub struct VabcParams {
|
||||
pub version: CowVersion,
|
||||
/// CoW compression algorithm.
|
||||
pub algo: VabcAlgo,
|
||||
/// The maximum number of bytes to compress at a time.
|
||||
pub compression_factor: u32,
|
||||
}
|
||||
|
||||
/// Ensure that the partition size is aligned to the block size and that the
|
||||
@@ -1244,17 +1228,16 @@ fn validate_partition_size(
|
||||
partition_name: &str,
|
||||
file_size: u64,
|
||||
block_size: u32,
|
||||
compression_factor: u32,
|
||||
compression_factor: Option<u32>,
|
||||
) -> Result<()> {
|
||||
if block_size == 0 || !block_size.is_power_of_two() || CHUNK_SIZE % u64::from(block_size) != 0 {
|
||||
return Err(Error::InvalidBlockSize(block_size));
|
||||
}
|
||||
|
||||
if compression_factor == 0
|
||||
|| !compression_factor.is_power_of_two()
|
||||
|| CHUNK_SIZE % u64::from(compression_factor) != 0
|
||||
if let Some(factor) = compression_factor
|
||||
&& (factor == 0 || !factor.is_power_of_two() || CHUNK_SIZE % u64::from(factor) != 0)
|
||||
{
|
||||
return Err(Error::InvalidMaxCompressionChunkSize(compression_factor));
|
||||
return Err(Error::InvalidMaxCompressionChunkSize(factor));
|
||||
}
|
||||
|
||||
if file_size % u64::from(block_size) != 0 {
|
||||
@@ -1275,7 +1258,7 @@ fn validate_partition_size(
|
||||
/// [`PartitionUpdate::estimate_op_count_max`] or else update_engine may fail to
|
||||
/// flash the partition due to running out of space on the CoW block device.
|
||||
pub fn compute_cow_estimate(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
payload_install_ops: u64,
|
||||
partition_name: &str,
|
||||
block_size: u32,
|
||||
@@ -1283,24 +1266,24 @@ pub fn compute_cow_estimate(
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<CowEstimate> {
|
||||
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))?;
|
||||
.file_len()
|
||||
.map_err(|e| Error::InputSize(partition_name.to_owned(), e))?;
|
||||
let final_chunk_different = file_size % CHUNK_SIZE != 0;
|
||||
|
||||
validate_partition_size(
|
||||
partition_name,
|
||||
file_size,
|
||||
block_size,
|
||||
vabc_params.compression_factor,
|
||||
vabc_params.version.compression_factor(),
|
||||
)?;
|
||||
|
||||
let chunking = ChunkingParams {
|
||||
block_size,
|
||||
method: match vabc_params.version {
|
||||
CowVersion::V2 => ChunkingMethod::Exact,
|
||||
CowVersion::V3 => {
|
||||
ChunkingMethod::MaxPowerOf2(vabc_params.compression_factor.try_into().unwrap())
|
||||
CowVersion::V3 { compression_factor } => {
|
||||
// validate_partition_size() already validated that it is not 0.
|
||||
ChunkingMethod::MaxPowerOf2(compression_factor.try_into().unwrap())
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1311,7 +1294,7 @@ pub fn compute_cow_estimate(
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<CowEstimate> {
|
||||
let data = (|| {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut reader = UserPosFile::new(input);
|
||||
reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
|
||||
|
||||
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
|
||||
@@ -1358,8 +1341,8 @@ pub fn compute_cow_estimate(
|
||||
/// This is more efficient than separately calling [`compute_cow_estimate`]
|
||||
/// since the input does not need to be read twice.
|
||||
pub fn compress_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
output: &(dyn WriteAt + Sync),
|
||||
partition_name: &str,
|
||||
block_size: u32,
|
||||
vabc_params: Option<VabcParams>,
|
||||
@@ -1368,18 +1351,21 @@ pub fn compress_image(
|
||||
const CHUNK_GROUP: u64 = 32;
|
||||
|
||||
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))?;
|
||||
.file_len()
|
||||
.map_err(|e| Error::InputSize(partition_name.to_owned(), e))?;
|
||||
let final_chunk_different = file_size % CHUNK_SIZE != 0;
|
||||
|
||||
let compression_factor = vabc_params.map_or(block_size, |p| p.compression_factor);
|
||||
validate_partition_size(partition_name, file_size, block_size, compression_factor)?;
|
||||
validate_partition_size(
|
||||
partition_name,
|
||||
file_size,
|
||||
block_size,
|
||||
vabc_params.and_then(|p| p.version.compression_factor()),
|
||||
)?;
|
||||
|
||||
let chunking = ChunkingParams {
|
||||
block_size,
|
||||
method: match vabc_params.map(|p| p.version) {
|
||||
Some(CowVersion::V3) => {
|
||||
Some(CowVersion::V3 { compression_factor }) => {
|
||||
ChunkingMethod::MaxPowerOf2(compression_factor.try_into().unwrap())
|
||||
}
|
||||
_ => ChunkingMethod::Exact,
|
||||
@@ -1402,7 +1388,7 @@ pub fn compress_image(
|
||||
let uncompressed_data_group = (chunks_done..chunks_done + chunks_group)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> io::Result<(u64, Vec<u8>)> {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
let mut reader = UserPosFile::new(input);
|
||||
let offset = reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
|
||||
|
||||
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
|
||||
@@ -1462,7 +1448,7 @@ pub fn compress_image(
|
||||
let group_operations = compressed_data_group
|
||||
.into_par_iter()
|
||||
.map(|(data, operation, _)| -> io::Result<InstallOperation> {
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
let mut writer = UserPosFile::new(output);
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
|
||||
@@ -1531,8 +1517,8 @@ fn extents_sorted(operations: &[InstallOperation]) -> bool {
|
||||
///
|
||||
/// Returns the ranges of indices of `operations` that were updated.
|
||||
pub fn compress_modified_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
input: &(dyn ReadAt + Sync),
|
||||
output: &(dyn WriteAt + Sync),
|
||||
block_size: u32,
|
||||
partition_info: &mut PartitionInfo,
|
||||
operations: &mut [InstallOperation],
|
||||
@@ -1581,7 +1567,7 @@ pub fn compress_modified_image(
|
||||
let extents_size: usize = util::try_cast(extents_size)
|
||||
.map_err(|e| Error::IntOutOfBounds("extents_size", e))?;
|
||||
|
||||
let mut reader = input.reopen_boxed().map_err(Error::ChunkRead)?;
|
||||
let mut reader = UserPosFile::new(input);
|
||||
reader
|
||||
.seek(SeekFrom::Start(extents_start))
|
||||
.map_err(Error::ChunkRead)?;
|
||||
@@ -1628,7 +1614,7 @@ pub fn compress_modified_image(
|
||||
let modified_group_operations = compressed_data_group
|
||||
.into_par_iter()
|
||||
.map(|(data, i, operation)| {
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
let mut writer = UserPosFile::new(output);
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
io::{self, Read, Seek, Write},
|
||||
mem,
|
||||
ops::Range,
|
||||
};
|
||||
@@ -520,14 +520,14 @@ impl ChunkList {
|
||||
// Find the chunk to insert before. We save the last used index to
|
||||
// optimize for sequential insertion and avoid needing to search the
|
||||
// entire list every time.
|
||||
let mut insert_before = self.chunks.front_index();
|
||||
|
||||
if let Some(last_used) = self.last_used {
|
||||
if chunk.bounds.start >= self.chunks.get(last_used).unwrap().bounds.start {
|
||||
// The new chunk starts after the last used chunk.
|
||||
insert_before = Some(last_used);
|
||||
}
|
||||
}
|
||||
let mut insert_before = if let Some(last_used) = self.last_used
|
||||
&& chunk.bounds.start >= self.chunks.get(last_used).unwrap().bounds.start
|
||||
{
|
||||
// The new chunk starts after the last used chunk.
|
||||
Some(last_used)
|
||||
} else {
|
||||
self.chunks.front_index()
|
||||
};
|
||||
|
||||
while let Some(index) = insert_before {
|
||||
if self.chunks.get(index).unwrap().bounds.start >= chunk.bounds.start {
|
||||
@@ -672,7 +672,7 @@ fn hash_fill_chunk(
|
||||
/// A type for reading sparse files.
|
||||
pub struct SparseReader<R> {
|
||||
inner: R,
|
||||
seek: Option<fn(&mut R, SeekFrom) -> io::Result<u64>>,
|
||||
seek_relative: Option<fn(&mut R, i64) -> io::Result<()>>,
|
||||
header: RawHeader,
|
||||
/// Starting block for next chunk.
|
||||
block: u32,
|
||||
@@ -689,7 +689,7 @@ impl<R: Read + Seek> SparseReader<R> {
|
||||
/// efficiently skipped without reading them.
|
||||
pub fn new_seekable(inner: R, crc_mode: CrcMode) -> Result<Self> {
|
||||
let mut result = Self::new(inner, crc_mode)?;
|
||||
result.seek = Some(Seek::seek);
|
||||
result.seek_relative = Some(Seek::seek_relative);
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -710,7 +710,7 @@ impl<R: Read> SparseReader<R> {
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
seek: None,
|
||||
seek_relative: None,
|
||||
header,
|
||||
block: 0,
|
||||
chunk: 0,
|
||||
@@ -744,12 +744,12 @@ impl<R: Read> SparseReader<R> {
|
||||
/// perform its own verification.
|
||||
pub fn next_chunk(&mut self) -> Result<Option<Chunk>> {
|
||||
if self.data_remain != 0 {
|
||||
if let Some(seek) = self.seek {
|
||||
if let Some(seek_relative) = self.seek_relative {
|
||||
if self.hasher.is_some() {
|
||||
return Err(Error::Crc32RandomRead);
|
||||
}
|
||||
|
||||
seek(&mut self.inner, SeekFrom::Current(self.data_remain.into()))
|
||||
seek_relative(&mut self.inner, self.data_remain.into())
|
||||
.map_err(|e| Error::DataRead("data_remain", e))?;
|
||||
self.data_remain = 0;
|
||||
} else {
|
||||
@@ -1026,7 +1026,7 @@ impl<W: Write> Write for SparseWriter<W> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Chunk, ChunkBounds, ChunkData, ChunkList};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn chunk_list_merge() {
|
||||
|
||||
+464
-73
@@ -1,103 +1,494 @@
|
||||
// SPDX-FileCopyrightText: 2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Seek, SeekFrom, Write};
|
||||
|
||||
use zip::{
|
||||
ZipWriter,
|
||||
result::ZipResult,
|
||||
write::{FileOptionExtension, FileOptions, StreamWriter},
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
};
|
||||
|
||||
/// A wrapper around a seekable writer. `W` must implement [`Seek`], but only
|
||||
/// during the creation of a new instance. The resulting type can be stored in a
|
||||
/// parent container where the generic type does not implement [`Seek`].
|
||||
pub struct SeekWriter<W: Write> {
|
||||
inner: W,
|
||||
seek_fn: fn(&mut W, SeekFrom) -> io::Result<u64>,
|
||||
use bstr::ByteSlice;
|
||||
use rawzip::{
|
||||
CompressionMethod, RECOMMENDED_BUFFER_SIZE, ReaderAt, ZipArchive, ZipEntries, ZipEntry,
|
||||
ZipFileHeaderRecord, ZipLocator, ZipReader, ZipSliceArchive, ZipSliceEntries, ZipSliceEntry,
|
||||
ZipSliceVerifier, ZipVerifier,
|
||||
extra_fields::{ExtraFieldId, ExtraFields},
|
||||
};
|
||||
use zerocopy::{FromZeros, IntoBytes, little_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
format::compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
|
||||
stream::ReadAt,
|
||||
};
|
||||
|
||||
pub trait ZipFileHeaderRecordExt<'a> {
|
||||
fn file_path_utf8(&self) -> Result<&'a str, rawzip::Error>;
|
||||
}
|
||||
|
||||
impl<W: Write> SeekWriter<W> {
|
||||
pub fn into_inner(self) -> W {
|
||||
self.inner
|
||||
impl<'a> ZipFileHeaderRecordExt<'a> for ZipFileHeaderRecord<'a> {
|
||||
fn file_path_utf8(&self) -> Result<&'a str, rawzip::Error> {
|
||||
str::from_utf8(self.file_path().as_bytes())
|
||||
.map_err(|e| rawzip::ErrorKind::InvalidUtf8(e).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> SeekWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
seek_fn: W::seek,
|
||||
/// Validate that the current entry's compressed data range does not overlap
|
||||
/// previously visited entries' ranges. This approach is identical to what
|
||||
/// rawzip recommends in their examples.
|
||||
fn validate_and_add_range(
|
||||
compressed_ranges: &mut Vec<(u64, u64)>,
|
||||
current_range: (u64, u64),
|
||||
path: &[u8],
|
||||
) -> Result<(), rawzip::Error> {
|
||||
let (current_start, current_end) = current_range;
|
||||
|
||||
let insert_pos = compressed_ranges
|
||||
.binary_search_by_key(¤t_start, |&(start, _)| start)
|
||||
.unwrap_or_else(|pos| pos);
|
||||
|
||||
if insert_pos > 0 {
|
||||
let (prev_start, prev_end) = compressed_ranges[insert_pos - 1];
|
||||
if prev_end > current_start {
|
||||
return Err(rawzip::ErrorKind::InvalidInput {
|
||||
msg: format!("{:?} ({current_start}..{current_end}) overlaps previous range ({prev_start}..{prev_end})", path.as_bstr()),
|
||||
}.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SeekWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.inner.write(buf)
|
||||
if insert_pos < compressed_ranges.len() {
|
||||
let (next_start, next_end) = compressed_ranges[insert_pos];
|
||||
if current_end > next_start {
|
||||
return Err(rawzip::ErrorKind::InvalidInput {
|
||||
msg: format!("{:?} ({current_start}..{current_end}) overlaps next range ({next_start}..{next_end})", path.as_bstr()),
|
||||
}.into());
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
compressed_ranges.insert(insert_pos, current_range);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<W: Write> Seek for SeekWriter<W> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
(self.seek_fn)(&mut self.inner, pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is an ugly hack to have a single type represent both seekable and
|
||||
/// streaming [`ZipWriter`]s. `W` only needs to implement [`Seek`] when creating
|
||||
/// a seekable instance via [`Self::new_seekable`].
|
||||
pub enum ZipWriterWrapper<W: Write> {
|
||||
Streaming(ZipWriter<StreamWriter<W>>),
|
||||
Seekable(ZipWriter<SeekWriter<W>>),
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> ZipWriterWrapper<W> {
|
||||
pub fn new_seekable(inner: W) -> Self {
|
||||
Self::Seekable(ZipWriter::new(SeekWriter::new(inner)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ZipWriterWrapper<W> {
|
||||
pub fn new_streaming(inner: W) -> Self {
|
||||
Self::Streaming(ZipWriter::new_stream(inner))
|
||||
/// Validate that the entry's compression ratio is not excessively large, based
|
||||
/// on a constant factor for [`CompressionMethod::Deflate`]. This approach is
|
||||
/// identical to what rawzip recommends in their examples.
|
||||
fn validate_compression_ratio(
|
||||
compressed_size: u64,
|
||||
uncompressed_size: u64,
|
||||
path: &[u8],
|
||||
) -> Result<(), rawzip::Error> {
|
||||
if compressed_size > 0 && uncompressed_size / compressed_size > 1032 {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
return Err(rawzip::ErrorKind::InvalidInput {
|
||||
msg: format!(
|
||||
"{:?} has excessively large compression ratio: {})",
|
||||
path.as_bstr(),
|
||||
uncompressed_size as f64 / compressed_size as f64,
|
||||
),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
pub fn start_file(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ZipEntriesSafe<'archive, 'buf, R> {
|
||||
archive: &'archive ZipArchive<R>,
|
||||
entries: ZipEntries<'archive, 'buf, R>,
|
||||
compressed_ranges: Vec<(u64, u64)>,
|
||||
}
|
||||
|
||||
impl<R: ReaderAt> ZipEntriesSafe<'_, '_, R> {
|
||||
#[inline]
|
||||
pub fn next_entry(
|
||||
&mut self,
|
||||
name: impl ToString,
|
||||
options: FileOptions<impl FileOptionExtension>,
|
||||
) -> ZipResult<u64> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.start_file(name, options),
|
||||
Self::Seekable(z) => z.start_file(name, options),
|
||||
}
|
||||
}
|
||||
) -> Result<Option<(ZipFileHeaderRecord<'_>, ZipEntry<'_, R>)>, rawzip::Error> {
|
||||
let cd_entry = self.entries.next_entry()?;
|
||||
let Some(cd_entry) = cd_entry else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
pub fn finish(self) -> ZipResult<W> {
|
||||
match self {
|
||||
Self::Streaming(z) => Ok(z.finish()?.into_inner()),
|
||||
Self::Seekable(z) => Ok(z.finish()?.into_inner()),
|
||||
validate_compression_ratio(
|
||||
cd_entry.compressed_size_hint(),
|
||||
cd_entry.uncompressed_size_hint(),
|
||||
cd_entry.file_path().as_ref(),
|
||||
)?;
|
||||
|
||||
let entry = self.archive.get_entry(cd_entry.wayfinder())?;
|
||||
|
||||
validate_and_add_range(
|
||||
&mut self.compressed_ranges,
|
||||
entry.compressed_data_range(),
|
||||
cd_entry.file_path().as_ref(),
|
||||
)?;
|
||||
|
||||
Ok(Some((cd_entry, entry)))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ZipEntriesSafeExt<R> {
|
||||
fn entries_safe<'archive, 'buf>(
|
||||
&'archive self,
|
||||
buffer: &'buf mut [u8],
|
||||
) -> ZipEntriesSafe<'archive, 'buf, R>;
|
||||
}
|
||||
|
||||
impl<R> ZipEntriesSafeExt<R> for ZipArchive<R> {
|
||||
fn entries_safe<'archive, 'buf>(
|
||||
&'archive self,
|
||||
buffer: &'buf mut [u8],
|
||||
) -> ZipEntriesSafe<'archive, 'buf, R> {
|
||||
let entries = self.entries(buffer);
|
||||
|
||||
ZipEntriesSafe {
|
||||
archive: self,
|
||||
entries,
|
||||
compressed_ranges: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for ZipWriterWrapper<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.write(buf),
|
||||
Self::Seekable(z) => z.write(buf),
|
||||
}
|
||||
impl<R, T: ZipEntriesSafeExt<R>> ZipEntriesSafeExt<R> for &T {
|
||||
fn entries_safe<'archive, 'buf>(
|
||||
&'archive self,
|
||||
buffer: &'buf mut [u8],
|
||||
) -> ZipEntriesSafe<'archive, 'buf, R> {
|
||||
(**self).entries_safe(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.flush(),
|
||||
Self::Seekable(z) => z.flush(),
|
||||
impl<R, T: ZipEntriesSafeExt<R>> ZipEntriesSafeExt<R> for &mut T {
|
||||
fn entries_safe<'archive, 'buf>(
|
||||
&'archive self,
|
||||
buffer: &'buf mut [u8],
|
||||
) -> ZipEntriesSafe<'archive, 'buf, R> {
|
||||
(**self).entries_safe(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ZipSliceEntriesSafe<'data, T: AsRef<[u8]>> {
|
||||
archive: &'data ZipSliceArchive<T>,
|
||||
entries: ZipSliceEntries<'data>,
|
||||
compressed_ranges: Vec<(u64, u64)>,
|
||||
}
|
||||
|
||||
impl<'data, T: AsRef<[u8]>> ZipSliceEntriesSafe<'data, T> {
|
||||
#[inline]
|
||||
pub fn next_entry(
|
||||
&mut self,
|
||||
) -> Result<Option<(ZipFileHeaderRecord<'data>, ZipSliceEntry<'data>)>, rawzip::Error> {
|
||||
let cd_entry = self.entries.next_entry()?;
|
||||
let Some(cd_entry) = cd_entry else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
validate_compression_ratio(
|
||||
cd_entry.compressed_size_hint(),
|
||||
cd_entry.uncompressed_size_hint(),
|
||||
cd_entry.file_path().as_ref(),
|
||||
)?;
|
||||
|
||||
let entry = self.archive.get_entry(cd_entry.wayfinder())?;
|
||||
|
||||
validate_and_add_range(
|
||||
&mut self.compressed_ranges,
|
||||
entry.compressed_data_range(),
|
||||
cd_entry.file_path().as_ref(),
|
||||
)?;
|
||||
|
||||
Ok(Some((cd_entry, entry)))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ZipSliceEntriesSafeExt<T: AsRef<[u8]>> {
|
||||
fn entries_safe(&self) -> ZipSliceEntriesSafe<'_, T>;
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> ZipSliceEntriesSafeExt<T> for ZipSliceArchive<T> {
|
||||
fn entries_safe(&self) -> ZipSliceEntriesSafe<'_, T> {
|
||||
let entries = self.entries();
|
||||
|
||||
ZipSliceEntriesSafe {
|
||||
archive: self,
|
||||
entries,
|
||||
compressed_ranges: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>, U: ZipSliceEntriesSafeExt<T>> ZipSliceEntriesSafeExt<T> for &U {
|
||||
fn entries_safe(&self) -> ZipSliceEntriesSafe<'_, T> {
|
||||
(**self).entries_safe()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>, U: ZipSliceEntriesSafeExt<T>> ZipSliceEntriesSafeExt<T> for &mut U {
|
||||
fn entries_safe(&self) -> ZipSliceEntriesSafe<'_, T> {
|
||||
(**self).entries_safe()
|
||||
}
|
||||
}
|
||||
|
||||
fn compression_method_to_format(
|
||||
compression_method: CompressionMethod,
|
||||
) -> Result<CompressedFormat, rawzip::Error> {
|
||||
match compression_method {
|
||||
CompressionMethod::Store => Ok(CompressedFormat::None),
|
||||
CompressionMethod::Deflate => Ok(CompressedFormat::Deflate),
|
||||
c => Err(rawzip::ErrorKind::InvalidInput {
|
||||
msg: format!("Unsupported compression method: {c:?}"),
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compressed_reader<'archive, R: ReaderAt>(
|
||||
entry: &ZipEntry<'archive, R>,
|
||||
compression_method: CompressionMethod,
|
||||
) -> Result<CompressedReader<ZipReader<&'archive R>>, rawzip::Error> {
|
||||
let format = compression_method_to_format(compression_method)?;
|
||||
|
||||
Ok(CompressedReader::with_format(entry.reader(), format))
|
||||
}
|
||||
|
||||
pub fn compressed_slice_reader<'archive>(
|
||||
entry: &ZipSliceEntry<'archive>,
|
||||
compression_method: CompressionMethod,
|
||||
) -> Result<CompressedReader<Cursor<&'archive [u8]>>, rawzip::Error> {
|
||||
let format = compression_method_to_format(compression_method)?;
|
||||
let raw_reader = Cursor::new(entry.data());
|
||||
|
||||
Ok(CompressedReader::with_format(raw_reader, format))
|
||||
}
|
||||
|
||||
pub fn verifying_reader<'archive, R: ReaderAt>(
|
||||
entry: &ZipEntry<'archive, R>,
|
||||
compression_method: CompressionMethod,
|
||||
) -> Result<ZipVerifier<CompressedReader<ZipReader<&'archive R>>, &'archive R>, rawzip::Error> {
|
||||
compressed_reader(entry, compression_method).map(|r| entry.verifying_reader(r))
|
||||
}
|
||||
|
||||
pub fn verifying_slice_reader<'archive>(
|
||||
entry: &ZipSliceEntry<'archive>,
|
||||
compression_method: CompressionMethod,
|
||||
) -> Result<ZipSliceVerifier<CompressedReader<Cursor<&'archive [u8]>>>, rawzip::Error> {
|
||||
compressed_slice_reader(entry, compression_method).map(|r| entry.verifying_reader(r))
|
||||
}
|
||||
|
||||
pub fn compressed_writer<W: Write>(
|
||||
writer: W,
|
||||
compression_method: CompressionMethod,
|
||||
) -> Result<CompressedWriter<W>, rawzip::Error> {
|
||||
use compression::Error;
|
||||
|
||||
let format = compression_method_to_format(compression_method)?;
|
||||
|
||||
match CompressedWriter::new(writer, format) {
|
||||
Ok(w) => Ok(w),
|
||||
Err(Error::Lz4Init(e) | Error::XzInit(e)) => Err(e.into()),
|
||||
Err(Error::UnknownFormat | Error::AutoDetect(_)) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ZipArchiveReadAtExt {
|
||||
fn from_read_at<R: ReadAt>(
|
||||
file: R,
|
||||
buffer: &mut [u8],
|
||||
) -> Result<ZipArchive<ReaderAtWrapper<R>>, rawzip::Error> {
|
||||
let end_offset = file.file_len()?;
|
||||
|
||||
ZipLocator::new()
|
||||
.locate_in_reader(ReaderAtWrapper(file), buffer, end_offset)
|
||||
.map_err(|(_, e)| e)
|
||||
}
|
||||
}
|
||||
|
||||
impl ZipArchiveReadAtExt for ZipArchive<()> {}
|
||||
|
||||
pub struct ReaderAtWrapper<R: ReadAt>(R);
|
||||
|
||||
impl<R: ReadAt> ReaderAt for ReaderAtWrapper<R> {
|
||||
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
ReadAt::read_at(&self.0, buf, offset)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
|
||||
#[repr(C, packed)]
|
||||
struct ZipLocalHeader {
|
||||
signature: little_endian::U32,
|
||||
version_needed: little_endian::U16,
|
||||
flags: little_endian::U16,
|
||||
compression_method: little_endian::U16,
|
||||
last_mod_time: little_endian::U16,
|
||||
last_mod_date: little_endian::U16,
|
||||
crc32: little_endian::U32,
|
||||
compressed_size: little_endian::U32,
|
||||
uncompressed_size: little_endian::U32,
|
||||
file_name_len: little_endian::U16,
|
||||
extra_field_len: little_endian::U16,
|
||||
}
|
||||
|
||||
/// Convert a streaming zip into a non-streaming one. If any entry uses ZIP64,
|
||||
/// the local header must contain an [`ExtraFieldId::ANDROID_ZIP_ALIGNMENT`]
|
||||
/// extra field with sufficient size (16 bytes to be safe). This is used as
|
||||
/// reserved space for creating a new [`ExtraFieldId::ZIP64`] extra field. Any
|
||||
/// leftover space must be at least 4 bytes so that a new extra field can
|
||||
/// consume the space. The existing data descriptor will remain in the gap
|
||||
/// between entries and data will not be shifted.
|
||||
pub fn make_non_streaming(file: impl Read + Write + Seek) -> Result<(), rawzip::Error> {
|
||||
// rawzip currently does not expose the CRC32 value, so we'll have to read
|
||||
// it ourselves.
|
||||
struct EntryInfo {
|
||||
local_header_offset: u64,
|
||||
central_header_offset: u64,
|
||||
crc32: u32,
|
||||
compressed_size: u64,
|
||||
uncompressed_size: u64,
|
||||
local_extra_fields: Vec<u8>,
|
||||
}
|
||||
|
||||
let mut to_update = vec![];
|
||||
|
||||
let mut central_buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
|
||||
let mut local_buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
|
||||
let archive = ZipArchive::from_seekable(file, &mut central_buffer)?;
|
||||
let mut entries = archive.entries_safe(&mut central_buffer);
|
||||
|
||||
while let Some((cd_entry, entry)) = entries.next_entry()? {
|
||||
let wf = cd_entry.wayfinder();
|
||||
|
||||
let local_header = entry.local_header(&mut local_buffer)?;
|
||||
|
||||
to_update.push(EntryInfo {
|
||||
local_header_offset: cd_entry.local_header_offset(),
|
||||
central_header_offset: cd_entry.central_directory_offset(),
|
||||
crc32: cd_entry.crc32(),
|
||||
compressed_size: wf.compressed_size_hint(),
|
||||
uncompressed_size: wf.uncompressed_size_hint(),
|
||||
local_extra_fields: local_header.extra_fields().remaining_bytes().to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut file = archive.into_inner().into_inner();
|
||||
|
||||
for entry in to_update {
|
||||
// Clear the central header's streaming flag.
|
||||
let mut central_flags = little_endian::U16::new(0);
|
||||
file.seek(SeekFrom::Start(entry.central_header_offset + 8))?;
|
||||
file.read_exact(central_flags.as_mut_bytes())?;
|
||||
central_flags &= !0x8;
|
||||
file.seek_relative(-(central_flags.as_bytes().len() as i64))?;
|
||||
file.write_all(central_flags.as_bytes())?;
|
||||
|
||||
file.seek(SeekFrom::Start(entry.local_header_offset))?;
|
||||
|
||||
let mut local_header = ZipLocalHeader::new_zeroed();
|
||||
file.read_exact(local_header.as_mut_bytes())?;
|
||||
|
||||
// Clear the local header's streaming flag.
|
||||
local_header.flags &= !0x8;
|
||||
|
||||
// Remove dependency on the data descriptor.
|
||||
local_header.crc32.set(entry.crc32);
|
||||
|
||||
let compressed_is_zip64 = entry.compressed_size >= 0xffffffff;
|
||||
let uncompressed_is_zip64 = entry.uncompressed_size >= 0xffffffff;
|
||||
|
||||
if compressed_is_zip64 {
|
||||
local_header.compressed_size.set(0xffffffff);
|
||||
} else {
|
||||
local_header
|
||||
.compressed_size
|
||||
.set(entry.compressed_size as u32);
|
||||
}
|
||||
|
||||
if uncompressed_is_zip64 {
|
||||
local_header.uncompressed_size.set(0xffffffff);
|
||||
} else {
|
||||
local_header
|
||||
.uncompressed_size
|
||||
.set(entry.uncompressed_size as u32);
|
||||
}
|
||||
|
||||
file.seek_relative(-(local_header.as_bytes().len() as i64))?;
|
||||
file.write_all(local_header.as_bytes())?;
|
||||
|
||||
file.seek_relative(i64::from(local_header.file_name_len.get()))?;
|
||||
|
||||
if !compressed_is_zip64 && !uncompressed_is_zip64 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut extra_fields = Vec::with_capacity(entry.local_extra_fields.len());
|
||||
let mut patched_placeholder = false;
|
||||
|
||||
for (id, data) in ExtraFields::new(&entry.local_extra_fields) {
|
||||
if id == ExtraFieldId::ANDROID_ZIP_ALIGNMENT {
|
||||
let zip64_len =
|
||||
8 * (usize::from(compressed_is_zip64) + usize::from(uncompressed_is_zip64));
|
||||
|
||||
// Any unused space needs to be at least 4 bytes, so we can
|
||||
// properly write a new extra field for padding.
|
||||
let have_needed_space = match data.len().cmp(&zip64_len) {
|
||||
Ordering::Less => false,
|
||||
Ordering::Equal => true,
|
||||
Ordering::Greater => data.len() - zip64_len >= 4,
|
||||
};
|
||||
if !have_needed_space {
|
||||
return Err(rawzip::ErrorKind::InvalidInput {
|
||||
msg: format!(
|
||||
"Invalid reserved ZIP64 local extra field size: {}",
|
||||
data.len()
|
||||
),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
// The order is indeed backwards compared to the header
|
||||
// fields (APPNOTE 4.5.3).
|
||||
extra_fields.extend_from_slice(&ExtraFieldId::ZIP64.as_u16().to_le_bytes());
|
||||
extra_fields.extend_from_slice(&(zip64_len as u16).to_le_bytes());
|
||||
if uncompressed_is_zip64 {
|
||||
extra_fields.extend_from_slice(&entry.uncompressed_size.to_le_bytes());
|
||||
}
|
||||
if compressed_is_zip64 {
|
||||
extra_fields.extend_from_slice(&entry.compressed_size.to_le_bytes());
|
||||
}
|
||||
|
||||
// Keep using ANDROID_ZIP_ALIGNMENT for padding.
|
||||
if data.len() > zip64_len {
|
||||
let padding_len = data.len() - zip64_len - 4;
|
||||
extra_fields.extend_from_slice(&id.as_u16().to_le_bytes());
|
||||
extra_fields.extend_from_slice(&(padding_len as u16).to_le_bytes());
|
||||
extra_fields.resize(extra_fields.len() + padding_len, 0);
|
||||
}
|
||||
|
||||
patched_placeholder = true;
|
||||
} else if id == ExtraFieldId::ZIP64 {
|
||||
return Err(rawzip::ErrorKind::InvalidInput {
|
||||
msg: "Unexpected ZIP64 extra field present".to_owned(),
|
||||
}
|
||||
.into());
|
||||
} else {
|
||||
extra_fields.extend_from_slice(&id.as_u16().to_le_bytes());
|
||||
extra_fields.extend_from_slice(&(data.len() as u16).to_le_bytes());
|
||||
extra_fields.extend_from_slice(data);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(extra_fields.len(), entry.local_extra_fields.len());
|
||||
|
||||
if !patched_placeholder {
|
||||
return Err(rawzip::ErrorKind::InvalidInput {
|
||||
msg: "ZIP64 required, but no placeholder extra field found".to_owned(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
|
||||
file.write_all(&extra_fields)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+214
-140
@@ -1,7 +1,8 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Write,
|
||||
@@ -15,10 +16,8 @@ use std::{
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
write::XzEncoder,
|
||||
};
|
||||
use lzma_rust2::{CheckType, XzOptions, XzWriter};
|
||||
use rawzip::{RECOMMENDED_BUFFER_SIZE, ZipArchive};
|
||||
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||
use regex::bytes::Regex;
|
||||
use ring::digest::Context;
|
||||
@@ -26,7 +25,6 @@ use rsa::RsaPublicKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{Span, debug, debug_span, trace, warn};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{ZipArchive, result::ZipError};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, RsaSigningKey},
|
||||
@@ -35,9 +33,11 @@ use crate::{
|
||||
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
|
||||
compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntry, CpioEntryData},
|
||||
zip::{self, ZipEntriesSafeExt, ZipFileHeaderRecordExt, ZipSliceEntriesSafeExt},
|
||||
},
|
||||
patch::otacert::{self, OtaCertBuildFlags},
|
||||
stream::{self, FromReader, HashingWriter, ReadSeek, SectionReader, ToWriter, WriteSeek},
|
||||
util,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -83,17 +83,19 @@ pub enum Error {
|
||||
#[error("Failed to generate replacement otacerts zip")]
|
||||
OtaCertZip(#[source] otacert::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] liblzma::stream::Error),
|
||||
XzInit(#[source] io::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),
|
||||
ZipOpen(PathBuf, #[source] rawzip::Error),
|
||||
#[error("Failed to list zip entries")]
|
||||
ZipEntryList(#[source] rawzip::Error),
|
||||
#[error("Missing zip entry: {0:?}")]
|
||||
ZipEntryMissing(Cow<'static, str>),
|
||||
#[error("Failed to open zip entry: {0:?}")]
|
||||
ZipEntryOpen(&'static str, #[source] ZipError),
|
||||
ZipEntryOpen(Cow<'static, str>, #[source] rawzip::Error),
|
||||
#[error("Failed to read zip entry: {0:?}")]
|
||||
ZipEntryRead(&'static str, #[source] io::Error),
|
||||
#[error("Failed to open zip entry #{0}")]
|
||||
ZipIndexOpen(usize, #[source] ZipError),
|
||||
ZipEntryRead(Cow<'static, str>, #[source] io::Error),
|
||||
#[error("Failed to open file: {0:?}")]
|
||||
FileOpen(PathBuf, #[source] io::Error),
|
||||
}
|
||||
@@ -194,11 +196,12 @@ impl MagiskRootPatcher {
|
||||
// replaced by PREINITDEVICE
|
||||
// - 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..30300];
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..30700];
|
||||
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: RangeFrom<u32> = 26403..;
|
||||
const VER_VENDOR_BOOT: RangeFrom<u32> = 30300..;
|
||||
|
||||
const ZIP_INIT_LD: &'static str = "lib/arm64-v8a/libinit-ld.so";
|
||||
const ZIP_LIBMAGISK: &'static str = "lib/arm64-v8a/libmagisk.so";
|
||||
@@ -256,41 +259,54 @@ impl MagiskRootPatcher {
|
||||
})
|
||||
}
|
||||
|
||||
fn get_version(path: &Path) -> Result<u32> {
|
||||
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::ZipEntryOpen(Self::ZIP_UTIL_FUNCTIONS, e))?;
|
||||
let mut entry = BufReader::new(entry);
|
||||
let mut line = String::new();
|
||||
fn get_version(apk_path: &Path) -> Result<u32> {
|
||||
let file = File::open(apk_path).map_err(|e| Error::FileOpen(apk_path.to_owned(), e))?;
|
||||
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
|
||||
let archive = ZipArchive::from_file(file, &mut buffer)
|
||||
.map_err(|e| Error::ZipOpen(apk_path.to_owned(), e))?;
|
||||
let mut entries = archive.entries_safe(&mut buffer);
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
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()));
|
||||
while let Some((cd_entry, entry)) = entries.next_entry().map_err(Error::ZipEntryList)? {
|
||||
let path = cd_entry.file_path_utf8().map_err(Error::ZipEntryList)?;
|
||||
|
||||
if path != Self::ZIP_UTIL_FUNCTIONS {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(suffix) = line.trim_end().strip_prefix("MAGISK_VER_CODE=") {
|
||||
trace!("Magisk version code line: {line:?}");
|
||||
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
|
||||
.map(BufReader::new)
|
||||
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_UTIL_FUNCTIONS.into(), e))?;
|
||||
let mut line = String::new();
|
||||
|
||||
let version = suffix
|
||||
.parse()
|
||||
.map_err(|e| Error::ParseMagiskVersion(suffix.to_owned(), e))?;
|
||||
return Ok(version);
|
||||
loop {
|
||||
line.clear();
|
||||
let n = reader
|
||||
.read_line(&mut line)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_UTIL_FUNCTIONS.into(), e))?;
|
||||
if n == 0 {
|
||||
return Err(Error::FindMagiskVersion(apk_path.to_owned()));
|
||||
}
|
||||
|
||||
if let Some(suffix) = line.trim_end().strip_prefix("MAGISK_VER_CODE=") {
|
||||
trace!("Magisk version code line: {line:?}");
|
||||
|
||||
let version = suffix
|
||||
.parse()
|
||||
.map_err(|e| Error::ParseMagiskVersion(suffix.to_owned(), e))?;
|
||||
return Ok(version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::ZipEntryMissing(Self::ZIP_UTIL_FUNCTIONS.into()))
|
||||
}
|
||||
|
||||
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 mut options = XzOptions::with_preset(9);
|
||||
options.set_check_sum_type(CheckType::None);
|
||||
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = XzEncoder::new_stream(raw_writer, stream);
|
||||
let mut writer = XzWriter::new(raw_writer, options).map_err(Error::XzInit)?;
|
||||
|
||||
let raw_writer = stream::copy(reader, &mut writer, cancel_signal)
|
||||
.and_then(|_| writer.finish())
|
||||
@@ -376,17 +392,15 @@ impl MagiskRootPatcher {
|
||||
let mut new_path = b".backup/".to_vec();
|
||||
new_path.extend(&old_entry.path);
|
||||
|
||||
let mut new_data = None;
|
||||
let new_data = if xz_compress && let CpioEntryData::Data(data) = &old_entry.data {
|
||||
new_path.extend(b".xz");
|
||||
|
||||
if xz_compress {
|
||||
if let CpioEntryData::Data(data) = &old_entry.data {
|
||||
new_path.extend(b".xz");
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
let buf = Self::xz_compress(&new_path, reader, cancel_signal)?;
|
||||
new_data = Some(CpioEntryData::Data(buf));
|
||||
}
|
||||
}
|
||||
let reader = Cursor::new(data);
|
||||
let buf = Self::xz_compress(&new_path, reader, cancel_signal)?;
|
||||
Some(CpioEntryData::Data(buf))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
new_entries.push(CpioEntry {
|
||||
path: new_path,
|
||||
@@ -438,12 +452,6 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
}
|
||||
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
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.
|
||||
let ramdisk = match boot_image {
|
||||
@@ -469,60 +477,95 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
// Delete the original init.
|
||||
entries.retain(|e| e.path != b"init");
|
||||
|
||||
// Add magiskinit.
|
||||
{
|
||||
let mut zip_entry = zip
|
||||
.by_name(Self::ZIP_MAGISKINIT)
|
||||
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_MAGISKINIT, e))?;
|
||||
let mut data = vec![];
|
||||
zip_entry
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_MAGISKINIT, e))?;
|
||||
let file =
|
||||
File::open(&self.apk_path).map_err(|e| Error::FileOpen(self.apk_path.clone(), e))?;
|
||||
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
|
||||
let archive = ZipArchive::from_file(file, &mut buffer)
|
||||
.map_err(|e| Error::ZipOpen(self.apk_path.clone(), e))?;
|
||||
|
||||
let mut zip_entries = archive.entries_safe(&mut buffer);
|
||||
let mut found_magiskinit = false;
|
||||
let mut found_libmagisk = false;
|
||||
|
||||
while let Some((cd_entry, entry)) = zip_entries.next_entry().map_err(Error::ZipEntryList)? {
|
||||
let path = cd_entry.file_path_utf8().map_err(Error::ZipEntryList)?;
|
||||
|
||||
// magiskinit is the only entry that is not xz-compressed.
|
||||
if path == Self::ZIP_MAGISKINIT {
|
||||
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
|
||||
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_MAGISKINIT.into(), e))?;
|
||||
let mut data = vec![];
|
||||
|
||||
reader
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| Error::ZipEntryRead(Self::ZIP_MAGISKINIT.into(), e))?;
|
||||
|
||||
entries.push(CpioEntry::new_file(
|
||||
b"init",
|
||||
0o750,
|
||||
CpioEntryData::Data(data),
|
||||
));
|
||||
|
||||
found_magiskinit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Keep a 'static version of the zip path.
|
||||
let (path, cpio_path): (_, &[u8]) = match path {
|
||||
// Newer Magisk versions only include a single binary for the
|
||||
// target ABI in the ramdisk. This was introduced in commit
|
||||
// fb5ee86615ed3df830e8538f8b39b1b133caea34.
|
||||
p if p == Self::ZIP_LIBMAGISK => {
|
||||
debug!("Single libmagisk");
|
||||
found_libmagisk = true;
|
||||
(Self::ZIP_LIBMAGISK, b"overlay.d/sbin/magisk.xz")
|
||||
}
|
||||
// Older Magisk versions include the 64-bit binary and,
|
||||
// optionally, the 32-bit binary if the device supports it. We
|
||||
// unconditionally include the magisk32 because the boot image
|
||||
// itself doesn't have sufficient information to determine if a
|
||||
// device is 64-bit only.
|
||||
p if p == Self::ZIP_LIBMAGISK32 => {
|
||||
debug!("Split libmagisk32");
|
||||
found_libmagisk = true;
|
||||
(Self::ZIP_LIBMAGISK32, b"overlay.d/sbin/magisk32.xz")
|
||||
}
|
||||
p if p == Self::ZIP_LIBMAGISK64 => {
|
||||
debug!("Split libmagisk64");
|
||||
found_libmagisk = true;
|
||||
(Self::ZIP_LIBMAGISK64, b"overlay.d/sbin/magisk64.xz")
|
||||
}
|
||||
// The stub apk was introduced in commit
|
||||
// ad0e6511e11ebec65aa9b5b916e1397342850319.
|
||||
p if p == Self::ZIP_STUB => {
|
||||
debug!("Magisk stub found");
|
||||
(Self::ZIP_STUB, b"overlay.d/sbin/stub.xz")
|
||||
}
|
||||
// init-ld was introduced in commit
|
||||
// 33aebb59763b6ec27209563035303700e998633d.
|
||||
p if p == Self::ZIP_INIT_LD => {
|
||||
debug!("Magisk init-ld found");
|
||||
(Self::ZIP_INIT_LD, b"overlay.d/sbin/init-ld.xz")
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let reader = zip::verifying_reader(&entry, cd_entry.compression_method())
|
||||
.map_err(|e| Error::ZipEntryOpen(path.into(), e))?;
|
||||
|
||||
let buf = Self::xz_compress(path.as_bytes(), reader, cancel_signal)?;
|
||||
|
||||
entries.push(CpioEntry::new_file(
|
||||
b"init",
|
||||
0o750,
|
||||
CpioEntryData::Data(data),
|
||||
cpio_path,
|
||||
0o644,
|
||||
CpioEntryData::Data(buf),
|
||||
));
|
||||
}
|
||||
|
||||
let mut xz_files = HashMap::<&str, &[u8]>::new();
|
||||
if zip.file_names().any(|n| n == Self::ZIP_LIBMAGISK) {
|
||||
// Newer Magisk versions only include a single binary for the target
|
||||
// ABI in the ramdisk. fb5ee86615ed3df830e8538f8b39b1b133caea34.
|
||||
debug!("Single libmagisk");
|
||||
xz_files.insert(Self::ZIP_LIBMAGISK, b"overlay.d/sbin/magisk.xz");
|
||||
} else {
|
||||
// Older Magisk versions include the 64-bit binary and, optionally,
|
||||
// the 32-bit binary if the device supports it. We unconditionally
|
||||
// include the magisk32 because the boot image itself doesn't have
|
||||
// sufficient information to determine if a device is 64-bit only.
|
||||
debug!("Split libmagisk32/libmagisk64");
|
||||
xz_files.insert(Self::ZIP_LIBMAGISK32, b"overlay.d/sbin/magisk32.xz");
|
||||
xz_files.insert(Self::ZIP_LIBMAGISK64, b"overlay.d/sbin/magisk64.xz");
|
||||
}
|
||||
|
||||
// Add stub apk, which only exists after Magisk commit
|
||||
// ad0e6511e11ebec65aa9b5b916e1397342850319.
|
||||
if zip.file_names().any(|n| n == Self::ZIP_STUB) {
|
||||
debug!("Magisk stub found");
|
||||
xz_files.insert(Self::ZIP_STUB, b"overlay.d/sbin/stub.xz");
|
||||
}
|
||||
|
||||
// Add init-ld, which only exists after Magisk commit
|
||||
// 33aebb59763b6ec27209563035303700e998633d
|
||||
if zip.file_names().any(|n| n == Self::ZIP_INIT_LD) {
|
||||
debug!("Magisk init-ld found");
|
||||
xz_files.insert(Self::ZIP_INIT_LD, b"overlay.d/sbin/init-ld.xz");
|
||||
}
|
||||
|
||||
for (source, target) in xz_files {
|
||||
let reader = zip
|
||||
.by_name(source)
|
||||
.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)));
|
||||
if !found_magiskinit {
|
||||
return Err(Error::ZipEntryMissing(Self::ZIP_MAGISKINIT.into()));
|
||||
} else if !found_libmagisk {
|
||||
return Err(Error::ZipEntryMissing(Self::ZIP_LIBMAGISK.into()));
|
||||
}
|
||||
|
||||
// Create Magisk .backup directory structure.
|
||||
@@ -544,10 +587,17 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
|
||||
magisk_config.push_str("RECOVERYMODE=false\n");
|
||||
|
||||
if Self::VER_PREINIT_DEVICE.contains(&self.version) {
|
||||
if let Some(device) = &self.preinit_device {
|
||||
writeln!(&mut magisk_config, "PREINITDEVICE={device}").unwrap();
|
||||
}
|
||||
// We never install Magisk into the init_boot.cpio within the
|
||||
// vendor_boot ramdisk. This config option was introduced in commit
|
||||
// 742913ebcb10cf819a54699497359535047874f7.
|
||||
if Self::VER_VENDOR_BOOT.contains(&self.version) {
|
||||
magisk_config.push_str("VENDORBOOT=false\n");
|
||||
}
|
||||
|
||||
if Self::VER_PREINIT_DEVICE.contains(&self.version)
|
||||
&& 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
|
||||
@@ -604,7 +654,7 @@ pub struct OtaCertPatcher {
|
||||
}
|
||||
|
||||
impl OtaCertPatcher {
|
||||
const OTACERTS_PATH: &'static [u8] = b"system/etc/security/otacerts.zip";
|
||||
const OTACERTS_PATH: &'static str = "system/etc/security/otacerts.zip";
|
||||
|
||||
pub fn new(cert: Certificate) -> Self {
|
||||
Self { cert }
|
||||
@@ -630,29 +680,33 @@ impl OtaCertPatcher {
|
||||
}
|
||||
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let Some(entry) = entries.iter().find(|e| e.path == Self::OTACERTS_PATH) else {
|
||||
let Some(entry) = entries
|
||||
.iter()
|
||||
.find(|e| e.path == Self::OTACERTS_PATH.as_bytes())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let CpioEntryData::Data(data) = &entry.data else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(|e| {
|
||||
Error::ZipOpen(str::from_utf8(Self::OTACERTS_PATH).unwrap().into(), e)
|
||||
})?;
|
||||
let archive = ZipArchive::from_slice(data)
|
||||
.map_err(|e| Error::ZipOpen(Self::OTACERTS_PATH.into(), e))?;
|
||||
let mut entries = archive.entries_safe();
|
||||
|
||||
for index in 0..zip.len() {
|
||||
let zip_entry = zip
|
||||
.by_index(index)
|
||||
.map_err(|e| Error::ZipIndexOpen(index, e))?;
|
||||
if !zip_entry.name().ends_with(".x509.pem") {
|
||||
debug!("Skipping invalid entry path: {}", zip_entry.name());
|
||||
while let Some((cd_entry, entry)) = entries.next_entry().map_err(Error::ZipEntryList)? {
|
||||
let path = cd_entry.file_path_utf8().map_err(Error::ZipEntryList)?;
|
||||
|
||||
if !path.ends_with(".x509.pem") {
|
||||
debug!("Skipping invalid entry path: {path:?}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = PathBuf::from(zip_entry.name());
|
||||
let reader = zip::verifying_slice_reader(&entry, cd_entry.compression_method())
|
||||
.map_err(|e| Error::ZipEntryOpen(path.to_owned().into(), e))?;
|
||||
|
||||
let certificate =
|
||||
crypto::read_pem_cert(&path, zip_entry).map_err(Error::OtaCertLoad)?;
|
||||
crypto::read_pem_cert(Path::new(path), reader).map_err(Error::OtaCertLoad)?;
|
||||
certificates.push(certificate);
|
||||
}
|
||||
}
|
||||
@@ -666,7 +720,10 @@ impl OtaCertPatcher {
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<bool> {
|
||||
let (mut entries, ramdisk_format) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let Some(entry) = entries.iter_mut().find(|e| e.path == Self::OTACERTS_PATH) else {
|
||||
let Some(entry) = entries
|
||||
.iter_mut()
|
||||
.find(|e| e.path == Self::OTACERTS_PATH.as_bytes())
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
@@ -707,7 +764,10 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
|
||||
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) {
|
||||
if entries
|
||||
.iter()
|
||||
.any(|e| e.path == Self::OTACERTS_PATH.as_bytes())
|
||||
{
|
||||
targets.push(name);
|
||||
continue 'outer;
|
||||
}
|
||||
@@ -742,7 +802,7 @@ impl BootImagePatch for OtaCertPatcher {
|
||||
// out of future updates if the OTA certificate mechanism has changed.
|
||||
Err(Error::Validation(format!(
|
||||
"No ramdisk contains {:?}",
|
||||
Self::OTACERTS_PATH.as_bstr(),
|
||||
Self::OTACERTS_PATH,
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -930,16 +990,17 @@ impl PrepatchedImagePatcher {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let kmi_version = captures
|
||||
.iter()
|
||||
// Capture #0 is the entire match.
|
||||
.skip(1)
|
||||
.flatten()
|
||||
.map(|c| c.as_bytes())
|
||||
// Our regex only matches ASCII bytes.
|
||||
.map(|c| std::str::from_utf8(c).unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
.join("-");
|
||||
let kmi_version = util::join(
|
||||
captures
|
||||
.iter()
|
||||
// Capture #0 is the entire match.
|
||||
.skip(1)
|
||||
.flatten()
|
||||
.map(|c| c.as_bytes())
|
||||
// Our regex only matches ASCII bytes.
|
||||
.map(|c| std::str::from_utf8(c).unwrap()),
|
||||
"-",
|
||||
);
|
||||
|
||||
Ok(Some(kmi_version))
|
||||
}
|
||||
@@ -1211,9 +1272,20 @@ fn save_boot_image(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait BootImageOpener {
|
||||
fn open_original(&self, name: &str) -> io::Result<Box<dyn ReadSeek + Sync>>;
|
||||
|
||||
fn open_replacement(&self, name: &str) -> io::Result<Box<dyn WriteSeek + Sync>> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("{name} boot image not found"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_boot_images<'a>(
|
||||
names: &[&'a str],
|
||||
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
opener: &(dyn BootImageOpener + Sync),
|
||||
) -> TargetsResult<HashMap<&'a str, BootImageInfo>> {
|
||||
let parent_span = Span::current();
|
||||
|
||||
@@ -1221,8 +1293,9 @@ pub fn load_boot_images<'a>(
|
||||
.par_iter()
|
||||
.map(|&name| {
|
||||
let _span = debug_span!(parent: &parent_span, "image", name).entered();
|
||||
let mut reader =
|
||||
open_input(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
let mut reader = opener
|
||||
.open_original(name)
|
||||
.map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
|
||||
let info =
|
||||
load_boot_image(&mut reader).map_err(|e| TargetsError::Load(name.to_owned(), e))?;
|
||||
@@ -1239,8 +1312,7 @@ pub fn load_boot_images<'a>(
|
||||
/// be opened from multiple threads, but at most once each.
|
||||
pub fn patch_boot_images<'a>(
|
||||
names: &[&'a str],
|
||||
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
opener: &(dyn BootImageOpener + Sync),
|
||||
key: &RsaSigningKey,
|
||||
patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -1253,7 +1325,7 @@ pub fn patch_boot_images<'a>(
|
||||
}
|
||||
|
||||
// Preparse all images. Some patchers need to inspect every candidate.
|
||||
let mut images = load_boot_images(names, open_input)?;
|
||||
let mut images = load_boot_images(names, opener)?;
|
||||
|
||||
// Find the targets that each patcher wants to patch.
|
||||
let all_targets = patchers
|
||||
@@ -1304,7 +1376,9 @@ pub fn patch_boot_images<'a>(
|
||||
// Resign and write new images.
|
||||
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 mut writer = opener
|
||||
.open_replacement(name)
|
||||
.map_err(|e| TargetsError::Open(name.to_owned(), e))?;
|
||||
|
||||
save_boot_image(&mut writer, info, key).map_err(|e| TargetsError::Save(name.to_owned(), e))
|
||||
})?;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use rawzip::{CompressionMethod, ZipArchiveWriter};
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
use x509_cert::{Certificate, der::asn1::BitString};
|
||||
use zip::{CompressionMethod, DateTime, ZipWriter, result::ZipError, write::SimpleFileOptions};
|
||||
|
||||
use crate::{crypto, format::ota};
|
||||
use crate::{
|
||||
crypto,
|
||||
format::{ota, zip},
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
@@ -18,7 +21,7 @@ pub enum Error {
|
||||
#[error("New otacerts.zip is too large to fit in {0} bytes")]
|
||||
ZipTooLarge(usize),
|
||||
#[error("Failed to write otacerts zip")]
|
||||
ZipWrite(#[source] ZipError),
|
||||
ZipWrite(#[source] rawzip::Error),
|
||||
#[error("Failed to write certificate to otacerts zip")]
|
||||
CertWrite(#[source] crypto::Error),
|
||||
}
|
||||
@@ -71,19 +74,23 @@ bitflags! {
|
||||
/// Create an `otacerts.zip` file containing the specified certificate.
|
||||
pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = ZipWriter::new(raw_writer);
|
||||
let mut writer = ZipArchiveWriter::new(raw_writer);
|
||||
|
||||
let compression_method = if flags.contains(OtaCertBuildFlags::COMPRESS_DEFLATE) {
|
||||
CompressionMethod::Deflated
|
||||
CompressionMethod::Deflate
|
||||
} else {
|
||||
CompressionMethod::Stored
|
||||
CompressionMethod::Store
|
||||
};
|
||||
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(compression_method);
|
||||
let name = "ota.x509.pem";
|
||||
writer.start_file(name, options).map_err(Error::ZipWrite)?;
|
||||
let (entry_writer, data_config) = writer
|
||||
.new_file(name)
|
||||
.compression_method(compression_method)
|
||||
.start()
|
||||
.map_err(Error::ZipWrite)?;
|
||||
let compressed_writer =
|
||||
zip::compressed_writer(entry_writer, compression_method).map_err(Error::ZipWrite)?;
|
||||
let mut data_writer = data_config.wrap(compressed_writer);
|
||||
|
||||
let cert = if flags.is_empty() {
|
||||
Cow::Borrowed(cert)
|
||||
@@ -95,10 +102,10 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
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 {
|
||||
extensions.clear();
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS)
|
||||
&& let Some(extensions) = &mut modified.tbs_certificate.extensions
|
||||
{
|
||||
extensions.clear();
|
||||
}
|
||||
if flags.contains(OtaCertBuildFlags::REMOVE_ISSUER) {
|
||||
modified.tbs_certificate.issuer.0.clear();
|
||||
@@ -112,9 +119,16 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
Cow::Owned(modified)
|
||||
};
|
||||
|
||||
crypto::write_pem_cert(Path::new(name), &mut writer, &cert).map_err(Error::CertWrite)?;
|
||||
crypto::write_pem_cert(Path::new(name), &mut data_writer, &cert).map_err(Error::CertWrite)?;
|
||||
|
||||
let raw_writer = writer.finish().map_err(Error::ZipWrite)?;
|
||||
data_writer
|
||||
.finish()
|
||||
.and_then(|(w, d)| w.finish()?.finish(d))
|
||||
.map_err(Error::ZipWrite)?;
|
||||
|
||||
let mut raw_writer = writer.finish().map_err(Error::ZipWrite)?;
|
||||
|
||||
zip::make_non_streaming(&mut raw_writer).map_err(Error::ZipWrite)?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
+32
-34
@@ -1,27 +1,28 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, SeekFrom},
|
||||
io::{self, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use memchr::memmem;
|
||||
use rawzip::ZipArchive;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use thiserror::Error;
|
||||
use tracing::{Span, debug, debug_span, trace};
|
||||
use x509_cert::Certificate;
|
||||
use zip::ZipArchive;
|
||||
|
||||
use crate::{
|
||||
crypto::RsaSigningKey,
|
||||
format::{
|
||||
avb::{self, AppendedDescriptorMut, Footer},
|
||||
ota,
|
||||
zip::{ZipFileHeaderRecordExt, ZipSliceEntriesSafeExt},
|
||||
},
|
||||
patch::otacert,
|
||||
stream::{self, ReadFixedSizeExt, ReadSeekReopen, SectionReader, WriteSeekReopen},
|
||||
stream::{self, ReadFixedSizeExt, ReadWriteAt, UserPosFile},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -68,25 +69,28 @@ fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
|
||||
|
||||
trace!("Found zip bounds: {:?}", start..end);
|
||||
|
||||
let reader = SectionReader::new(Cursor::new(data), start as u64, (end - start) as u64).ok()?;
|
||||
let mut zip_reader = ZipArchive::new(reader).ok()?;
|
||||
let archive = ZipArchive::from_slice(&data[start..end]).ok()?;
|
||||
let mut entries = archive.entries_safe();
|
||||
let mut matches = 0;
|
||||
|
||||
if zip_reader.is_empty() {
|
||||
while let Some((cd_entry, _)) = entries.next_entry().ok()? {
|
||||
let path = cd_entry.file_path_utf8().ok()?;
|
||||
|
||||
if !path.ends_with(".x509.pem") {
|
||||
// otacerts.zip files only contain files named this way.
|
||||
trace!("Excluded due to invalid name: {path:?}");
|
||||
return None;
|
||||
}
|
||||
|
||||
matches += 1;
|
||||
}
|
||||
|
||||
if matches == 0 {
|
||||
// otacerts.zip files contain at least one cert.
|
||||
trace!("Zip is empty");
|
||||
return None;
|
||||
}
|
||||
|
||||
for index in 0..zip_reader.len() {
|
||||
let entry = zip_reader.by_index_raw(index).ok()?;
|
||||
|
||||
if !entry.name().ends_with(".x509.pem") {
|
||||
// otacerts.zip files only contain files named this way.
|
||||
trace!("Excluded due to invalid name: {:?}", entry.name());
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Found otacerts.zip candidate");
|
||||
|
||||
// There's one or more entries and every one is named *.x509.pem.
|
||||
@@ -96,8 +100,7 @@ fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
|
||||
/// Replace `otacerts.zip` with a new one containing the new certificate, but
|
||||
/// padded to the same size. If the new zip is too large, the certificate will
|
||||
/// be modified to remove unnecessary components until it fits. All operations
|
||||
/// run in parallel where possible. The input and output must refer to the same
|
||||
/// file and will be reopened from multiple threads.
|
||||
/// run in parallel where possible.
|
||||
///
|
||||
/// Returns two sorted and non-overlapping lists of byte ranges that were
|
||||
/// modified. The first list are the byte regions within the filesystem data
|
||||
@@ -108,8 +111,7 @@ fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
|
||||
/// modified.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn patch_system_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
raw_file: &(dyn ReadWriteAt + Sync),
|
||||
certificate: &Certificate,
|
||||
key: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -122,8 +124,7 @@ pub fn patch_system_image(
|
||||
let parent_span = Span::current();
|
||||
|
||||
let (mut header, footer, image_size) =
|
||||
avb::load_image(input.reopen_boxed().map_err(Error::ReadData)?)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
avb::load_image(UserPosFile::new(raw_file)).map_err(Error::AvbUpdate)?;
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
@@ -144,15 +145,13 @@ pub fn patch_system_image(
|
||||
let offset = chunk * CHUNK_SIZE;
|
||||
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
|
||||
|
||||
let mut reader = input.reopen_boxed().map_err(Error::ReadData)?;
|
||||
reader
|
||||
.seek(SeekFrom::Start(offset))
|
||||
let mut file = UserPosFile::new(raw_file);
|
||||
file.seek(SeekFrom::Start(offset))
|
||||
.map_err(Error::ReadData)?;
|
||||
let buf = reader
|
||||
let buf = file
|
||||
.read_vec_exact(size as usize)
|
||||
.map_err(Error::ReadData)?;
|
||||
|
||||
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) {
|
||||
@@ -171,10 +170,9 @@ pub fn patch_system_image(
|
||||
|
||||
stream::check_cancel(cancel_signal).map_err(Error::WriteData)?;
|
||||
|
||||
writer
|
||||
.seek(SeekFrom::Start(bounds.start))
|
||||
file.seek(SeekFrom::Start(bounds.start))
|
||||
.map_err(Error::WriteData)?;
|
||||
writer.write_all(&new_zip).map_err(Error::WriteData)?;
|
||||
file.write_all(&new_zip).map_err(Error::WriteData)?;
|
||||
|
||||
ranges.push(bounds);
|
||||
}
|
||||
@@ -195,7 +193,7 @@ pub fn patch_system_image(
|
||||
let update_ranges = Some(modified_ranges.as_slice());
|
||||
|
||||
descriptor
|
||||
.update(input, output, update_ranges, cancel_signal)
|
||||
.update(raw_file, update_ranges, cancel_signal)
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
if !header.public_key.is_empty() {
|
||||
@@ -204,8 +202,8 @@ pub fn patch_system_image(
|
||||
header.sign(key).map_err(Error::AvbUpdate)?;
|
||||
}
|
||||
|
||||
let writer = output.reopen_boxed().map_err(Error::WriteData)?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))
|
||||
let file = UserPosFile::new(raw_file);
|
||||
avb::write_appended_image(file, &header, &mut footer, Some(image_size))
|
||||
.map_err(Error::AvbUpdate)?;
|
||||
|
||||
let AppendedDescriptorMut::HashTree(descriptor) =
|
||||
|
||||
+315
-200
@@ -1,11 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
sync::{
|
||||
Arc, Mutex, RwLock,
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
@@ -15,39 +15,32 @@ use ring::digest::Context;
|
||||
|
||||
use crate::util;
|
||||
|
||||
/// A trait for seekable readers. This is only needed because `dyn Read + Seek`
|
||||
/// is not a valid construct in Rust yet.
|
||||
pub trait ReadSeek: Read + Seek {}
|
||||
/// This is only needed because `dyn Read + Seek` is not a valid construct in
|
||||
/// Rust yet.
|
||||
pub trait ReadSeek: Read + Seek {
|
||||
// https://github.com/rust-lang/rust/issues/145752
|
||||
fn issue_145752(&self) {}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> ReadSeek for R {}
|
||||
|
||||
/// A trait for seekable writers. This is only needed because `dyn Write + Seek`
|
||||
/// is not a valid construct in Rust yet.
|
||||
pub trait WriteSeek: Write + Seek {}
|
||||
/// This is only needed because `dyn Write + Seek` is not a valid construct in
|
||||
/// Rust yet.
|
||||
pub trait WriteSeek: Write + Seek {
|
||||
// https://github.com/rust-lang/rust/issues/145752
|
||||
fn issue_145752(&self) {}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> WriteSeek for W {}
|
||||
|
||||
/// A trait for seekable and reopenable readers.
|
||||
pub trait ReadSeekReopen: ReadSeek {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>>;
|
||||
/// This is only needed because `dyn Read + Write + Seek` is not a valid
|
||||
/// construct in Rust yet.
|
||||
pub trait ReadWriteSeek: ReadSeek + WriteSeek {
|
||||
// https://github.com/rust-lang/rust/issues/145752
|
||||
fn issue_145752(&self) {}
|
||||
}
|
||||
|
||||
impl<R: ReadSeek + Reopen + 'static> ReadSeekReopen for R {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>> {
|
||||
Ok(Box::new(self.reopen()?))
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for seekable and reopenable writers.
|
||||
pub trait WriteSeekReopen: WriteSeek {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>>;
|
||||
}
|
||||
|
||||
impl<W: WriteSeek + Reopen + 'static> WriteSeekReopen for W {
|
||||
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>> {
|
||||
Ok(Box::new(self.reopen()?))
|
||||
}
|
||||
}
|
||||
impl<W: ReadSeek + WriteSeek> ReadWriteSeek for W {}
|
||||
|
||||
/// Common function for reading a structure from a reader.
|
||||
pub trait FromReader<R: Read>: Sized {
|
||||
@@ -144,25 +137,121 @@ impl<R: Read> ReadFixedSizeExt for R {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions for file-like types to reopen themselves.
|
||||
pub trait Reopen: Sized {
|
||||
/// Open a new handle to the same file. The new handle is independently
|
||||
/// seekable and the file offset is initially set to 0.
|
||||
fn reopen(&self) -> io::Result<Self>;
|
||||
/// Extensions for file-like types to query the file size. No guarantees are
|
||||
/// made about the state of the underlying file position after performing any
|
||||
/// operation.
|
||||
pub trait FileLen {
|
||||
fn file_len(&self) -> io::Result<u64>;
|
||||
}
|
||||
|
||||
impl<R: Read + Reopen> Reopen for BufReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(Self::new(self.get_ref().reopen()?))
|
||||
macro_rules! file_len_blanket_impl {
|
||||
($type:ty) => {
|
||||
impl<F: ?Sized + FileLen> FileLen for $type {
|
||||
fn file_len(&self) -> io::Result<u64> {
|
||||
(**self).file_len()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
file_len_blanket_impl!(&F);
|
||||
file_len_blanket_impl!(Arc<F>);
|
||||
file_len_blanket_impl!(Box<F>);
|
||||
|
||||
/// Extensions for file-like types that support multi-threaded reads at specific
|
||||
/// offsets. No guarantees are made about the state of underlying file position
|
||||
/// after performing any operation.
|
||||
pub trait ReadAt: FileLen {
|
||||
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize>;
|
||||
|
||||
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
|
||||
let n = self.read_at(buf, offset)?;
|
||||
if n != buf.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!(
|
||||
"Expected to read {} bytes at {offset}, but reached EOF after {n} bytes",
|
||||
buf.len(),
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Reopen> Reopen for BufWriter<W> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(Self::new(self.get_ref().reopen()?))
|
||||
}
|
||||
macro_rules! read_at_blanket_impl {
|
||||
($type:ty) => {
|
||||
impl<R: ?Sized + ReadAt> ReadAt for $type {
|
||||
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
(**self).read_at(buf, offset)
|
||||
}
|
||||
|
||||
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
|
||||
(**self).read_exact_at(buf, offset)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
read_at_blanket_impl!(&R);
|
||||
read_at_blanket_impl!(Arc<R>);
|
||||
read_at_blanket_impl!(Box<R>);
|
||||
|
||||
/// Extensions for file-like types that support multi-threaded writes at
|
||||
/// specific offsets. The behavior is unspecified if writes would overlap. No
|
||||
/// guarantees are made about the state of the underlying file position after
|
||||
/// performing any operation.
|
||||
pub trait WriteAt: FileLen {
|
||||
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize>;
|
||||
|
||||
fn write_all_at(&self, buf: &[u8], offset: u64) -> io::Result<()> {
|
||||
let n = self.write_at(buf, offset)?;
|
||||
if n != buf.len() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
format!(
|
||||
"Expected to write {} bytes at {offset}, but reached EOF after {n} bytes",
|
||||
buf.len(),
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn file_flush(&self) -> io::Result<()>;
|
||||
}
|
||||
|
||||
macro_rules! write_at_blanket_impl {
|
||||
($type:ty) => {
|
||||
impl<W: ?Sized + WriteAt> WriteAt for $type {
|
||||
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
|
||||
(**self).write_at(buf, offset)
|
||||
}
|
||||
|
||||
fn write_all_at(&self, buf: &[u8], offset: u64) -> io::Result<()> {
|
||||
(**self).write_all_at(buf, offset)
|
||||
}
|
||||
|
||||
fn file_flush(&self) -> io::Result<()> {
|
||||
(**self).file_flush()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
write_at_blanket_impl!(&W);
|
||||
write_at_blanket_impl!(Arc<W>);
|
||||
write_at_blanket_impl!(Box<W>);
|
||||
|
||||
/// This is only needed because `dyn ReadAt + WriteAt` is not a valid construct
|
||||
/// in Rust yet.
|
||||
pub trait ReadWriteAt: ReadAt + WriteAt {
|
||||
// https://github.com/rust-lang/rust/issues/145752
|
||||
fn issue_145752(&self) {}
|
||||
}
|
||||
|
||||
impl<F: ReadAt + WriteAt> ReadWriteAt for F {}
|
||||
|
||||
/// A reader wrapper that implements [`Seek`], but only for reporting the
|
||||
/// current file position.
|
||||
pub struct CountingReader<R> {
|
||||
@@ -320,14 +409,6 @@ impl<R: Read + Seek> SectionReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek + Reopen> Reopen for SectionReader<R> {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
let inner = self.inner.reopen()?;
|
||||
|
||||
Self::new(inner, self.start, self.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Read for SectionReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let to_read = self.size.saturating_sub(self.pos).min(buf.len() as u64) as usize;
|
||||
@@ -370,93 +451,179 @@ impl<R: Read + Seek> Seek for SectionReader<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A file wrapper that uses a userspace file offset. A reopened instance uses
|
||||
/// the same underlying kernel file descriptor, but a new userspace file offset,
|
||||
/// initially set to 0.
|
||||
#[derive(Debug)]
|
||||
pub struct PSeekFile {
|
||||
// The lock is needed because flush() takes a `&mut self`.
|
||||
file: Arc<RwLock<File>>,
|
||||
offset: u64,
|
||||
/// A reader wrapper that only allows reading a specific section of a file.
|
||||
pub struct SectionReaderAt<R> {
|
||||
inner: R,
|
||||
start: u64,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl PSeekFile {
|
||||
pub fn new(file: File) -> Self {
|
||||
Self {
|
||||
file: Arc::new(RwLock::new(file)),
|
||||
offset: 0,
|
||||
}
|
||||
impl<R: ReadAt> SectionReaderAt<R> {
|
||||
pub fn new(inner: R, start: u64, size: u64) -> io::Result<Self> {
|
||||
Ok(Self { inner, start, size })
|
||||
}
|
||||
|
||||
pub fn set_len(&self, size: u64) -> io::Result<()> {
|
||||
let file_locked = self.file.read().unwrap();
|
||||
file_locked.set_len(size)
|
||||
pub fn into_inner(self) -> R {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> FileLen for SectionReaderAt<R> {
|
||||
fn file_len(&self) -> io::Result<u64> {
|
||||
Ok(self.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: ReadAt> ReadAt for SectionReaderAt<R> {
|
||||
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
let to_read = self.size.saturating_sub(offset).min(buf.len() as u64) as usize;
|
||||
|
||||
self.inner.read_at(&mut buf[..to_read], self.start + offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// Regular files support parallel reads.
|
||||
impl ReadAt for File {
|
||||
/// Read data from offset. The kernel's file position *will* be changed.
|
||||
#[cfg(windows)]
|
||||
fn read_at(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
use std::os::windows::fs::FileExt;
|
||||
self.file.read().unwrap().seek_read(buf, self.offset)
|
||||
FileExt::seek_read(self, buf, offset)
|
||||
}
|
||||
|
||||
/// Read data from offset. The kernel's file position will *not* be changed.
|
||||
#[cfg(unix)]
|
||||
fn read_at(&self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
use std::os::unix::fs::FileExt;
|
||||
self.file.read().unwrap().read_at(buf, self.offset)
|
||||
FileExt::read_at(self, buf, offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// Regular files support parallel writes.
|
||||
impl WriteAt for File {
|
||||
/// Write data to offset. The kernel's file position *will* be changed.
|
||||
#[cfg(windows)]
|
||||
fn write_at(&self, buf: &[u8]) -> io::Result<usize> {
|
||||
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
|
||||
use std::os::windows::fs::FileExt;
|
||||
self.file.read().unwrap().seek_write(buf, self.offset)
|
||||
FileExt::seek_write(self, buf, offset)
|
||||
}
|
||||
|
||||
/// Write data to offset. The kernel's file position will *not* be changed.
|
||||
#[cfg(unix)]
|
||||
fn write_at(&self, buf: &[u8]) -> io::Result<usize> {
|
||||
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
|
||||
use std::os::unix::fs::FileExt;
|
||||
self.file.read().unwrap().write_at(buf, self.offset)
|
||||
FileExt::write_at(self, buf, offset)
|
||||
}
|
||||
|
||||
fn file_flush(&self) -> io::Result<()> {
|
||||
(&*self).flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Reopen for PSeekFile {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
file: self.file.clone(),
|
||||
offset: 0,
|
||||
})
|
||||
impl FileLen for File {
|
||||
fn file_len(&self) -> io::Result<u64> {
|
||||
(&*self).seek(SeekFrom::End(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for PSeekFile {
|
||||
/// A file wrapper that implements [`ReadAt`] and [`WriteAt`] on top of
|
||||
/// [`Read`], [`Write`], and [`Seek`] via a mutex that makes operations
|
||||
/// single-threaded. This is the inverse of [`UserPosFile`].
|
||||
pub struct MutexFile<F>(Mutex<F>);
|
||||
|
||||
impl<F> MutexFile<F> {
|
||||
pub fn new(file: F) -> Self {
|
||||
Self(Mutex::new(file))
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> F {
|
||||
self.0.into_inner().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Seek> FileLen for MutexFile<F> {
|
||||
fn file_len(&self) -> io::Result<u64> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
inner.seek(SeekFrom::End(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Read + Seek> ReadAt for MutexFile<F> {
|
||||
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
let pos = inner.stream_position()?;
|
||||
|
||||
inner.seek(SeekFrom::Start(offset))?;
|
||||
|
||||
let result = inner.read(buf);
|
||||
|
||||
inner.seek(SeekFrom::Start(pos))?;
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: Write + Seek> WriteAt for MutexFile<F> {
|
||||
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
let pos = inner.stream_position()?;
|
||||
|
||||
inner.seek(SeekFrom::Start(offset))?;
|
||||
|
||||
let result = inner.write(buf);
|
||||
|
||||
inner.seek(SeekFrom::Start(pos))?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn file_flush(&self) -> io::Result<()> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// A file wrapper than implements the standard [`Read`], [`Write`], and
|
||||
/// [`Seek`] traits on top of [`ReadAt`] and [`WriteAt`]. The file position is
|
||||
/// unique for every instance, even if the underlying file is shared. This is
|
||||
/// the inverse of [`MutexFile`].
|
||||
pub struct UserPosFile<F> {
|
||||
file: F,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl<F> UserPosFile<F> {
|
||||
pub fn new(file: F) -> Self {
|
||||
Self { file, offset: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: ReadAt> Read for UserPosFile<F> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let n = self.read_at(buf)?;
|
||||
let n = self.file.read_at(buf, self.offset)?;
|
||||
self.offset += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for PSeekFile {
|
||||
impl<F: WriteAt> Write for UserPosFile<F> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let n = self.write_at(buf)?;
|
||||
let n = self.file.write_at(buf, self.offset)?;
|
||||
self.offset += n as u64;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.file.write().unwrap().flush()
|
||||
self.file.file_flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for PSeekFile {
|
||||
impl<F: FileLen> Seek for UserPosFile<F> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.offset = match pos {
|
||||
SeekFrom::Start(o) => o,
|
||||
SeekFrom::End(o) => {
|
||||
let file_size = self.file.read().unwrap().metadata()?.len();
|
||||
let file_size = self.file.file_len()?;
|
||||
file_size
|
||||
.to_i64()
|
||||
.and_then(|s| s.checked_add(o))
|
||||
@@ -485,70 +652,6 @@ impl Seek for PSeekFile {
|
||||
}
|
||||
}
|
||||
|
||||
/// A small wrapper around a [`Cursor`] that allows multiple instances to share
|
||||
/// the same underlying file. All reads, writes, and seeks are single-threaded.
|
||||
/// This is useful for scenarios where data needs to be copied from multiple
|
||||
/// readers into different parts of the same [`SharedCursor`] writer and the
|
||||
/// read operation is significantly more expensive than the write operation (eg.
|
||||
/// due to decompression).
|
||||
#[derive(Default)]
|
||||
pub struct SharedCursor {
|
||||
inner: Arc<Mutex<Cursor<Vec<u8>>>>,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl SharedCursor {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Reopen for SharedCursor {
|
||||
fn reopen(&self) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: self.inner.clone(),
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for SharedCursor {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.seek(SeekFrom::Start(self.offset))?;
|
||||
|
||||
let n = inner.read(buf)?;
|
||||
self.offset += n as u64;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for SharedCursor {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.seek(SeekFrom::Start(self.offset))?;
|
||||
|
||||
let n = inner.write(buf)?;
|
||||
self.offset += n as u64;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for SharedCursor {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
self.offset = inner.seek(pos)?;
|
||||
Ok(self.offset)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an I/O error with the [`io::ErrorKind::Interrupted`] type if
|
||||
/// `cancel_signal` is true. This should be called frequently in I/O loops for
|
||||
/// cancellation to be responsive.
|
||||
@@ -603,12 +706,14 @@ pub fn copy_n(
|
||||
copy_n_inspect(reader, writer, size, |_| {}, cancel_signal)
|
||||
}
|
||||
|
||||
/// Copy data from `reader` to `writer` until `reader` reaches EOF. If `writer`
|
||||
/// reaches EOF before `reader` does, an error is returned. The operation is
|
||||
/// cancelled on the next loop iteration if `cancel_signal` is set to `true`.
|
||||
pub fn copy(
|
||||
/// Copy data from `reader` to `writer` until `reader` reaches EOF, invoking
|
||||
/// `inspect` after every buffer read iteration. If `writer` reaches EOF before
|
||||
/// `reader` does, an error is returned. The operation is cancelled on the next
|
||||
/// loop iteration if `cancel_signal` is set to `true`.
|
||||
pub fn copy_inspect(
|
||||
mut reader: impl Read,
|
||||
mut writer: impl Write,
|
||||
mut inspect: impl FnMut(&[u8]),
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<u64> {
|
||||
let mut buf = [0u8; 16384];
|
||||
@@ -622,6 +727,8 @@ pub fn copy(
|
||||
break;
|
||||
}
|
||||
|
||||
inspect(&buf[..n]);
|
||||
|
||||
writer.write_all(&buf[..n])?;
|
||||
|
||||
copied += n as u64;
|
||||
@@ -630,6 +737,11 @@ pub fn copy(
|
||||
Ok(copied)
|
||||
}
|
||||
|
||||
/// Copy data from `reader` to `writer` until `reader` reaches EOF.
|
||||
pub fn copy(reader: impl Read, writer: impl Write, cancel_signal: &AtomicBool) -> io::Result<u64> {
|
||||
copy_inspect(reader, writer, |_| {}, cancel_signal)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
@@ -639,10 +751,9 @@ mod tests {
|
||||
|
||||
use ring::digest::Context;
|
||||
|
||||
use super::{
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
|
||||
Reopen, SectionReader, SharedCursor, WriteZerosExt,
|
||||
};
|
||||
use crate::stream::FileLen;
|
||||
|
||||
use super::*;
|
||||
|
||||
const FOOBAR_SHA256: [u8; 32] = [
|
||||
0xc3, 0xab, 0x8f, 0xf1, 0x37, 0x20, 0xe8, 0xad, 0x90, 0x47, 0xdd, 0x39, 0x46, 0x6b, 0x3c,
|
||||
@@ -767,7 +878,7 @@ mod tests {
|
||||
assert_eq!(&buf[..4], b"nner");
|
||||
|
||||
buf = *b"\0\0\0\0\0";
|
||||
reader.seek(SeekFrom::Current(-5)).unwrap();
|
||||
reader.seek_relative(-5).unwrap();
|
||||
reader.read_exact(&mut buf[..3]).unwrap();
|
||||
assert_eq!(&buf[..3], b"inn");
|
||||
|
||||
@@ -776,94 +887,98 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pseek_file() {
|
||||
let raw_file = tempfile::tempfile().unwrap();
|
||||
let mut a = PSeekFile::new(raw_file);
|
||||
let mut b = a.reopen().unwrap();
|
||||
let mut c = b.reopen().unwrap();
|
||||
fn section_reader_at() {
|
||||
let raw_reader = MutexFile::new(Cursor::new(b"fooinnerbar"));
|
||||
let reader = SectionReaderAt::new(raw_reader, 3, 5).unwrap();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
b.write_all(b"world").unwrap();
|
||||
c.seek(SeekFrom::Start(0)).unwrap();
|
||||
c.write_all(b"hi").unwrap();
|
||||
let mut buf = [0u8; 5];
|
||||
reader.read_exact_at(&mut buf[3..5], 3).unwrap();
|
||||
reader.read_exact_at(&mut buf[..3], 0).unwrap();
|
||||
assert_eq!(&buf, b"inner");
|
||||
|
||||
let mut buf = [0u8; 11];
|
||||
a.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf, b"hillorworld");
|
||||
|
||||
let n = a.read_discard(1).unwrap();
|
||||
let n = reader.read_at(&mut buf, 5).unwrap();
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_cursor() {
|
||||
let mut a = SharedCursor::default();
|
||||
let mut b = a.reopen().unwrap();
|
||||
let mut c = b.reopen().unwrap();
|
||||
fn mutex_file() {
|
||||
let file = MutexFile::new(Cursor::new(Vec::new()));
|
||||
assert_eq!(file.file_len().unwrap(), 0);
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
b.write_all(b"world").unwrap();
|
||||
c.seek(SeekFrom::Start(0)).unwrap();
|
||||
c.write_all(b"hi").unwrap();
|
||||
file.write_all_at(b"bar", 3).unwrap();
|
||||
assert_eq!(file.file_len().unwrap(), 6);
|
||||
|
||||
let mut buf = [0u8; 11];
|
||||
a.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf, b"hillorworld");
|
||||
file.write_all_at(b"foo", 0).unwrap();
|
||||
assert_eq!(file.file_len().unwrap(), 6);
|
||||
|
||||
let n = a.read_discard(1).unwrap();
|
||||
assert_eq!(n, 0);
|
||||
let data = file.into_inner().into_inner();
|
||||
assert_eq!(data, b"foobar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy() {
|
||||
fn user_pos_file() {
|
||||
let mut raw_file = tempfile::tempfile().unwrap();
|
||||
raw_file.write_all(b"foobar").unwrap();
|
||||
|
||||
let mut file = UserPosFile::new(raw_file);
|
||||
let mut buf = [0u8; 3];
|
||||
|
||||
file.rewind().unwrap();
|
||||
file.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf, b"foo");
|
||||
|
||||
let pos = file.seek(SeekFrom::End(-3)).unwrap();
|
||||
assert_eq!(pos, 3);
|
||||
|
||||
file.read_exact(&mut buf).unwrap();
|
||||
assert_eq!(&buf, b"bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_functions() {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let mut reader = Cursor::new(b"foobar");
|
||||
let mut writer = Cursor::new([0u8; 6]);
|
||||
|
||||
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap();
|
||||
copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap();
|
||||
assert_eq!(writer.get_ref(), b"foobar");
|
||||
|
||||
// Reader early EOF.
|
||||
reader.seek(SeekFrom::Start(3)).unwrap();
|
||||
writer.rewind().unwrap();
|
||||
let err =
|
||||
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
|
||||
let err = copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
|
||||
|
||||
// Writer early EOF.
|
||||
reader.rewind().unwrap();
|
||||
writer.seek(SeekFrom::Start(3)).unwrap();
|
||||
let err =
|
||||
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
|
||||
let err = copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
|
||||
|
||||
reader.rewind().unwrap();
|
||||
writer.rewind().unwrap();
|
||||
let n = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap();
|
||||
let n = copy(&mut reader, &mut writer, &cancel_signal).unwrap();
|
||||
assert_eq!(n, 6);
|
||||
assert_eq!(writer.get_ref(), b"foobar");
|
||||
|
||||
// Reader early EOF.
|
||||
reader.seek(SeekFrom::Start(3)).unwrap();
|
||||
writer.rewind().unwrap();
|
||||
let n = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap();
|
||||
let n = copy(&mut reader, &mut writer, &cancel_signal).unwrap();
|
||||
assert_eq!(n, 3);
|
||||
|
||||
// Writer early EOF.
|
||||
reader.rewind().unwrap();
|
||||
writer.seek(SeekFrom::Start(3)).unwrap();
|
||||
let err = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
|
||||
let err = copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
|
||||
|
||||
reader.rewind().unwrap();
|
||||
writer.rewind().unwrap();
|
||||
cancel_signal.store(true, Ordering::SeqCst);
|
||||
let err =
|
||||
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
|
||||
let err = copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::Interrupted);
|
||||
let err = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
|
||||
let err = copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::Interrupted);
|
||||
}
|
||||
}
|
||||
|
||||
+73
-10
@@ -8,7 +8,7 @@ use std::{
|
||||
ops::{
|
||||
Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
|
||||
},
|
||||
path::Path,
|
||||
path::{Component, Path, PathBuf},
|
||||
};
|
||||
|
||||
use num_traits::{NumCast, PrimInt};
|
||||
@@ -304,15 +304,76 @@ pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
/// Get the non-empty parent of a path. If the path has no parent in the string,
|
||||
/// then `.` is returned. This does not perform any filesystem operations.
|
||||
pub fn parent_path(path: &Path) -> &Path {
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
return parent;
|
||||
}
|
||||
if let Some(parent) = path.parent()
|
||||
&& !parent.as_os_str().is_empty()
|
||||
{
|
||||
return parent;
|
||||
}
|
||||
|
||||
Path::new(".")
|
||||
}
|
||||
|
||||
/// Path safety-related errors.
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum PathSafetyError {
|
||||
#[error("Path contains multiple components: {0:?}")]
|
||||
NotSingle(PathBuf),
|
||||
#[error("Path contains '..': {0:?}")]
|
||||
HasDotDot(PathBuf),
|
||||
}
|
||||
|
||||
/// Join `parent` with `child`, disallowing unsafe child paths. Absolute child
|
||||
/// paths are converted into relative paths and `..` components result in an
|
||||
/// error.
|
||||
pub fn path_join(
|
||||
parent: impl AsRef<Path>,
|
||||
child: impl AsRef<Path>,
|
||||
) -> Result<PathBuf, PathSafetyError> {
|
||||
fn inner(parent: &Path, child: &Path) -> Result<PathBuf, PathSafetyError> {
|
||||
let mut result = parent.to_owned();
|
||||
|
||||
for component in child.components() {
|
||||
match component {
|
||||
// Make absolute paths relative.
|
||||
Component::Prefix(_) | Component::RootDir | Component::CurDir => continue,
|
||||
// Unsafe path. We block this even if it wouldn't escape the parent.
|
||||
Component::ParentDir => {
|
||||
return Err(PathSafetyError::HasDotDot(child.to_path_buf()));
|
||||
}
|
||||
Component::Normal(os_str) => result.push(os_str),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
inner(parent.as_ref(), child.as_ref())
|
||||
}
|
||||
|
||||
/// Ensure that the path has no directory components.
|
||||
pub fn path_ensure_single(name: impl AsRef<Path>) -> Result<(), PathSafetyError> {
|
||||
fn inner(name: &Path) -> Result<(), PathSafetyError> {
|
||||
// Path::file_name() already checks for `.` and `..`.
|
||||
if name.file_name() != Some(name.as_os_str()) {
|
||||
return Err(PathSafetyError::NotSingle(name.to_path_buf()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
inner(name.as_ref())
|
||||
}
|
||||
|
||||
/// Like [`path_join`], but ensures that the child path contains no directory
|
||||
/// components with [`path_ensure_single`].
|
||||
pub fn path_join_single(
|
||||
parent: impl AsRef<Path>,
|
||||
child: impl AsRef<Path>,
|
||||
) -> Result<PathBuf, PathSafetyError> {
|
||||
path_ensure_single(child.as_ref())?;
|
||||
path_join(parent.as_ref(), child.as_ref())
|
||||
}
|
||||
|
||||
/// Sort and merge overlapping intervals.
|
||||
pub fn merge_overlapping<T>(sections: &[Range<T>]) -> Vec<Range<T>>
|
||||
where
|
||||
@@ -326,11 +387,11 @@ where
|
||||
for section in sections {
|
||||
if section.start >= section.end {
|
||||
continue;
|
||||
} else if let Some(last) = result.last_mut() {
|
||||
if section.start <= last.end {
|
||||
last.end = last.end.max(section.end);
|
||||
continue;
|
||||
}
|
||||
} else if let Some(last) = result.last_mut()
|
||||
&& section.start <= last.end
|
||||
{
|
||||
last.end = last.end.max(section.end);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push(section);
|
||||
@@ -379,6 +440,7 @@ where
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Join arbitrary displayable items with a separator.
|
||||
pub fn join(into_iter: impl IntoIterator<Item = impl Display>, sep: &str) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
@@ -395,6 +457,7 @@ pub fn join(into_iter: impl IntoIterator<Item = impl Display>, sep: &str) -> Str
|
||||
result
|
||||
}
|
||||
|
||||
/// Sort arbitrary sequence of sortable items.
|
||||
pub fn sort<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
|
||||
let mut items = iter.collect::<Vec<_>>();
|
||||
items.sort();
|
||||
|
||||
+16
-14
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -18,7 +18,7 @@ use avbroot::{
|
||||
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
|
||||
KernelCmdlineDescriptor, PropertyDescriptor,
|
||||
},
|
||||
stream::SharedCursor,
|
||||
stream::{MutexFile, UserPosFile},
|
||||
};
|
||||
|
||||
fn get_test_key() -> RsaSigningKey {
|
||||
@@ -306,16 +306,17 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let mut writer = SharedCursor::default();
|
||||
let writer = MutexFile::new(Cursor::new(Vec::new()));
|
||||
let mut pos_writer = UserPosFile::new(&writer);
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
// Write the raw partition data.
|
||||
writer.write_all(&raw_data).unwrap();
|
||||
pos_writer.write_all(&raw_data).unwrap();
|
||||
|
||||
// Generate and write the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.update(&writer, &writer, None, &cancel_signal).unwrap();
|
||||
d.update(&writer, None, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
@@ -334,10 +335,10 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
|
||||
avb::write_appended_image(&mut pos_writer, &header, &mut footer, Some(image_size)).unwrap();
|
||||
let mut data = Vec::new();
|
||||
writer.rewind().unwrap();
|
||||
writer.read_to_end(&mut data).unwrap();
|
||||
pos_writer.rewind().unwrap();
|
||||
pos_writer.read_to_end(&mut data).unwrap();
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
@@ -413,16 +414,17 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let mut writer = SharedCursor::default();
|
||||
let writer = MutexFile::new(Cursor::new(Vec::new()));
|
||||
let mut pos_writer = UserPosFile::new(&writer);
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
// Write the raw partition data.
|
||||
writer.write_all(&raw_data).unwrap();
|
||||
pos_writer.write_all(&raw_data).unwrap();
|
||||
|
||||
// Generate and write the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.update(&writer, &writer, None, &cancel_signal).unwrap();
|
||||
d.update(&writer, None, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
@@ -441,10 +443,10 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, None).unwrap();
|
||||
avb::write_appended_image(&mut pos_writer, &header, &mut footer, None).unwrap();
|
||||
let mut data = Vec::new();
|
||||
writer.rewind().unwrap();
|
||||
writer.read_to_end(&mut data).unwrap();
|
||||
pos_writer.rewind().unwrap();
|
||||
pos_writer.read_to_end(&mut data).unwrap();
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
|
||||
@@ -34,20 +34,13 @@ allow = [
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"bzip2-1.0.6",
|
||||
"GPL-3.0",
|
||||
"GPL-3.0-only",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn"
|
||||
multiple-versions-include-dev = true
|
||||
@@ -63,11 +56,13 @@ include-workspace = true
|
||||
bypass = [
|
||||
# Copies of unmodified crashwrangler objects for old macOS versions.
|
||||
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
|
||||
# Only used in tests.
|
||||
{ name = "libloading", allow-globs = ["tests/nagisa*.dll"] },
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/zip2",
|
||||
"https://github.com/chenxiaolong/system-properties",
|
||||
]
|
||||
|
||||
+3
-12
@@ -4,7 +4,8 @@ version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
publish.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
@@ -14,6 +15,7 @@ avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
rawzip = "0.4.0"
|
||||
ring = "0.17.14"
|
||||
rsa = { version = "0.9.6", features = ["hazmat"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
@@ -24,16 +26,5 @@ tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = "0.2.5"
|
||||
|
||||
# https://github.com/zip-rs/zip2/pull/367
|
||||
# https://github.com/zip-rs/zip2/pull/368
|
||||
# For getting the data offset when writing new zip entries.
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip2"
|
||||
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
|
||||
default-features = false
|
||||
|
||||
[features]
|
||||
static = ["avbroot/static"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+21
-17
@@ -14,8 +14,9 @@ security_patch_level = "2024-01-01"
|
||||
|
||||
[profile.pixel_v4_gki.vabc]
|
||||
# CoW v3 is used starting with the Google Pixel 9a.
|
||||
version = "V3"
|
||||
version = { V3 = { compression_factor = 65536 } }
|
||||
algo = { kind = "Lz4" }
|
||||
force_compression_factor = false
|
||||
|
||||
[profile.pixel_v4_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
@@ -51,12 +52,12 @@ data.version = "vendor_v4"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v4_gki.hashes_streaming]
|
||||
original = "ef6261cd9ebea90f036e52a46160a400c5b8f6ef24ed2469c4a1e9689987aa06"
|
||||
patched = "37fd353a766a7b9a339fbf51fa79c703e94640dc6a2c6310d79357aaefcc7ca1"
|
||||
original = "ea96196191e3a4133db4aff45d47aa3468514e29e0a724faac8081cbf4adf808"
|
||||
patched = "357a448d1a7505b2308ce1c2d19b063e606c60e7349784913a48cdc3f6d50aa4"
|
||||
|
||||
[profile.pixel_v4_gki.hashes_seekable]
|
||||
original = "8a2c717607c10dfa5483d6f9a9f37b3d978acaf8d2ea18e36544af267943e750"
|
||||
patched = "2c4734c9e1d028ee6aaf02bb416e5e173857faffd2ca067366790655147b3afa"
|
||||
original = "f6615ae355eba38689d24aa535981d09175d4832e7c65c04cdd89aa95d21f09d"
|
||||
patched = "9195ba963d9897af2f0821ff6051c1efe3fe130055b7936bedf2cd189998fd89"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
@@ -64,6 +65,8 @@ patched = "2c4734c9e1d028ee6aaf02bb416e5e173857faffd2ca067366790655147b3afa"
|
||||
[profile.pixel_v4_non_gki.vabc]
|
||||
version = "V2"
|
||||
algo = { kind = "Lz4" }
|
||||
# delta_generator sets it for v2, even though it's not used.
|
||||
force_compression_factor = true
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
@@ -93,12 +96,12 @@ data.version = "vendor_v4"
|
||||
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_streaming]
|
||||
original = "630220ef813a2b4743d1941179cc9705da86ad4805f1c52341dcb38fbce3d29e"
|
||||
patched = "b725e91751fe58aed20495aecbf9b4bdc14d2799cd88dcbd58f3a3b02b3af15b"
|
||||
original = "cb2a2e406d2b4c68c8a38f819256a10ada0ed5a4732822bec35bdf7bcdc458eb"
|
||||
patched = "12f15d29fceeb18af14c9a805de3aedb7f270fe8ec316d9d0fe37fb0c667eacd"
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_seekable]
|
||||
original = "1afbe6867ded345d941098ee7c7fcf94a3df52c50ff96ab8f3a67b2ab957259a"
|
||||
patched = "4357b977249006b101002c961916f962787315a80b8608494c6a1f0cf09cecd1"
|
||||
original = "9450c212c34fe55453b52345c7cc22f72397dc80d9ddc792eafc20b461c2afc9"
|
||||
patched = "5018c579df9608f9dfe3add2afd8b176de61ba267376cf3a3e422ecc9d3dd112"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
@@ -106,6 +109,7 @@ patched = "4357b977249006b101002c961916f962787315a80b8608494c6a1f0cf09cecd1"
|
||||
[profile.pixel_v3.vabc]
|
||||
version = "V2"
|
||||
algo = { kind = "Gz" }
|
||||
force_compression_factor = false
|
||||
|
||||
[profile.pixel_v3.partitions.boot]
|
||||
avb.signed = true
|
||||
@@ -136,12 +140,12 @@ data.version = "vendor_v3"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v3.hashes_streaming]
|
||||
original = "9b65037343d45211e0f9706929cba34643a9c54274d1b39740c43f45974984e0"
|
||||
patched = "fb23ab9616968b38b96d1e5e6a503154f89aebc1741e89a9e9dfd2c4d9946b05"
|
||||
original = "4b864b906a13ec98b1d6c3440a5eca7404ec63d073080e80a3b84afabcbd2fa2"
|
||||
patched = "a9077fe32dee8eb7a0369c1334d8f3378ae8044814ae04da700f33294b12a4d8"
|
||||
|
||||
[profile.pixel_v3.hashes_seekable]
|
||||
original = "e581934887dd93b8a9d9c3aa5dec1d48aa7e01bf01ac507e8c5fb256b59cbe7d"
|
||||
patched = "669a826abc6d67e7e0b1def724aa7b470461087255c65663d195df1426a355f0"
|
||||
original = "e0e93ee11de56992f3a5f543858c1d11858b4deb76a91a4caf4fb0f3f34be855"
|
||||
patched = "8730b398367179a0be092a67e4714f09c2267c656fe3e42ec1f8b43bb3a28634"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
@@ -169,9 +173,9 @@ data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes_streaming]
|
||||
original = "f10ee15c900a474cc6bbefa705f272cef42636ea096e75563d2d78f6c4327fd1"
|
||||
patched = "6929f65909037f5550a53982b71e96bdf69ab876bc5e86702c469ed601be8a9a"
|
||||
original = "abc0ad7f80020018101437c2494d6564a877df663d208a123267fc100734c175"
|
||||
patched = "b1876455f5be9d5b6eafc598a017ad10e5e76339137f8b29777097ab19cdc3b0"
|
||||
|
||||
[profile.pixel_v2.hashes_seekable]
|
||||
original = "4e863d251b9ff6eaa1511f9c03e9bdb8919650b2e0eaf23e33892a639edafcaf"
|
||||
patched = "9a103222e73df70a097281525546d25c850df2ae7a2ba715aa5dfbbba3f7972b"
|
||||
original = "57d4ac5ab7d362a593f3c02f3d2044b5400c10db0f732741a2d18e025d4231ec"
|
||||
patched = "40d7674be14e19747e7ead118e0f4faf50880c638dfb4bfc0d0154b0e9202d3f"
|
||||
|
||||
@@ -110,6 +110,7 @@ pub struct Partition {
|
||||
pub struct VabcSettings {
|
||||
pub version: CowVersion,
|
||||
pub algo: VabcAlgo,
|
||||
pub force_compression_factor: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
|
||||
+90
-79
@@ -37,7 +37,7 @@ use avbroot::{
|
||||
ota::{self, SigningWriter, ZipEntry, ZipMode},
|
||||
padding,
|
||||
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcParams},
|
||||
zip::ZipWriterWrapper,
|
||||
zip,
|
||||
},
|
||||
patch::otacert::{self, OtaCertBuildFlags},
|
||||
protobuf::{
|
||||
@@ -46,15 +46,16 @@ use avbroot::{
|
||||
DeltaArchiveManifest, DynamicPartitionGroup, DynamicPartitionMetadata, PartitionUpdate,
|
||||
},
|
||||
},
|
||||
stream::{self, CountingWriter, FromReader, HashingReader, PSeekFile, Reopen, ToWriter},
|
||||
stream::{self, FromReader, HashingReader, ToWriter},
|
||||
util,
|
||||
};
|
||||
use clap::Parser;
|
||||
use rawzip::{CompressionMethod, ZipArchiveWriter};
|
||||
use rsa::{BigUint, rand_core::OsRng, traits::PublicKeyParts};
|
||||
use tempfile::TempDir;
|
||||
use topological_sort::TopologicalSort;
|
||||
use tracing::{info, info_span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{CompressionMethod, DateTime, ZipWriter, write::SimpleFileOptions};
|
||||
|
||||
use crate::{
|
||||
cli::{Cli, Command, HelperCli, ListCli, PassSource, ProfileGroup, TestCli},
|
||||
@@ -96,7 +97,7 @@ fn verify_hash(path: &Path, sha256: &[u8; 32], cancel_signal: &AtomicBool) -> Re
|
||||
}
|
||||
|
||||
fn append_avb(
|
||||
file: &mut PSeekFile,
|
||||
file: &File,
|
||||
name: &str,
|
||||
avb: Avb,
|
||||
hash_tree: bool,
|
||||
@@ -104,7 +105,7 @@ fn append_avb(
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let image_size = file.seek(SeekFrom::End(0))?;
|
||||
let image_size = (&*file).seek(SeekFrom::End(0))?;
|
||||
let salt = ring::digest::digest(&ring::digest::SHA256, b"avbroot");
|
||||
let descriptors = vec![
|
||||
if hash_tree {
|
||||
@@ -126,7 +127,7 @@ fn append_avb(
|
||||
reserved: [0u8; 60],
|
||||
};
|
||||
|
||||
descriptor.update(file, file, None, cancel_signal)?;
|
||||
descriptor.update(file, None, cancel_signal)?;
|
||||
|
||||
Descriptor::HashTree(descriptor)
|
||||
} else {
|
||||
@@ -140,8 +141,8 @@ fn append_avb(
|
||||
reserved: [0u8; 60],
|
||||
};
|
||||
|
||||
file.rewind()?;
|
||||
descriptor.update(&mut *file, cancel_signal)?;
|
||||
(&*file).rewind()?;
|
||||
descriptor.update(file, cancel_signal)?;
|
||||
|
||||
Descriptor::Hash(descriptor)
|
||||
},
|
||||
@@ -189,7 +190,7 @@ fn append_avb(
|
||||
reserved: Default::default(),
|
||||
};
|
||||
|
||||
let eof_size = file.seek(SeekFrom::End(0))?;
|
||||
let eof_size = (&*file).seek(SeekFrom::End(0))?;
|
||||
let full_image_size = eof_size
|
||||
.checked_add(8192)
|
||||
.and_then(|s| padding::round(s, 4096))
|
||||
@@ -294,7 +295,7 @@ fn create_ramdisk(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_boot_image(
|
||||
file: &mut PSeekFile,
|
||||
file: &File,
|
||||
name: &str,
|
||||
avb: Avb,
|
||||
boot_data: &BootData,
|
||||
@@ -411,7 +412,7 @@ fn create_boot_image(
|
||||
}
|
||||
};
|
||||
|
||||
boot_image.to_writer(&mut *file)?;
|
||||
boot_image.to_writer(file)?;
|
||||
|
||||
append_avb(file, name, avb, false, ota_info, key_avb, cancel_signal)
|
||||
.with_context(|| format!("Failed to append AVB metadata for {name}"))?;
|
||||
@@ -421,7 +422,7 @@ fn create_boot_image(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn create_dm_verity_image(
|
||||
file: &mut PSeekFile,
|
||||
file: &File,
|
||||
name: &str,
|
||||
avb: Avb,
|
||||
dm_verity_data: DmVerityData,
|
||||
@@ -432,16 +433,16 @@ fn create_dm_verity_image(
|
||||
) -> Result<()> {
|
||||
match dm_verity_data.content {
|
||||
DmVerityContent::SystemOtacerts => {
|
||||
file.write_all(b"arbitrary_prefix")?;
|
||||
(&*file).write_all(b"arbitrary_prefix")?;
|
||||
|
||||
let data = otacert::create_zip(cert_ota, OtaCertBuildFlags::empty())?;
|
||||
file.write_all(&data)?;
|
||||
(&*file).write_all(&data)?;
|
||||
|
||||
file.write_all(b"arbitrary_suffix")?;
|
||||
(&*file).write_all(b"arbitrary_suffix")?;
|
||||
}
|
||||
}
|
||||
|
||||
padding::write_zeros(&mut *file, 4096)?;
|
||||
padding::write_zeros(file, 4096)?;
|
||||
|
||||
append_avb(file, name, avb, true, ota_info, key_avb, cancel_signal)
|
||||
.with_context(|| format!("Failed to append AVB metadata for {name}"))?;
|
||||
@@ -450,19 +451,18 @@ fn create_dm_verity_image(
|
||||
}
|
||||
|
||||
fn create_vbmeta_image(
|
||||
file: &mut PSeekFile,
|
||||
file: &File,
|
||||
name: &str,
|
||||
avb: Avb,
|
||||
vbmeta_data: &VbmetaData,
|
||||
inputs: &BTreeMap<String, PSeekFile>,
|
||||
inputs: &BTreeMap<String, File>,
|
||||
key: &RsaSigningKey,
|
||||
) -> Result<()> {
|
||||
let mut descriptors = Vec::new();
|
||||
|
||||
for dep in &vbmeta_data.deps {
|
||||
let reader = inputs[dep].reopen()?;
|
||||
let (child_header, _, _) =
|
||||
avb::load_image(reader).with_context(|| format!("Failed to parse AVB image: {dep}"))?;
|
||||
let (child_header, _, _) = avb::load_image(&inputs[dep])
|
||||
.with_context(|| format!("Failed to parse AVB image: {dep}"))?;
|
||||
|
||||
if child_header.public_key.is_empty() {
|
||||
descriptors.extend(child_header.descriptors);
|
||||
@@ -510,7 +510,7 @@ fn create_partition_images(
|
||||
key_avb: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<BTreeMap<String, PSeekFile>> {
|
||||
) -> Result<BTreeMap<String, File>> {
|
||||
let mut topo = TopologicalSort::<&String>::new();
|
||||
|
||||
for (name, partition) in partitions {
|
||||
@@ -529,14 +529,13 @@ fn create_partition_images(
|
||||
};
|
||||
let partition = &partitions[name];
|
||||
|
||||
let mut file = tempfile::tempfile()
|
||||
.map(PSeekFile::new)
|
||||
let file = tempfile::tempfile()
|
||||
.with_context(|| format!("Failed to create temp file for {name}"))?;
|
||||
|
||||
match &partition.data {
|
||||
Data::Boot(data) => {
|
||||
create_boot_image(
|
||||
&mut file,
|
||||
&file,
|
||||
name,
|
||||
partition.avb,
|
||||
data,
|
||||
@@ -549,7 +548,7 @@ fn create_partition_images(
|
||||
}
|
||||
Data::DmVerity(data) => {
|
||||
create_dm_verity_image(
|
||||
&mut file,
|
||||
&file,
|
||||
name,
|
||||
partition.avb,
|
||||
*data,
|
||||
@@ -561,7 +560,7 @@ fn create_partition_images(
|
||||
.with_context(|| format!("Failed to create dm-verity image: {name}"))?;
|
||||
}
|
||||
Data::Vbmeta(data) => {
|
||||
create_vbmeta_image(&mut file, name, partition.avb, data, &files, key_avb)
|
||||
create_vbmeta_image(&file, name, partition.avb, data, &files, key_avb)
|
||||
.with_context(|| format!("Failed to create vbmeta image: {name}"))?;
|
||||
}
|
||||
}
|
||||
@@ -575,14 +574,12 @@ fn create_partition_images(
|
||||
fn create_payload(
|
||||
writer: impl Write,
|
||||
partitions: &BTreeMap<String, Partition>,
|
||||
inputs: &BTreeMap<String, PSeekFile>,
|
||||
inputs: &BTreeMap<String, File>,
|
||||
ota_info: &OtaInfo,
|
||||
profile: &Profile,
|
||||
key_ota: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(String, u64)> {
|
||||
const COMPRESSION_FACTOR: u32 = 64 * 1024;
|
||||
|
||||
let dynamic_partitions_names = partitions
|
||||
.iter()
|
||||
.filter(|(_, p)| matches!(&p.data, Data::DmVerity(_)))
|
||||
@@ -590,18 +587,16 @@ fn create_payload(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut payload_partitions = vec![];
|
||||
let mut compressed = BTreeMap::<&String, PSeekFile>::new();
|
||||
let mut compressed = BTreeMap::<&String, File>::new();
|
||||
|
||||
for (name, file) in inputs {
|
||||
let writer = tempfile::tempfile()
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to create temp file for: {name}"))?;
|
||||
|
||||
let vabc_params = if dynamic_partitions_names.contains(name) {
|
||||
profile.vabc.map(|v| VabcParams {
|
||||
version: v.version,
|
||||
algo: v.algo,
|
||||
compression_factor: COMPRESSION_FACTOR,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -612,7 +607,9 @@ fn create_payload(
|
||||
|
||||
compressed.insert(name, writer);
|
||||
|
||||
let is_v3 = profile.vabc.is_some_and(|e| e.version == CowVersion::V3);
|
||||
let is_v3 = profile
|
||||
.vabc
|
||||
.is_some_and(|e| matches!(e.version, CowVersion::V3 { .. }));
|
||||
|
||||
payload_partitions.push(PartitionUpdate {
|
||||
partition_name: name.clone(),
|
||||
@@ -660,10 +657,13 @@ fn create_payload(
|
||||
vabc_compression_param: profile.vabc.map(|v| v.algo.to_string()),
|
||||
cow_version: profile.vabc.map(|v| match v.version {
|
||||
CowVersion::V2 => 2,
|
||||
CowVersion::V3 => 3,
|
||||
CowVersion::V3 { .. } => 3,
|
||||
}),
|
||||
vabc_feature_set: None,
|
||||
compression_factor: profile.vabc.map(|_| COMPRESSION_FACTOR.into()),
|
||||
compression_factor: profile.vabc.and_then(|v| match v.version {
|
||||
CowVersion::V2 => v.force_compression_factor.then_some(64 * 1024),
|
||||
CowVersion::V3 { compression_factor } => Some(compression_factor.into()),
|
||||
}),
|
||||
}),
|
||||
partial_update: None,
|
||||
apex_info: vec![],
|
||||
@@ -739,40 +739,32 @@ fn create_ota(
|
||||
.truncate(true)
|
||||
.open(output)
|
||||
.with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
let mut zip_writer = match zip_mode {
|
||||
ZipMode::Streaming => {
|
||||
let signing_writer = SigningWriter::new_streaming(raw_writer);
|
||||
ZipWriterWrapper::new_streaming(signing_writer)
|
||||
}
|
||||
ZipMode::Seekable => {
|
||||
let signing_writer = SigningWriter::new_seekable(raw_writer);
|
||||
ZipWriterWrapper::new_seekable(signing_writer)
|
||||
}
|
||||
let signing_writer = match zip_mode {
|
||||
ZipMode::Streaming => SigningWriter::new_streaming(raw_writer),
|
||||
ZipMode::Seekable => SigningWriter::new_seekable(raw_writer),
|
||||
};
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(CompressionMethod::Stored)
|
||||
.large_file(false);
|
||||
let mut zip_writer = ZipArchiveWriter::new(signing_writer);
|
||||
|
||||
let mut entries = vec![];
|
||||
let mut properties = None;
|
||||
let mut payload_metadata_size = None;
|
||||
|
||||
for path in [ota::PATH_OTACERT, ota::PATH_PAYLOAD, ota::PATH_PROPERTIES] {
|
||||
// All remaining entries are written immediately.
|
||||
let offset = zip_writer
|
||||
.start_file(path, options)
|
||||
let (entry_writer, data_config) = zip_writer
|
||||
.new_file(path)
|
||||
.start()
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let mut writer = CountingWriter::new(&mut zip_writer);
|
||||
let offset = entry_writer.stream_offset();
|
||||
let mut data_writer = data_config.wrap(entry_writer);
|
||||
|
||||
match path {
|
||||
ota::PATH_OTACERT => {
|
||||
crypto::write_pem_cert(Path::new(path), &mut writer, cert_ota)
|
||||
crypto::write_pem_cert(Path::new(path), &mut data_writer, cert_ota)
|
||||
.with_context(|| format!("Failed to write entry: {path}"))?;
|
||||
}
|
||||
ota::PATH_PAYLOAD => {
|
||||
let (p, m) = create_payload(
|
||||
&mut writer,
|
||||
&mut data_writer,
|
||||
&profile.partitions,
|
||||
&inputs,
|
||||
ota_info,
|
||||
@@ -786,15 +778,17 @@ fn create_ota(
|
||||
payload_metadata_size = Some(m);
|
||||
}
|
||||
ota::PATH_PROPERTIES => {
|
||||
writer
|
||||
data_writer
|
||||
.write_all(properties.as_ref().unwrap().as_bytes())
|
||||
.with_context(|| format!("Failed to write payload properties: {path}"))?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// Cannot fail.
|
||||
let size = writer.stream_position()?;
|
||||
let size = data_writer
|
||||
.finish()
|
||||
.and_then(|(w, d)| w.finish(d))
|
||||
.with_context(|| format!("Failed to finalize zip entry: {path}"))?;
|
||||
|
||||
entries.push(ZipEntry {
|
||||
path: path.to_owned(),
|
||||
@@ -831,18 +825,15 @@ fn create_ota(
|
||||
spl_downgrade: false,
|
||||
};
|
||||
|
||||
let data_descriptor_size = match zip_mode {
|
||||
ZipMode::Streaming => 16,
|
||||
ZipMode::Seekable => 0,
|
||||
};
|
||||
let next_offset = zip_writer.stream_offset();
|
||||
|
||||
ota::add_metadata(
|
||||
&entries,
|
||||
&mut zip_writer,
|
||||
// Offset where next entry would begin.
|
||||
entries.last().map(|e| e.offset + e.size).unwrap() + data_descriptor_size,
|
||||
next_offset,
|
||||
&metadata,
|
||||
payload_metadata_size.unwrap(),
|
||||
zip_mode,
|
||||
)
|
||||
.context("Failed to write new OTA metadata")?;
|
||||
|
||||
@@ -860,13 +851,19 @@ fn create_ota(
|
||||
}
|
||||
|
||||
fn create_fake_magisk(output: &Path) -> Result<()> {
|
||||
let raw_writer =
|
||||
File::create(output).with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
let mut zip_writer = ZipWriter::new(raw_writer);
|
||||
let options = SimpleFileOptions::default().last_modified_time(DateTime::default());
|
||||
let raw_writer = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(output)
|
||||
.with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
let mut zip_writer = ZipArchiveWriter::new(raw_writer);
|
||||
let compression_method = CompressionMethod::Deflate;
|
||||
|
||||
for path in [
|
||||
"assets/stub.apk",
|
||||
"assets/util_functions.sh",
|
||||
"lib/arm64-v8a/libinit-ld.so",
|
||||
"lib/arm64-v8a/libmagisk64.so",
|
||||
"lib/arm64-v8a/libmagiskinit.so",
|
||||
@@ -880,13 +877,31 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
|
||||
"lib/x86_64/libmagisk64.so",
|
||||
"lib/x86_64/libmagiskinit.so",
|
||||
] {
|
||||
zip_writer.start_file(path, options)?;
|
||||
write!(zip_writer, "dummy contents for {path}")?;
|
||||
let (entry_writer, data_config) = zip_writer
|
||||
.new_file(path)
|
||||
.compression_method(compression_method)
|
||||
.start()
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let compressed_writer = zip::compressed_writer(entry_writer, compression_method)
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let mut data_writer = data_config.wrap(compressed_writer);
|
||||
|
||||
if path == "assets/util_functions.sh" {
|
||||
// avbroot looks for the version number in this file.
|
||||
data_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
|
||||
} else {
|
||||
write!(data_writer, "dummy contents for {path}")?;
|
||||
}
|
||||
|
||||
data_writer
|
||||
.finish()
|
||||
.and_then(|(w, d)| w.finish()?.finish(d))?;
|
||||
}
|
||||
|
||||
// avbroot looks for the version number in this file.
|
||||
zip_writer.start_file("assets/util_functions.sh", options)?;
|
||||
zip_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
|
||||
let raw_writer = zip_writer.finish()?;
|
||||
|
||||
zip::make_non_streaming(raw_writer)
|
||||
.with_context(|| format!("Failed to convert to non-streaming zip: {output:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1140,7 +1155,7 @@ fn clean_boot_image_certs(path: &Path, cancel_signal: &AtomicBool) -> Result<()>
|
||||
.iter_mut()
|
||||
.find(|e| e.path == b"system/etc/security/otacerts.zip")
|
||||
{
|
||||
let zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let zip_writer = ZipArchiveWriter::new(Cursor::new(Vec::new()));
|
||||
let empty_zip = zip_writer.finish()?.into_inner();
|
||||
|
||||
entry.data = CpioEntryData::Data(empty_zip);
|
||||
@@ -1222,10 +1237,6 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
];
|
||||
|
||||
for name in profiles {
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
bail!("Unsafe profile name: {name}");
|
||||
}
|
||||
|
||||
let profile = &config.profile[name];
|
||||
|
||||
for (zip_mode, hashes) in [
|
||||
@@ -1235,7 +1246,7 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let _span = info_span!("profile", name, %zip_mode).entered();
|
||||
|
||||
// Can't use NamedTempFile because avbroot does atomic replaces.
|
||||
let mut profile_dir = work_dir.join(name);
|
||||
let mut profile_dir = util::path_join_single(work_dir, name)?;
|
||||
profile_dir.push(zip_mode.to_string());
|
||||
let out_original = profile_dir.join("ota.zip");
|
||||
let out_magisk = profile_dir.join("ota_magisk.zip");
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
publish.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
+6
-4
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#[cfg(not(windows))]
|
||||
@@ -7,7 +7,7 @@ mod fuzz {
|
||||
|
||||
use avbroot::{
|
||||
format::fec::FecImage,
|
||||
stream::{FromReader, SharedCursor, WriteZerosExt},
|
||||
stream::{FromReader, MutexFile, UserPosFile, WriteZerosExt},
|
||||
};
|
||||
use honggfuzz::fuzz;
|
||||
|
||||
@@ -18,12 +18,14 @@ mod fuzz {
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
if let Ok(fec) = FecImage::from_reader(reader) {
|
||||
let mut input = SharedCursor::new();
|
||||
let input = MutexFile::new(Cursor::new(Vec::new()));
|
||||
|
||||
// Allow verify() to get further, but don't blow up the host
|
||||
// with excessive memory usage.
|
||||
if fec.data_size < 64 * 1024 * 1024 {
|
||||
input.write_zeros_exact(fec.data_size).unwrap();
|
||||
UserPosFile::new(&input)
|
||||
.write_zeros_exact(fec.data_size)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let _ = fec.verify(&input, &cancel_signal);
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@ version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
publish.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
||||
Reference in New Issue
Block a user