Compare commits

...

45 Commits

Author SHA1 Message Date
Andrew Gunnerson 05cd74719e Version 3.10.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 19:20:39 -05:00
Andrew Gunnerson 7e0d5584d9 CHANGELOG.md: Add entry for PR #392
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 19:01:08 -05:00
Andrew Gunnerson b351d47f28 Update dependencies and fix most pedantic clippy warnings
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 18:46:40 -05:00
Andrew Gunnerson 421b0a5207 CHANGELOG.md: Add entry for PR #391
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 17:14:21 -05:00
Andrew Gunnerson cad08a6a2f Add support for Magisk 28100
There is nothing new that breaks compatibility with avbroot.

Fixes: #389

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:21:05 -05:00
Andrew Gunnerson 0a32dfaec3 CHANGELOG.md: Add entry for PR #390
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:19:46 -05:00
Andrew Gunnerson b03b2ade6a Improve parser error messages
* Remove the ReadString trait and perform UTF-8 conversions explicitly.
  UTF-8 conversion errors are now individual errors instead of being
  hidden in an Io error.
* Add new IntOutOfBounds errors that include the actual field value and
  the range of values that are valid for the field. Integer casting also
  uses this error type where possible.
* Replace remaining uses of FieldOutOfBounds with IntOverflow, which is
  returned when checked arithmetic fails. For code simplicity, it does
  not store intermediate values.
* Replace ReadFieldError and WriteFieldError with the generic Io error
  to simplify the code. They were used inconsistently anyway.
* Use fully qualified field names in avb and bootimage error messages
  since the same name frequently appears in multiple structs.
* Replace InvalidFieldValue with more specific errors in bootimage.
* Replace all stringly-typed errors in lp and sparse with (very)
  specific errors.
* Fix the wording in a number of errors.
* Add new ReadFixedSizeExt trait for reading fixed size arrays and vecs
  from Read types.
* Remove some fallible casts from u32 to usize and from usize to u64.
  avbroot only supports 32-bit and 64-bit systems anyway.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:08:28 -05:00
Andrew Gunnerson c198646c7c Add documentation for Android 16's developer switch for 16K page size kernels
avbroot will not support patching these internal OTAs because it
requires modifying filesystems and creating incremental OTAs. However,
it should be possible to do this manually if someone really wants to try
out a 16K page size kernel.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-06 18:09:34 -05:00
Andrew Gunnerson d815a78e9a CHANGELOG.md: Add entry for PR #386
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:47:31 -05:00
Andrew Gunnerson 99c5800f96 Update dependencies and pin Github Actions actions to specific commits
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:42:35 -05:00
Andrew Gunnerson c801fbb069 CHANGELOG.md: Add entry for PR #385
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:37:54 -05:00
Andrew Gunnerson 0b250086a3 protobuf: Update update_metadata.proto from AOSP
Neither of the two new fields need any special handling in avbroot. This
just allows us to preserve the values from the original OTA.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:33:34 -05:00
Andrew Gunnerson f6c6c8ab40 CHANGELOG.md: Add entry for PR #384
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:27:38 -05:00
Andrew Gunnerson 5c353a5f42 Remove unused byteorder crate
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:57 -05:00
Andrew Gunnerson ad932ed445 sparse: Switch to read_from_io()/write_to_io()
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:44 -05:00
Andrew Gunnerson d04d5451dc stream: Remove unused padded string functions
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:30 -05:00
Andrew Gunnerson 1d5db9f731 bootimage: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:28 -05:00
Andrew Gunnerson 409b9298a5 avb: Switch to zerocopy
This commit also fixes a bug where avb::Header::release_string was
allowed to take the full 48-bytes, which was incorrect because libavb
expects the field to be NULL-terminated. This was not a problem in
practice because the release string is usually short and even if it
wasn't, the 80 reserved bytes that immediately follow it are all zeros.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:01:43 -05:00
Andrew Gunnerson 516238d907 hashtree: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:59:56 -05:00
Andrew Gunnerson 39d5fbf76d payload: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:58:35 -05:00
Andrew Gunnerson 20f5bdacc7 compression: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:58:08 -05:00
Andrew Gunnerson 5b8eefa867 fec: Switch to zerocopy
This commit also adds support for parsing FEC images with unknown extra
custom fields.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:57:55 -05:00
Andrew Gunnerson b2610fa0e9 Update zerocopy to 0.8.11
>=0.8.10 is needed for the new read_from_io()/write_to_io() helper
functions.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:57:39 -05:00
Andrew Gunnerson 8a5550a43c Add new traits for zero padding and unpadding strings
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:56:55 -05:00
Ivan Katrovsky e16f4c7fa7 README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/10c425dedea262678fda3c0c097b82fdf4352f08
2024-11-13 17:54:14 +03:00
Andrew Gunnerson 983e6c40a5 Version 3.9.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 20:04:59 -05:00
Andrew Gunnerson 8729854727 CHANGELOG.md: Add entry for PR #377
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:55:44 -05:00
Andrew Gunnerson 0d1beb7734 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:54:50 -05:00
Andrew Gunnerson 2cd0d69238 CHANGELOG.md: Add entry for PR #376
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:43:50 -05:00
Andrew Gunnerson c0e65264e5 sparse: Allow parsing files with unknown fields
The official AOSP implementation does, so we should too. This allows
unpacking Samsung's sparse images, which have an extra 4 bytes in both
the file header and chunk headers.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:41:27 -05:00
Andrew Gunnerson 37b15a2b6a CHANGELOG.md: Add entry for PR #374
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:38:56 -05:00
Andrew Gunnerson e4994fbe98 format/payload: Allow verifying signatures without an unpadded size
Older OTAs created before the payload metadata format supported EC
signatures will not have the `unpadded_signature_size` field set. In
this case, we'll just use the full length of `data`, which is what
update_engine also does.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:38:28 -05:00
Andrew Gunnerson cf1bacab30 CHANGELOG.md: Add entry for PR #373
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:27:24 -05:00
Andrew Gunnerson 4832121160 format/ota: Allow verifying OTAs without metadata.pb
While patched OTAs produced by avbroot always include the protobuf
version of the OTA metadata, the original OTA may not. Verifying those
with `avbroot ota verify` is a valid use case.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:25:00 -05:00
Andrew Gunnerson c614e61744 CHANGELOG.md: Add entry for PR #371
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-07 22:13:45 -05:00
Andrew Gunnerson 9ebce1666c Add option to skip verifying recovery's OTA cert too
Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-07 22:04:11 -05:00
Andrew Gunnerson fb34198ffc CHANGELOG.md: Add entry for PR #370
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 18:00:13 -05:00
Andrew Gunnerson 4a5eab4ba0 crypto: Fix minor clippy warning
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:59:26 -05:00
Andrew Gunnerson 1e1818ad8f CHANGELOG.md: Add entry for PR #369
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:58:59 -05:00
Andrew Gunnerson 3f25ad7c76 Add more context for avb::Header::set_algo_for_key() calls
Previously, it was not always obvious which key was problematic.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:53:01 -05:00
Andrew Gunnerson 37e28eb040 CHANGELOG.md: Add entry for PR #367
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:51:41 -05:00
Andrew Gunnerson ce87757fd1 patch/boot: Avoid loading boot images when there are no patchers
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 09:30:04 -05:00
Andrew Gunnerson 10c425dede Add option to skip inserting OTA cert into recovery image
Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 09:30:03 -05:00
Andrew Gunnerson 8a0d147993 CHANGELOG.md: Add entry for PR #368
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-04 18:54:23 -05:00
Andrew Gunnerson 062aa21485 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-04 18:32:58 -05:00
40 changed files with 2443 additions and 1748 deletions
+3 -3
View File
@@ -42,7 +42,7 @@ jobs:
android_api: '31'
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# For git describe
fetch-depth: 0
@@ -84,7 +84,7 @@ jobs:
done
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2.7.5
with:
key: ${{ matrix.artifact.name }}
@@ -155,7 +155,7 @@ jobs:
run: cp LICENSE README.md target/output/
- name: Archive executable
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
path: |
+2 -3
View File
@@ -1,4 +1,3 @@
---
name: cargo-deny
on:
push:
@@ -11,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@v1
uses: EmbarkStudios/cargo-deny-action@e2f4ede4a4e60ea15ff31bc0647485d80c66cfba # v2.0.4
+2 -3
View File
@@ -1,4 +1,3 @@
---
name: Github Release
on:
push:
@@ -25,10 +24,10 @@ jobs:
echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Create release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
with:
tag_name: v${{ steps.get_version.outputs.version }}
name: Version ${{ steps.get_version.outputs.version }}
+34
View File
@@ -7,6 +7,24 @@
to update the actual links at the bottom of the file.
-->
### Version 3.10.0
* Switch to using zerocopy library for all binary file format parsers ([PR #384])
* Update to latest AOSP protobuf schema for the `payload.bin` metadata file format ([PR #385])
* Update dependencies and pin Github Actions actions to specific commits ([PR #386], [PR #392])
* Improve error messages from file format parsers ([PR #390])
* Add support for Magisk 28100 ([PR #391])
### Version 3.9.0
* Update all dependencies ([PR #368], [PR #377])
* Add advanced option to skip replacing the OTA certificate in the recovery image ([Issue #366], [PR #367], [PR #371])
* Improve error message when an incompatible RSA key is used for AVB signing ([Issue #366], [PR #369])
* Fix clippy warnings ([PR #370])
* Allow `avbroot ota verify` to verify OTAs that lack `META-INF/com/android/metadata.pb` ([Issue #366], [PR #373])
* Allow `avbroot ota verify` to verify OTAs where the payload signature does not set `unpadded_signature_size` ([Issue #366], [PR #374])
* Allow `avbroot sparse` to parse sparse images with unknown fields (matches AOSP implementation) ([PR #376])
### Version 3.8.0
* Add `avbroot avb digest` subcommand for computing the special vbmeta digest ([PR #363])
@@ -265,6 +283,7 @@ Behind-the-scenes changes:
[Issue #328]: https://github.com/chenxiaolong/avbroot/issues/328
[Issue #332]: https://github.com/chenxiaolong/avbroot/issues/332
[Issue #356]: https://github.com/chenxiaolong/avbroot/issues/356
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
[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
@@ -376,3 +395,18 @@ Behind-the-scenes changes:
[PR #362]: https://github.com/chenxiaolong/avbroot/pull/362
[PR #363]: https://github.com/chenxiaolong/avbroot/pull/363
[PR #364]: https://github.com/chenxiaolong/avbroot/pull/364
[PR #367]: https://github.com/chenxiaolong/avbroot/pull/367
[PR #368]: https://github.com/chenxiaolong/avbroot/pull/368
[PR #369]: https://github.com/chenxiaolong/avbroot/pull/369
[PR #370]: https://github.com/chenxiaolong/avbroot/pull/370
[PR #371]: https://github.com/chenxiaolong/avbroot/pull/371
[PR #373]: https://github.com/chenxiaolong/avbroot/pull/373
[PR #374]: https://github.com/chenxiaolong/avbroot/pull/374
[PR #376]: https://github.com/chenxiaolong/avbroot/pull/376
[PR #377]: https://github.com/chenxiaolong/avbroot/pull/377
[PR #384]: https://github.com/chenxiaolong/avbroot/pull/384
[PR #385]: https://github.com/chenxiaolong/avbroot/pull/385
[PR #386]: https://github.com/chenxiaolong/avbroot/pull/386
[PR #390]: https://github.com/chenxiaolong/avbroot/pull/390
[PR #391]: https://github.com/chenxiaolong/avbroot/pull/391
[PR #392]: https://github.com/chenxiaolong/avbroot/pull/392
Generated
+184 -203
View File
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "adler2"
@@ -36,9 +36,9 @@ checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b"
[[package]]
name = "anstream"
version = "0.6.15"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
@@ -51,49 +51,49 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.8"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "anyhow"
version = "1.0.89"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "arbitrary"
version = "1.3.2"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
[[package]]
name = "assert_matches"
@@ -109,14 +109,13 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "avbroot"
version = "3.8.0"
version = "3.10.0"
dependencies = [
"anyhow",
"assert_matches",
"base64",
"bitflags",
"bstr",
"byteorder",
"bzip2",
"cap-std",
"cap-tempfile",
@@ -154,14 +153,14 @@ dependencies = [
"sha1",
"sha2",
"tempfile",
"thiserror",
"thiserror 2.0.6",
"toml_edit",
"topological-sort",
"tracing",
"tracing-subscriber",
"x509-cert",
"zerocopy 0.8.5",
"zerocopy-derive 0.8.5",
"zerocopy 0.8.13",
"zerocopy-derive 0.8.13",
"zip",
]
@@ -212,9 +211,9 @@ dependencies = [
[[package]]
name = "bstr"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c"
checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22"
dependencies = [
"memchr",
"regex-automata",
@@ -229,9 +228,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.7.2"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "bzip2"
@@ -254,9 +253,9 @@ dependencies = [
[[package]]
name = "cap-primitives"
version = "3.3.0"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff5bcbaf57897c8f14098cc9ad48a78052930a9948119eea01b80ca224070fa6"
checksum = "8fc15faeed2223d8b8e8cc1857f5861935a06d06713c4ac106b722ae9ce3c369"
dependencies = [
"ambient-authority",
"fs-set-times",
@@ -265,15 +264,15 @@ dependencies = [
"ipnet",
"maybe-owned",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
"winx",
]
[[package]]
name = "cap-std"
version = "3.3.0"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6cf1a22e6eab501e025a9953532b1e95efb8a18d6364bf8a4a7547b30c49186"
checksum = "c3dbd3e8e8d093d6ccb4b512264869e1281cdb032f7940bd50b2894f96f25609"
dependencies = [
"cap-primitives",
"io-extras",
@@ -283,9 +282,9 @@ dependencies = [
[[package]]
name = "cap-tempfile"
version = "3.3.0"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8563f37bd2d9ec79a08dc6b062b6733adc84f929d23f45388ba52025c7b32e26"
checksum = "1ffa1c0edc4958d742bab2e903e52f93ccee482072680e08d6ce0784873e65b1"
dependencies = [
"cap-std",
"rand",
@@ -304,9 +303,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.1.30"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d"
dependencies = [
"jobserver",
"libc",
@@ -337,9 +336,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.20"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
dependencies = [
"clap_builder",
"clap_derive",
@@ -347,9 +346,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.20"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
dependencies = [
"anstream",
"anstyle",
@@ -359,9 +358,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.33"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9646e2e245bf62f45d39a0f3f36f1171ad1ea0d6967fd114bca72cb02a8fcdfb"
checksum = "d9647a559c112175f17cf724dc72d3645680a883c58481332779192b0d8e7a01"
dependencies = [
"clap",
]
@@ -375,14 +374,14 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
name = "clap_lex"
version = "0.7.2"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "cms"
@@ -398,9 +397,9 @@ dependencies = [
[[package]]
name = "colorchoice"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "const-oid"
@@ -408,26 +407,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "constcat"
version = "0.5.1"
@@ -436,9 +415,9 @@ checksum = "4938185353434999ef52c81753c8cca8955ed38042fc29913db3751916f3b7ab"
[[package]]
name = "cpufeatures"
version = "0.2.14"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [
"libc",
]
@@ -477,12 +456,6 @@ version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -559,7 +532,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
@@ -576,16 +549,13 @@ dependencies = [
[[package]]
name = "dlv-list"
version = "0.5.2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
checksum = "ecb08c4819242b1ec89b3d0c6affa229005bef46ae4f7eed8b80768187c10087"
[[package]]
name = "e2e"
version = "3.8.0"
version = "3.10.0"
dependencies = [
"anyhow",
"avbroot",
@@ -618,12 +588,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.9"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -634,9 +604,9 @@ checksum = "cfc25fd417983cc7f203394ebb89eba18e2df1b0ac1be2673091b5aca52b595f"
[[package]]
name = "fastrand"
version = "2.1.1"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fixedbitset"
@@ -652,9 +622,9 @@ checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec"
[[package]]
name = "flate2"
version = "1.0.34"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -668,18 +638,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fs-set-times"
version = "0.20.1"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "033b337d725b97690d86893f9de22b67b80dcc4e9ad815f348254c38119db8fb"
checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4"
dependencies = [
"io-lifetimes",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "fuzz"
version = "3.8.0"
version = "3.10.0"
dependencies = [
"avbroot",
"honggfuzz",
@@ -731,9 +701,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.15.0"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
name = "heck"
@@ -779,9 +749,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "indexmap"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [
"equivalent",
"hashbrown",
@@ -799,19 +769,19 @@ dependencies = [
[[package]]
name = "io-extras"
version = "0.18.2"
version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9f046b9af244f13b3bd939f55d16830ac3a201e8a9ba9661bfcb03e2be72b9b"
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
dependencies = [
"io-lifetimes",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "io-lifetimes"
version = "2.0.3"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c"
checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983"
[[package]]
name = "ipnet"
@@ -836,9 +806,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.11"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
@@ -860,15 +830,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.159"
version = "0.2.167"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc"
[[package]]
name = "liblzma"
version = "0.3.4"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c45fc6fcf5b527d3cf89c1dee8c327943984b0dc8bfcf6e100473b00969e63"
checksum = "603222e049bf0da71529325ada5d02dc3871cbd3679cf905429f7f0de93da87b"
dependencies = [
"liblzma-sys",
]
@@ -886,9 +856,9 @@ dependencies = [
[[package]]
name = "libm"
version = "0.2.8"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "linux-raw-sys"
@@ -913,9 +883,9 @@ dependencies = [
[[package]]
name = "logos-codegen"
version = "0.14.2"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b32eb6b5f26efacd015b000bfc562186472cd9b34bdba3f6b264e2a052676d10"
checksum = "5f3303189202bb8a052bcd93d66b6c03e6fe70d9c7c47c0ea5e974955e54c876"
dependencies = [
"beef",
"fnv",
@@ -923,14 +893,15 @@ dependencies = [
"proc-macro2",
"quote",
"regex-syntax",
"syn 2.0.79",
"rustc_version",
"syn 2.0.90",
]
[[package]]
name = "logos-derive"
version = "0.14.2"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5d0c5463c911ef55624739fc353238b4e310f0144be1f875dc42fec6bfd5ec"
checksum = "774a1c225576486e4fdf40b74646f672c542ca3608160d348749693ae9d456e6"
dependencies = [
"logos-codegen",
]
@@ -967,25 +938,25 @@ dependencies = [
[[package]]
name = "miette"
version = "7.2.0"
version = "7.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1"
checksum = "317f146e2eb7021892722af37cf1b971f0a70c8406f487e24952667616192c64"
dependencies = [
"cfg-if",
"miette-derive",
"thiserror",
"thiserror 1.0.69",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "7.2.0"
version = "7.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
checksum = "23c9b935fbe1d6cbd1dac857b54a688145e2d93f48db36010514d0f612d0ad67"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
@@ -1087,9 +1058,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "passterm"
version = "2.0.3"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eea7e8981bca32c52230ca5f28b080dd5f28aed618a7bd12b5a382b234cd2b99"
checksum = "150ca2316c7813c688677784f20bb0a9efab639415ae1961869863ee99a81e51"
dependencies = [
"libc",
]
@@ -1153,7 +1124,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
@@ -1167,9 +1138,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.14"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pkcs1"
@@ -1226,28 +1197,28 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.22"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
dependencies = [
"proc-macro2",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
name = "proc-macro2"
version = "1.0.87"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "prost"
version = "0.13.3"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f"
checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec"
dependencies = [
"bytes",
"prost-derive",
@@ -1255,11 +1226,10 @@ dependencies = [
[[package]]
name = "prost-build"
version = "0.13.3"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c1318b19085f08681016926435853bbf7858f9c082d0999b80550ff5d9abe15"
checksum = "d0f3e5beed80eb580c68e2c600937ac2c4eedabdfd5ef1e5b7ea4f3fba84497b"
dependencies = [
"bytes",
"heck",
"itertools",
"log",
@@ -1270,28 +1240,28 @@ dependencies = [
"prost",
"prost-types",
"regex",
"syn 2.0.79",
"syn 2.0.90",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.13.3"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5"
checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
name = "prost-reflect"
version = "0.14.2"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b7535b02f0e5efe3e1dbfcb428be152226ed0c66cad9541f2274c8ba8d4cd40"
checksum = "20ae544fca2892fd4b7e9ff26cba1090cedf1d4d95c2aded1af15d2f93f270b8"
dependencies = [
"logos",
"miette",
@@ -1302,9 +1272,9 @@ dependencies = [
[[package]]
name = "prost-types"
version = "0.13.3"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4759aa0d3a6232fb8dbdb97b61de2c20047c68aca932c7ed76da9d788508d670"
checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc"
dependencies = [
"prost",
]
@@ -1321,7 +1291,7 @@ dependencies = [
"prost-reflect",
"prost-types",
"protox-parse",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@@ -1333,7 +1303,7 @@ dependencies = [
"logos",
"miette",
"prost-types",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@@ -1397,9 +1367,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@@ -1409,9 +1379,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
@@ -1441,9 +1411,9 @@ dependencies = [
[[package]]
name = "rsa"
version = "0.9.6"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
dependencies = [
"const-oid",
"digest",
@@ -1472,9 +1442,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.37"
version = "0.38.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811"
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
dependencies = [
"bitflags",
"errno",
@@ -1513,22 +1483,22 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
@@ -1652,9 +1622,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.79"
version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [
"proc-macro2",
"quote",
@@ -1663,9 +1633,9 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.13.0"
version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b"
checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
dependencies = [
"cfg-if",
"fastrand",
@@ -1676,22 +1646,42 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.64"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47"
dependencies = [
"thiserror-impl 2.0.6",
]
[[package]]
name = "thiserror-impl"
version = "1.0.64"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
name = "thiserror-impl"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.90",
]
[[package]]
@@ -1704,15 +1694,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tls_codec"
version = "0.4.1"
@@ -1731,7 +1712,7 @@ checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
@@ -1764,9 +1745,9 @@ checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
[[package]]
name = "tracing"
version = "0.1.40"
version = "0.1.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
dependencies = [
"pin-project-lite",
"tracing-attributes",
@@ -1775,20 +1756,20 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.27"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
name = "tracing-core"
version = "0.1.32"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
dependencies = [
"once_cell",
"valuable",
@@ -1807,9 +1788,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"sharded-slab",
@@ -1837,9 +1818,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "unicode-width"
@@ -1861,9 +1842,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
]
@@ -2001,12 +1982,12 @@ dependencies = [
[[package]]
name = "winx"
version = "0.36.3"
version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2025,7 +2006,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "3.8.0"
version = "3.10.0"
dependencies = [
"anyhow",
"clap",
@@ -2045,11 +2026,11 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.5"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6129d25825e874589a0e529175dd060c13dab4f3d960c6a0b711e5535b598bb2"
checksum = "67914ab451f3bfd2e69e5e9d2ef3858484e7074d63f204fd166ec391b54de21d"
dependencies = [
"zerocopy-derive 0.8.5",
"zerocopy-derive 0.8.13",
]
[[package]]
@@ -2060,18 +2041,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.5"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d917df3784b4e2f5deb708d14623b2c02833890e1aa7a5dd1088998e8e9402b1"
checksum = "7988d73a4303ca289df03316bc490e934accf371af6bc745393cf3c2c5c4f25d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
@@ -2091,7 +2072,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.79",
"syn 2.0.90",
]
[[package]]
+9 -1
View File
@@ -4,7 +4,15 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.8.0"
version = "3.10.0"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
[workspace.lints.clippy]
cast_lossless = "deny"
missing_fields_in_debug = "warn"
redundant_clone = "deny"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+57
View File
@@ -385,6 +385,12 @@ Note that avbroot will validate that the prepatched image is compatible with the
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
### Skipping recovery OTA certificate patches
avbroot can skip modifying `otacerts.zip` in the recovery image with the `--skip-recovery-ota-cert` option. **Do not do this unless you have a good reason to do so.** (For example, if you've already manually inserted the OTA certificate into a boot image specified with `--prepatched` or `--replace`.) When this option is used with `--rootless` (and `--dsu` is not specified), then no modifications are performed on any boot image besides ensuring they are properly signed.
When manually adding the OTA certificate to a boot image, [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done.
### Replacing partitions
avbroot supports replacing entire partitions in the OTA, even partitions that are not boot images (eg. `vendor_dlkm`). A partition can be replaced by passing in `--replace <partition name> /path/to/partition.img`.
@@ -484,6 +490,57 @@ By default, this behavior is compatible with the `--signing_helper` option in AO
Note that avbroot will verify the signature returned by helper program against the public key. This ensures that the patching process will fail appropriately if the wrong private key was used.
### 16K page size developer option
On recent devices running Android 16 and newer, there may be an option in Android's developer options to switch to a 16K page size kernel. This will not work when running an avbroot-patched OS. The switch internally works by flashing incremental OTAs:
* `/vendor/boot_otas/boot_ota_16k.zip` to switch to the 16K page size kernel (requires the `boot` partition to be currently flashed with the 4K kernel)
* `/vendor/boot_otas/boot_ota_4k.zip` to switch to the 4K page size kernel (requires the `boot` partition to be currently flashed with the 16K kernel)
These `boot_otas` are unflashable when running an avbroot-patched OS because the `payload.bin` inside of them are signed by the OEM's key. These are also not proper OTA files. They don't contain any OTA metadata and the zip file itself is not signed. It's nothing more than a plain old zip file that stores a signed `payload.bin`.
There are no plans to add support for patching these `boot_otas`. It requires support for modifying filesystems and handling incremental OTAs, both of which are very non-trivial.
Folks who are determined to make this work anyway can try these manual steps to sign these `boot_otas` with your own key. Since the incremental OTAs are not being regenerated, the `boot` partition must be left unmodified when running `avbroot ota patch`.
1. Unpack `vendor.img` with avbroot and [afsr](https://github.com/chenxiaolong/afsr).
```bash
avbroot avb unpack -i vendor.img
afsr unpack -i raw.img
```
2. Extract `payload.bin` from `boot_otas/boot_ota_16k.zip`.
3. Re-sign `payload.bin` with your OTA key.
```bash
avbroot payload repack \
-i payload.bin.orig \
-o payload.bin \
-k ota.key \
--output-properties payload_properties.txt
```
4. Create a new zip of `payload.bin` and `payload_properties.txt`. The files must be stored uncompressed (eg. with `zip -0`).
5. Repeat the procedure for `boot_otas/boot_ota_4k.zip`.
6. Repack `vendor.img` and sign it with your AVB key.
```bash
afsr pack -o raw.img
avbroot avb pack -o vendor.img -k avb.key --recompute-size
```
7. Patch the (normal) OTA with:
```bash
avbroot ota patch \
--replace vendor <modified vendor> \
<normal arguments...>
```
## Building from source
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
+7 -1
View File
@@ -377,10 +377,16 @@ avbroot может подменить используемый загрузоч
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
### Пропуск патчей для root-доступа
### Пропуск патча для root-доступа
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
### Пропуск патчинга сертификата OTA в разделе Recovery
avbroot может пропустить изменение файла `otacerts.zip` в разделе Recovery с помощью опции `--skip-recovery-ota-cert`. **Не используйте эту функцию, если на то нет веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный образ (`boot.img`) и передаете его программе через опции `--prepatched` или `--replace`.) Если эта опция применяется совместно с `--rootless` (и без указания параметра `--dsu`), то в загрузочный образ не будут внесены никакие изменения, кроме обеспечения его корректной подписи.
Если вы вручную добавили сертификат OTA в загрузочный образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
### Подмена разделов
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
+5 -6
View File
@@ -13,7 +13,6 @@ anyhow = "1.0.75"
base64 = "0.22.1"
bitflags = { version = "2.4.1", features = ["serde"] }
bstr = "1.6.2"
byteorder = "1.4.3"
cap-std = "3.0.0"
cap-tempfile = "3.0.0"
clap = { version = "4.4.1", features = ["derive"] }
@@ -22,7 +21,7 @@ cms = { version = "0.2.2", features = ["std"] }
const-oid = "0.9.5"
crc32fast = "1.4.2"
ctrlc = "3.4.0"
dlv-list = "0.5.2"
dlv-list = "0.6.0"
flate2 = "1.0.27"
gf256 = { version = "0.3.0", features = ["rs"] }
hex = { version = "0.4.3", features = ["serde"] }
@@ -49,13 +48,13 @@ serde = { version = "1.0.188", features = ["derive"] }
sha1 = "0.10.5"
sha2 = "0.10.7"
tempfile = "3.8.0"
thiserror = "1.0.47"
thiserror = "2.0.3"
toml_edit = { version = "0.22.9", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
x509-cert = { version = "0.2.4", features = ["builder"] }
zerocopy = "0.8.5"
zerocopy = { version = "0.8.10", features = ["std"] }
zerocopy-derive = "0.8.5"
# There are multiple upstream bugs that cause infinite loops in the Drop
@@ -87,5 +86,5 @@ assert_matches = "1.5.0"
[features]
static = ["bzip2/static", "liblzma/static"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
[lints]
workspace = true
+8
View File
@@ -316,6 +316,10 @@ message PartitionUpdate {
// as a hint. If set to 0, libsnapshot should use alternative
// methods for estimating size.
optional uint64 estimate_cow_size = 19;
// Information about the cow used by Cow Writer to specify
// number of cow operations to be written
optional uint64 estimate_op_count_max = 20;
}
message DynamicPartitionGroup {
@@ -368,6 +372,10 @@ message DynamicPartitionMetadata {
// A collection of knobs to tune Virtual AB Compression
optional VABCFeatureSet vabc_feature_set = 6;
// Max bytes to be compressed at once during ota. Options: 4k, 8k, 16k, 32k,
// 64k, 128k
optional uint64 compression_factor = 7;
}
// Definition has been duplicated from
+12 -15
View File
@@ -340,7 +340,13 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
}
let originally_signed = !info.header.signature.is_empty();
let mut sign_action = if originally_signed && &info.header != orig_header {
let sign_action = if key_group.force {
if key_group.key.is_some() {
SignAction::Sign
} else {
SignAction::Clear
}
} else if originally_signed && &info.header != orig_header {
SignAction::Sign
} else {
// If the original image was signed, we can preserve the existing
@@ -349,14 +355,6 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
SignAction::None
};
if key_group.force {
sign_action = if key_group.key.is_some() {
SignAction::Sign
} else {
SignAction::Clear
};
}
match sign_action {
SignAction::None => {
if originally_signed {
@@ -398,7 +396,9 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
RsaSigningKey::Internal(private_key)
};
info.header.set_algo_for_key(&signing_key)?;
info.header
.set_algo_for_key(&signing_key)
.context("Failed to set signature algorithm")?;
info.header
.sign(&signing_key)
.context("Failed to sign new AVB header")?;
@@ -519,10 +519,7 @@ fn verify_and_repair(
cancel_signal: &AtomicBool,
) -> Result<()> {
let _span = debug_span!("image", name = name.unwrap_or_default()).entered();
let suffix = match name {
Some(n) => format!(" for: {n}"),
None => String::new(),
};
let suffix = name.map_or_else(String::new, |n| format!(" for: {n}"));
match descriptor {
AppendedDescriptorRef::HashTree(d) => {
@@ -536,7 +533,7 @@ fn verify_and_repair(
d.repair(&file, &file, cancel_signal)
.with_context(|| format!("Failed to repair data{suffix}"))?;
d.verify(&file, cancel_signal).map(|_| {
d.verify(&file, cancel_signal).inspect(|()| {
info!("Successfully repaired data{suffix}");
})
}
+10 -13
View File
@@ -129,22 +129,19 @@ fn split_extents(extents: &[Extent]) -> Vec<CopyExtent> {
/// Use the CLI-specified slot or automatically select one if all slots are
/// identical.
fn get_slot_number(metadata: &Metadata, cli_slot: Option<u32>) -> Result<usize> {
match cli_slot {
Some(n) => {
let n = n as usize;
if n >= metadata.slots.len() {
bail!("Slot out of range: {n}");
}
Ok(n)
if let Some(n) = cli_slot {
let n = n as usize;
if n >= metadata.slots.len() {
bail!("Slot out of range: {n}");
}
None => {
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
bail!("A slot must be specified because they are not all identical");
}
Ok(0)
Ok(n)
} else {
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
bail!("A slot must be specified because they are not all identical");
}
Ok(0)
}
}
+42 -16
View File
@@ -8,7 +8,6 @@ use std::{
fmt::Display,
fs::{self, File},
io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
mem,
ops::Range,
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Mutex},
@@ -193,7 +192,7 @@ fn open_input_files(
fn patch_boot_images<'a, 'b: 'a>(
required_images: &'b RequiredImages,
input_files: &mut HashMap<String, InputFile>,
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
key_avb: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<()> {
@@ -201,7 +200,7 @@ fn patch_boot_images<'a, 'b: 'a>(
let boot_partitions = required_images.iter_boot().collect::<Vec<_>>();
info!(
"Patching boot images: {}",
"Candidate boot images: {}",
joined(sorted(boot_partitions.iter())),
);
@@ -219,7 +218,7 @@ fn patch_boot_images<'a, 'b: 'a>(
WriteSeekReopen::reopen_boxed(&input_file.file)
},
key_avb,
&boot_patchers,
boot_patchers,
cancel_signal,
)
.with_context(|| {
@@ -348,7 +347,7 @@ fn ensure_partitions_protected(
/// determine the order to patch the vbmeta images so that it can be done in a
/// single pass.
fn get_vbmeta_patch_order(
images: &mut HashMap<String, InputFile>,
images: &HashMap<String, InputFile>,
vbmeta_headers: &HashMap<String, Header>,
) -> Result<Vec<(String, HashSet<String>)>> {
let mut dep_graph = HashMap::<&str, HashSet<String>>::new();
@@ -593,7 +592,9 @@ fn update_vbmeta_headers(
// have no dependencies and are only being processed to ensure that the
// flags are set to a sane value.
if parent_header != &orig_parent_header {
parent_header.set_algo_for_key(key)?;
parent_header
.set_algo_for_key(key)
.with_context(|| format!("Failed to set signature algorithm: {name}"))?;
parent_header
.sign(key)
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
@@ -720,7 +721,7 @@ fn patch_ota_payload(
payload: &(dyn ReadSeekReopen + Sync),
writer: impl Write,
external_images: &HashMap<String, PathBuf>,
boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
clear_vbmeta_flags: bool,
key_avb: &RsaSigningKey,
key_ota: &RsaSigningKey,
@@ -792,7 +793,7 @@ fn patch_ota_payload(
ensure_partitions_protected(&required_images, &vbmeta_headers)?;
let mut vbmeta_order = get_vbmeta_patch_order(&mut input_files, &vbmeta_headers)?;
let mut vbmeta_order = get_vbmeta_patch_order(&input_files, &vbmeta_headers)?;
info!(
"Patching vbmeta images: {}",
@@ -912,7 +913,7 @@ fn patch_ota_zip(
zip_reader: &mut ZipArchive<impl Read + Seek>,
mut zip_writer: &mut ZipWriter<impl Write>,
external_images: &HashMap<String, PathBuf>,
mut boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
clear_vbmeta_flags: bool,
zip_mode: ZipMode,
key_avb: &RsaSigningKey,
@@ -1034,8 +1035,7 @@ fn patch_ota_zip(
&payload_reader,
&mut writer,
external_images,
// There's only one payload in the OTA.
mem::take(&mut boot_patchers),
boot_patchers,
clear_vbmeta_flags,
key_avb,
key_ota,
@@ -1304,7 +1304,11 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
assert!(cli.root.rootless);
};
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
if cli.skip_recovery_ota_cert {
warn!("Not inserting OTA cert into recovery image; sideloading further updates may fail");
} else {
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
}
if cli.dsu {
boot_patchers.push(Box::new(DsuPubKeyPatcher::new(key_avb.to_public_key())));
@@ -1341,7 +1345,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
&mut zip_reader,
&mut zip_writer,
&external_images,
boot_patchers,
&boot_patchers,
cli.clear_vbmeta_flags,
cli.zip_mode,
&key_avb,
@@ -1627,7 +1631,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
);
} else if let Some(p) = &cli.cert_ota {
let verify_cert = crypto::read_pem_cert_file(p)
.with_context(|| format!("Failed to load certificate: {:?}", p))?;
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
if embedded_cert != verify_cert {
bail!("OTA has a valid signature, but was not signed with: {p:?}");
@@ -1684,9 +1688,11 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
verify_partition_hashes(&temp_dir, &header, &unique_images, cancel_signal)?;
info!("Checking ramdisk's otacerts.zip");
if cli.skip_recovery_ota_cert {
warn!("Not verifying recovery ramdisk's otacerts.zip");
} else {
info!("Checking recovery ramdisk's otacerts.zip");
{
let required_images = RequiredImages::new(&header.manifest);
let boot_images =
boot::load_boot_images(&required_images.iter_boot().collect::<Vec<_>>(), |name| {
@@ -1922,6 +1928,17 @@ pub struct PatchCli {
)]
pub ignore_prepatched_compat: u8,
/// Skip adding OTA certificate to recovery image.
///
/// DO NOT USE THIS unless you've manually added the certificate to the
/// recovery image already. Otherwise, sideloading further updates will not
/// be possible.
///
/// When this option is used with --rootless, the boot images in the OTA
/// will not be modified.
#[arg(long, help_heading = HEADING_OTHER)]
pub skip_recovery_ota_cert: bool,
/// Add AVB public key to trusted keys for DSU.
#[arg(long, help_heading = HEADING_OTHER)]
pub dsu: bool,
@@ -2008,6 +2025,15 @@ pub struct VerifyCli {
/// valid, not that they are trusted.
#[arg(long, value_name = "FILE", value_parser)]
pub public_key_avb: Option<PathBuf>,
/// Skip verifying OTA certificate in recovery image.
///
/// This should not be used unless the OTA uses a special boot image format
/// that avbroot cannot parse. This certificate check ensures that the OTA
/// is configured properly to allow sideloading further OTAs signed by the
/// same key.
#[arg(long, help_heading = HEADING_OTHER)]
pub skip_recovery_ota_cert: bool,
}
#[allow(clippy::large_enum_variant)]
+5 -5
View File
@@ -28,7 +28,7 @@ use crate::{
struct CompactView<'a, T>(&'a [T]);
impl<'a, T: fmt::Debug> fmt::Debug for CompactView<'a, T> {
impl<T: fmt::Debug> fmt::Debug for CompactView<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
@@ -110,7 +110,7 @@ fn split_chunks(chunks: &[Chunk], block_size: u32) -> Vec<Chunk> {
#[cfg(any(target_os = "linux", target_os = "android"))]
fn find_allocated_regions(
path: &Path,
reader: &mut File,
reader: &File,
cancel_signal: &AtomicBool,
) -> Result<Vec<Range<u64>>> {
use rustix::{fs::SeekFrom, io::Errno};
@@ -122,13 +122,13 @@ fn find_allocated_regions(
loop {
stream::check_cancel(cancel_signal)?;
start = match rustix::fs::seek(&*reader, SeekFrom::Data(end as i64)) {
start = match rustix::fs::seek(reader, SeekFrom::Data(end as i64)) {
Ok(offset) => offset,
Err(e) if e == Errno::NXIO => break,
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
};
end = rustix::fs::seek(&*reader, SeekFrom::Hole(start as i64))
end = rustix::fs::seek(reader, SeekFrom::Hole(start as i64))
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
result.push(start..end);
@@ -375,7 +375,7 @@ fn pack_subcommand(
} else {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let regions = find_allocated_regions(&cli.input, &mut reader, cancel_signal)?;
let regions = find_allocated_regions(&cli.input, &reader, cancel_signal)?;
(regions, false)
}
+3 -3
View File
@@ -176,7 +176,7 @@ impl PassphraseSource {
}
Self::EnvVar(v) => env::var(v).map_err(|e| Error::InvalidEnvVar(v.clone(), e))?,
Self::File(p) => fs::read_to_string(p)?
.trim_end_matches(&['\r', '\n'])
.trim_end_matches(['\r', '\n'])
.to_owned(),
};
@@ -235,8 +235,8 @@ impl RsaSigningKey {
/// Get the public key portion of the signing key.
pub fn to_public_key(&self) -> RsaPublicKey {
match self {
RsaSigningKey::Internal(key) => key.to_public_key(),
RsaSigningKey::External { public_key, .. } => public_key.clone(),
Self::Internal(key) => key.to_public_key(),
Self::External { public_key, .. } => public_key.clone(),
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ where
{
struct EscapedStrVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for EscapedStrVisitor<T>
impl<T> Visitor<'_> for EscapedStrVisitor<T>
where
T: FromEscaped,
<T as FromEscaped>::Error: fmt::Display,
+540 -414
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,9 +1,8 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Read, Seek, Write};
use byteorder::{LittleEndian, WriteBytesExt};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use liblzma::{
read::XzDecoder,
@@ -59,7 +58,7 @@ impl<W: Write> Lz4LegacyEncoder<W> {
let compressed = lz4_flex::block::compress(&self.buf[..self.n_filled]);
let writer = self.writer.as_mut().unwrap();
writer.write_u32::<LittleEndian>(compressed.len() as u32)?;
writer.write_all(&(compressed.len() as u32).to_le_bytes())?;
writer.write_all(&compressed)?;
self.n_filled = 0;
+8 -11
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
@@ -50,12 +50,12 @@ pub enum Error {
UnknownMagic([u8; 6]),
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
HardLinksNotSupported(Vec<u8>),
#[error("Entry of type {0} should not have data: {:?}", .1.as_bstr())]
#[error("Entry of type {0} should not have data: {path:?}", path = .1.as_bstr())]
EntryHasData(CpioEntryType, Vec<u8>),
#[error("No inodes available for device {0:x},{1:x}")]
DeviceFull(u32, u32),
#[error("{0:?} field exceeds integer bounds")]
IntegerTooLarge(&'static str),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
}
@@ -211,17 +211,14 @@ impl CpioEntryData {
pub fn size(&self) -> Result<u32> {
let size = match self {
Self::Size(s) => *s,
Self::Data(d) => d
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("data_size"))?,
Self::Data(d) => d.len().to_u32().ok_or(Error::IntOverflow("data_size"))?,
};
Ok(size)
}
fn is_size(&self) -> bool {
matches!(self, CpioEntryData::Size(_))
matches!(self, Self::Size(_))
}
}
@@ -475,7 +472,7 @@ impl<W: Write> ToWriter<W> for CpioEntry {
.len()
.checked_add(1)
.and_then(|s| s.to_u32())
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
.ok_or(Error::IntOverflow("path_size"))?;
let file_size = self.data.size()?;
if file_size != 0
@@ -683,7 +680,7 @@ pub fn load(
stream::check_cancel(cancel_signal)?;
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
return Err(Error::HardLinksNotSupported(entry.path.clone()));
return Err(Error::HardLinksNotSupported(entry.path));
}
if let CpioEntryData::Size(s) = entry.data {
+84 -70
View File
@@ -1,31 +1,32 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::HashSet,
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
io::{self, Read, Seek, SeekFrom, Write},
mem,
ops::Range,
sync::atomic::AtomicBool,
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::ToPrimitive;
use rayon::{
prelude::{IndexedParallelIterator, ParallelIterator},
slice::{ParallelSlice, ParallelSliceMut},
};
use thiserror::Error;
use zerocopy::{little_endian, FromBytes, IntoBytes};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
format::verityrs,
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
util::{self, NumBytes},
util::{self, NumBytes, OutOfBoundsError},
};
// Not to be confused with the 255-byte RS block size.
const FEC_BLOCK_SIZE: usize = 4096;
const FEC_HEADER_SIZE: usize = 60;
const FEC_MAGIC: u32 = 0xFECFECFE;
const FEC_VERSION: u32 = 0;
@@ -64,7 +65,9 @@ pub enum Error {
#[error("Expected FEC digest {expected}, but have {actual}")]
InvalidFecDigest { expected: String, actual: String },
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
}
@@ -94,7 +97,7 @@ impl Codeword {
&mut self.data[..usize::from(self.rs_k)]
}
fn parity(&mut self) -> &[u8] {
fn parity(&self) -> &[u8] {
&self.data[usize::from(self.rs_k)..]
}
@@ -155,10 +158,11 @@ impl Fec {
input: file_size,
block: block_size,
});
} else if block_size > FEC_MAX_BLOCK_SIZE {
return Err(Error::FieldOutOfBounds("block_size"));
}
util::check_bounds(block_size, ..=FEC_MAX_BLOCK_SIZE)
.map_err(|e| Error::IntOutOfBounds("block_size", e))?;
let rs_k = 255 - parity;
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
return Err(Error::UnsupportedParity(parity));
@@ -172,11 +176,11 @@ impl Fec {
.checked_mul(u64::from(parity))
.and_then(|s| s.checked_mul(u64::from(block_size)))
.and_then(|s| s.to_usize())
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_size"))?;
.ok_or(Error::IntOverflow("fec_data_size"))?;
rounds
.checked_mul(u64::from(rs_k))
.and_then(|s| s.checked_mul(u64::from(block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("fec_grid_size"))?;
.ok_or(Error::IntOverflow("fec_grid_size"))?;
Ok(Self {
file_size,
@@ -210,9 +214,8 @@ impl Fec {
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
let ranges = util::merge_overlapping(ranges);
if let Some(last) = ranges.last() {
if last.end > self.file_size {
return Err(Error::FieldOutOfBounds("ranges"));
}
util::check_bounds(last.end, ..=self.file_size)
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
}
let block_size = u64::from(self.block_size);
@@ -574,6 +577,26 @@ impl Fec {
}
}
/// Raw on-disk layout for the FEC image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
struct RawHeader {
/// Magic value. This should be equal to [`FEC_MAGIC`].
magic: little_endian::U32,
/// Image version. This should be equal to [`FEC_VERSION`].
version: little_endian::U32,
/// Size of this [`RawHeader`].
header_size: little_endian::U32,
/// Number of parity bytes per 255-byte Reed-Solomon codeword.
parity: little_endian::U32,
/// Size of the FEC data.
fec_size: little_endian::U32,
/// Size of the actual data.
data_size: little_endian::U64,
/// SHA-256 digest of the FEC data.
digest: [u8; 32],
}
/// A type for reading and writing AOSP's standalone FEC image format.
///
/// The FEC data parser in this implementation is strict. All header fields,
@@ -667,26 +690,23 @@ impl FecImage {
/// Build one instance of the FEC header. The caller is responsible for
/// writing it to both of the header locations at the end of the file.
fn build_header(&self) -> Result<[u8; FEC_HEADER_SIZE]> {
let fec_size = self
.fec
.len()
.to_u32()
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
let mut writer = Cursor::new([0u8; FEC_HEADER_SIZE]);
fn build_header(&self) -> Result<RawHeader> {
let fec_size: u32 =
util::try_cast(self.fec.len()).map_err(|e| Error::IntOutOfBounds("fec_size", e))?;
let digest = ring::digest::digest(&ring::digest::SHA256, &self.fec);
writer.write_u32::<LittleEndian>(FEC_MAGIC)?;
writer.write_u32::<LittleEndian>(FEC_VERSION)?;
writer.write_u32::<LittleEndian>(FEC_HEADER_SIZE as u32)?;
writer.write_u32::<LittleEndian>(self.parity.into())?;
writer.write_u32::<LittleEndian>(fec_size)?;
writer.write_u64::<LittleEndian>(self.data_size)?;
writer.write_all(digest.as_ref())?;
let header = RawHeader {
magic: FEC_MAGIC.into(),
version: FEC_VERSION.into(),
header_size: (mem::size_of::<RawHeader>() as u32).into(),
parity: u32::from(self.parity).into(),
fec_size: fec_size.into(),
data_size: self.data_size.into(),
digest: digest.as_ref().try_into().unwrap(),
};
Ok(writer.into_inner())
Ok(header)
}
}
@@ -703,42 +723,39 @@ impl<R: Read> FromReader<R> for FecImage {
return Err(Error::DataTooSmall);
}
// Make sure both headers match.
let header1_offset = fec.len() - FEC_BLOCK_SIZE;
let header2_offset = fec.len() - FEC_HEADER_SIZE;
let header1_raw = &fec[header1_offset..header1_offset + FEC_HEADER_SIZE];
let header2_raw = &fec[header2_offset..header2_offset + FEC_HEADER_SIZE];
let (header, _) =
RawHeader::ref_from_prefix(&fec[header1_offset..]).map_err(|_| Error::DataTooSmall)?;
let header_size = header.header_size.get() as usize;
if header_size > FEC_BLOCK_SIZE / 2 {
// ref_from_prefix() already handles the "too small" case.
return Err(Error::InvalidHeaderSize(header.header_size.get()));
}
let header2_offset = fec.len() - header_size;
// Make sure both headers match, accounting for potential custom fields.
let header1_raw = &fec[header1_offset..][..header_size];
let header2_raw = &fec[header2_offset..][..header_size];
if header1_raw != header2_raw {
return Err(Error::HeadersDifferent);
}
let mut header_reader = Cursor::new(header1_raw);
let magic = header_reader.read_u32::<LittleEndian>()?;
if magic != FEC_MAGIC {
return Err(Error::InvalidHeaderMagic(magic));
if header.magic != FEC_MAGIC {
return Err(Error::InvalidHeaderMagic(header.magic.get()));
}
let version = header_reader.read_u32::<LittleEndian>()?;
if version != FEC_VERSION {
return Err(Error::UnsupportedHeaderVersion(version));
if header.version != FEC_VERSION {
return Err(Error::UnsupportedHeaderVersion(header.version.get()));
}
let header_size = header_reader.read_u32::<LittleEndian>()?;
if header_size != FEC_HEADER_SIZE as u32 {
return Err(Error::InvalidHeaderSize(header_size));
}
let parity: u8 =
util::try_cast(header.parity.get()).map_err(|e| Error::IntOutOfBounds("parity", e))?;
let parity = header_reader
.read_u32::<LittleEndian>()?
.to_u8()
.ok_or_else(|| Error::FieldOutOfBounds("parity"))?;
let fec_size = header_reader
.read_u32::<LittleEndian>()?
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
let fec_size = header.fec_size.get() as usize;
let actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
if fec_size != actual_fec_size {
return Err(Error::InvalidHeaderFecSize {
@@ -747,25 +764,22 @@ impl<R: Read> FromReader<R> for FecImage {
});
}
let input_size = header_reader.read_u64::<LittleEndian>()?;
let data_size = header.data_size.get();
let mut digest = [0u8; 32];
header_reader.read_exact(&mut digest)?;
// Chop off headers.
fec.resize(fec_size, 0);
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec);
if digest != actual_digest.as_ref() {
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec[..fec_size]);
if header.digest != actual_digest.as_ref() {
return Err(Error::InvalidFecDigest {
expected: hex::encode(digest),
expected: hex::encode(header.digest),
actual: hex::encode(actual_digest),
});
}
// Chop off headers.
fec.resize(fec_size, 0);
Ok(Self {
fec,
data_size: input_size,
data_size,
parity,
})
}
@@ -778,9 +792,9 @@ impl<W: Write> ToWriter<W> for FecImage {
let header = self.build_header()?;
writer.write_all(&self.fec)?;
writer.write_all(&header)?;
writer.write_zeros_exact((FEC_BLOCK_SIZE - 2 * FEC_HEADER_SIZE) as u64)?;
writer.write_all(&header)?;
header.write_to_io(&mut writer)?;
writer.write_zeros_exact((FEC_BLOCK_SIZE - 2 * header.as_bytes().len()) as u64)?;
header.write_to_io(&mut writer)?;
Ok(())
}
@@ -789,7 +803,7 @@ impl<W: Write> ToWriter<W> for FecImage {
#[cfg(test)]
mod tests {
use std::{
io::Seek,
io::{Cursor, Seek},
sync::{atomic::AtomicBool, Arc},
};
+88 -67
View File
@@ -1,27 +1,31 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
io::{self, Cursor, Read, SeekFrom, Write},
ops::Range,
str,
sync::atomic::AtomicBool,
};
use bstr::ByteSlice;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::ToPrimitive;
use rayon::{
iter::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
};
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use zerocopy::{little_endian, FromBytes, IntoBytes};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
format::{avb, padding},
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
util::{self, NumBytes},
format::{
avb,
padding::{self, ZeroPadding},
},
stream::{self, FromReader, ReadSeekReopen, ToWriter},
util::{self, NumBytes, OutOfBoundsError},
};
#[derive(Debug, Error)]
@@ -40,10 +44,12 @@ pub enum Error {
InvalidHeaderMagic([u8; 16]),
#[error("Invalid hash tree header version: {0}")]
InvalidHeaderVersion(u16),
#[error("Hashing algorithm not supported: {0:?}")]
UnsupportedHashAlgorithm(String),
#[error("Hashing algorithm not supported: {:?}", .0.as_bstr())]
UnsupportedHashAlgorithm(Vec<u8>),
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
}
@@ -81,13 +87,12 @@ impl HashTree {
level_size = blocks
.checked_mul(digest_size as u64)
.and_then(|s| padding::round(s, u64::from(self.block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
.ok_or(Error::IntOverflow("level_size"))?;
// Depending on the chosen block size, the original file size could
// overflow a usize without the first level's size doing the same.
let level_size_usize = level_size
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
let level_size_usize: usize =
util::try_cast(level_size).map_err(|e| Error::IntOutOfBounds("level_size", e))?;
ranges.push(0..level_size_usize);
}
@@ -109,9 +114,8 @@ impl HashTree {
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
let ranges = util::merge_overlapping(ranges);
if let Some(last) = ranges.last() {
if last.end > image_size {
return Err(Error::FieldOutOfBounds("ranges"));
}
util::check_bounds(last.end, ..=image_size)
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
}
let block_size = u64::from(self.block_size);
@@ -185,7 +189,7 @@ impl HashTree {
cancel_signal: &AtomicBool,
) -> io::Result<()> {
assert!(
size > self.block_size as u64,
size > u64::from(self.block_size),
"Images smaller than block size must use a normal hash",
);
@@ -325,7 +329,7 @@ impl HashTree {
cancel_signal: &AtomicBool,
) -> Result<(Vec<u8>, Vec<u8>)> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
let mut hash_tree_data = vec![0u8; hash_tree_size];
let root_digest = self.calculate(
@@ -351,7 +355,7 @@ impl HashTree {
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
if hash_tree_data.len() != hash_tree_size {
return Err(Error::InvalidHashTreeSize {
input: image_size,
@@ -380,7 +384,7 @@ impl HashTree {
cancel_signal: &AtomicBool,
) -> Result<()> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
if hash_tree_data.len() != hash_tree_size {
return Err(Error::InvalidHashTreeSize {
input: image_size,
@@ -415,6 +419,28 @@ impl HashTree {
}
}
/// Raw on-disk layout for our custom hash tree image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
struct RawHeader {
/// Magic value. This should be equal to [`HashTreeImage::MAGIC`].
magic: [u8; 16],
/// Image version. This should be equal to [`HashTreeImage::VERSION`].
version: little_endian::U16,
/// Size of the actual data.
image_size: little_endian::U64,
/// Block size.
block_size: little_endian::U32,
/// Hash algorithm.
algorithm: [u8; 16],
/// Salt size.
salt_size: little_endian::U16,
/// Root digest size.
root_digest_size: little_endian::U16,
/// Hash tree size.
hash_tree_size: little_endian::U32,
}
/// A type for reading and writing a custom hash tree image format.
///
/// File format:
@@ -442,6 +468,7 @@ pub struct HashTreeImage {
impl fmt::Debug for HashTreeImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HashTreeImage")
.field("image_size", &self.image_size)
.field("block_size", &self.block_size)
.field("algorithm", &self.algorithm)
.field("salt", &hex::encode(&self.salt))
@@ -457,7 +484,7 @@ impl HashTreeImage {
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
avb::ring_algorithm(name, false)
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
}
/// Generate hash tree data for a file.
@@ -530,40 +557,33 @@ impl<R: Read> FromReader<R> for HashTreeImage {
type Error = Error;
fn from_reader(mut reader: R) -> Result<Self> {
let mut magic = [0u8; 16];
reader.read_exact(&mut magic)?;
if magic != *Self::MAGIC {
return Err(Error::InvalidHeaderMagic(magic));
let header = RawHeader::read_from_io(&mut reader)?;
if header.magic != *Self::MAGIC {
return Err(Error::InvalidHeaderMagic(header.magic));
}
let version = reader.read_u16::<LittleEndian>()?;
if version != Self::VERSION {
return Err(Error::InvalidHeaderVersion(version));
if header.version != Self::VERSION {
return Err(Error::InvalidHeaderVersion(header.version.get()));
}
let image_size = reader.read_u64::<LittleEndian>()?;
let block_size = reader.read_u32::<LittleEndian>()?;
let algorithm = reader.read_string_padded(16)?;
let salt_size = reader.read_u16::<LittleEndian>()?;
let root_digest_size = reader.read_u16::<LittleEndian>()?;
let hash_tree_size = reader
.read_u32::<LittleEndian>()?
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
let algorithm = header.algorithm.trim_end_padding();
let algorithm = str::from_utf8(algorithm)
.map_err(|_| Error::UnsupportedHashAlgorithm(algorithm.to_vec()))?;
let mut salt = vec![0u8; usize::from(salt_size)];
let mut salt = vec![0u8; usize::from(header.salt_size)];
reader.read_exact(&mut salt)?;
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
let mut root_digest = vec![0u8; usize::from(header.root_digest_size)];
reader.read_exact(&mut root_digest)?;
let mut hash_tree = vec![0u8; hash_tree_size];
let mut hash_tree = vec![0u8; header.hash_tree_size.get() as usize];
reader.read_exact(&mut hash_tree)?;
Ok(Self {
image_size,
block_size,
algorithm,
image_size: header.image_size.get(),
block_size: header.block_size.get(),
algorithm: algorithm.to_owned(),
salt,
root_digest,
hash_tree,
@@ -575,30 +595,31 @@ impl<W: Write> ToWriter<W> for HashTreeImage {
type Error = Error;
fn to_writer(&self, mut writer: W) -> Result<()> {
let salt_size = self
.salt
.len()
.to_u16()
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
let root_digest_size = self
.root_digest
.len()
.to_u16()
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
let hash_tree_size = self
.hash_tree
.len()
.to_u32()
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
let algorithm = self
.algorithm
.as_bytes()
.to_padded_array::<16>()
.ok_or_else(|| Error::UnsupportedHashAlgorithm(self.algorithm.as_bytes().to_vec()))?;
writer.write_all(Self::MAGIC)?;
writer.write_u16::<LittleEndian>(Self::VERSION)?;
writer.write_u64::<LittleEndian>(self.image_size)?;
writer.write_u32::<LittleEndian>(self.block_size)?;
writer.write_string_padded(&self.algorithm, 16)?;
writer.write_u16::<LittleEndian>(salt_size)?;
writer.write_u16::<LittleEndian>(root_digest_size)?;
writer.write_u32::<LittleEndian>(hash_tree_size)?;
let salt_size: u16 =
util::try_cast(self.salt.len()).map_err(|e| Error::IntOutOfBounds("salt_size", e))?;
let root_digest_size: u16 = util::try_cast(self.root_digest.len())
.map_err(|e| Error::IntOutOfBounds("root_digest_size", e))?;
let hash_tree_size: u32 = util::try_cast(self.hash_tree.len())
.map_err(|e| Error::IntOutOfBounds("hash_tree_size", e))?;
let header = RawHeader {
magic: *Self::MAGIC,
version: Self::VERSION.into(),
image_size: self.image_size.into(),
block_size: self.block_size.into(),
algorithm,
salt_size: salt_size.into(),
root_digest_size: root_digest_size.into(),
hash_tree_size: hash_tree_size.into(),
};
header.write_to_io(&mut writer)?;
writer.write_all(&self.salt)?;
writer.write_all(&self.root_digest)?;
writer.write_all(&self.hash_tree)?;
@@ -646,7 +667,7 @@ mod tests {
);
assert_matches!(
hash_tree.blocks_for_ranges(16384, &[0..16385]),
Err(Error::FieldOutOfBounds(_))
Err(Error::IntOutOfBounds(_, _))
);
}
+265 -178
View File
@@ -20,7 +20,7 @@ use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
format::padding,
stream::{CountingReader, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt},
util::{self, is_zero},
util::{self, is_zero, DebugString},
};
/// Magic value for [`RawGeometry::magic`].
@@ -57,26 +57,134 @@ const METADATA_MAX_SIZE: u32 = 128 * 1024;
#[derive(Debug, Error)]
pub enum Error {
// Naming errors.
#[error("Invalid partition name: {0}")]
PartitionNameInvalid(String),
#[error("Geometry: {0}")]
Geometry(String),
#[error("Descriptor offset #{0}: {1}")]
Descriptor(u32, String),
#[error("Header: {0}")]
Header(String),
#[error("Partition: {0}: {1}")]
Partition(String, String),
#[error("Metadata extent #{0}: {1}")]
Extent(usize, String),
#[error("Partition group: {0}: {1}")]
PartitionGroup(String, String),
#[error("Block device: {0}: {1}")]
BlockDevice(String, String),
#[error("Metadata: {0}")]
Metadata(String),
// Geometry errors.
#[error("Invalid geometry magic: {0:#010x}")]
GeometryInvalidMagic(u32),
#[error("Invalid geometry size: {0} != {size}", size = mem::size_of::<RawGeometry>())]
GeometryInvalidSize(u32),
#[error("Expected geometry digest {expected}, but have {actual}")]
GeometryInvalidDigest { expected: String, actual: String },
#[error("Maximum metadata size is not sector-aligned: {0}")]
MaxMetadataSizeUnaligned(u32),
#[error("Maximum metadata size exceeds limit: {0} > {METADATA_MAX_SIZE}")]
MaxMetadataSizeTooLarge(u32),
#[error("No metadata slots defined")]
NoMetadataSlots,
#[error("Logical block size is not sector-aligned: {0}")]
LogicalBlockSizeUnaligned(u32),
// Descriptor errors.
#[error("Descriptor offset #{0}: Entry count too large")]
DescriptorEntryCountTooLarge(u32),
#[error("Descriptor offset #{0}: Next entry offset too large")]
DescriptorNextOffsetTooLarge(u32),
// Header errors.
#[error("Invalid header magic: {0:#010x}")]
HeaderInvalidMagic(u32),
#[error("Unsupported header version: {major}.{minor}")]
HeaderUnsupportedVersion { major: u16, minor: u16 },
#[error("Invalid header size: {0} != {size}", size = mem::size_of::<RawHeader>())]
HeaderInvalidSize(u32),
#[error("Expected header digest {expected}, but have {actual}")]
HeaderInvalidDigest { expected: String, actual: String },
#[error("Metadata slot exceeds maximum size: {metadata_size} > {max_size} - {header_size}")]
MetadataTooLarge {
metadata_size: u32,
max_size: u32,
header_size: u32,
},
#[error("Descriptors too large or have gaps")]
DescriptorsTooLargeOrHaveGaps,
#[error("Gap after last descriptor")]
DescriptorsFinalGap,
#[error("Invalid descriptor entry sizes")]
DescriptorsInvalidEntrySizes,
#[error("Descriptor entry count {entry_count} does not match {name} table length {table_len}")]
DescriptorMismatchedEntryCount {
name: &'static str,
entry_count: u32,
table_len: usize,
},
#[error("Expected tables digest {expected}, but have {actual}")]
HeaderInvalidTablesDigest { expected: String, actual: String },
// Partition errors.
#[error("Partition {name:?}: Invalid attributes: {}", .attributes.0)]
PartitionInvalidAttributes {
name: DebugString,
attributes: PartitionAttributes,
},
#[error("Partition {name:?}: Extent indices too large")]
PartitionExtentIndicesTooLarge { name: DebugString },
#[error("Partition {name:?}: Extent indices set on empty image")]
PartitionExtentIndicesEmptyImage { name: DebugString },
#[error("Partition {name:?}: Extent index too large")]
PartitionExtentIndexTooLarge { name: DebugString },
#[error("Partition {name:?}: Extent count too large")]
PartitionExtentCountTooLarge { name: DebugString },
#[error("Partition {name:?}: Invalid partition group index: {index}")]
PartitionInvalidGroupIndex { name: DebugString, index: u32 },
#[error("Partition {name:?}: Sector count too large")]
PartitionSectorCountTooLarge { name: DebugString },
#[error("Partition {name:?}: Byte count too large")]
PartitionByteCountTooLarge { name: DebugString },
// Extent errors.
#[error("Extent #{index}: Invalid block device index: {device_index}")]
ExtentInvalidDeviceIndex { index: usize, device_index: u32 },
#[error("Extent #{index}: End sector too large: {start} + {count}")]
ExtentEndSectorTooLarge {
index: usize,
start: u64,
count: u64,
},
#[error("Extent #{index}: {start} starts before block device's first sector {sector}")]
ExtentStartBeforeDeviceStart {
index: usize,
start: u64,
sector: u64,
},
#[error("Extent #{index}: {end} ends after block device's last sector {sector}")]
ExtentEndsAfterDeviceEnd { index: usize, end: u64, sector: u64 },
#[error("Extent #{index}: Type zero extents cannot have non-zero sector or device")]
ExtentTypeZeroNotEmpty { index: usize },
#[error("Extent #{index}: Invalid type: {extent_type}")]
ExtentInvalidType { index: usize, extent_type: u32 },
#[error("Extent #{index}: Overlaps previous extent")]
ExtentOverlapsPrevious { index: usize },
#[error("Extent #{index}: Earlier block device index than previous extent")]
ExtentDeviceNotConsecutive { index: usize },
#[error("Extent #{index}: Block device index too large")]
ExtentDeviceIndexTooLarge { index: usize },
// Partition group errors.
#[error("Group {name:?}: Total size of partitions too large")]
GroupTotalSizeTooLarge { name: DebugString },
#[error("Group {name:?}: Total partition size {size} exceeds limit {limit}")]
GroupTotalSizeExceedsLimit {
name: DebugString,
size: u64,
limit: u64,
},
#[error("Group {name:?}: Index too large")]
GroupIndexTooLarge { name: DebugString },
// Block device errors.
#[error("Device {name:?}: Alignment is 0")]
DeviceAlignmentIsZero { name: DebugString },
#[error("Device {name:?}: Partition alignment is not sector-aligned")]
DeviceAlignmentNotSectorAligned { name: DebugString },
#[error("Device {name:?}: First logical sector is not partition-aligned")]
DeviceFirstSectorNotAligned { name: DebugString },
#[error("Device {name:?}: Alignment offset is not sector-aligned")]
DeviceOffsetNotSectorAligned { name: DebugString },
#[error("Device {name:?}: Size is not sector-aligned")]
DeviceSizeNotSectorAligned { name: DebugString },
// Metadata errors.
#[error("Expected slot count {expected}, but have {actual}")]
MismatchedSlotCount { expected: usize, actual: usize },
// Allocator errors.
#[error("Insufficient space on block devices to allocate sectors")]
AllocatorDeviceFull,
// Wrapped errors.
#[error("I/O error")]
Io(#[from] io::Error),
}
@@ -180,17 +288,11 @@ impl RawGeometry {
/// further checks.
fn validate(&self) -> Result<()> {
if self.magic.get() != GEOMETRY_MAGIC {
return Err(Error::Geometry(format!(
"Invalid magic: {:#010x}",
self.magic.get(),
)));
return Err(Error::GeometryInvalidMagic(self.magic.get()));
}
if self.struct_size.get() != mem::size_of::<Self>() as u32 {
return Err(Error::Geometry(format!(
"Invalid struct size: {}",
self.struct_size.get(),
)));
return Err(Error::GeometryInvalidSize(self.struct_size.get()));
}
#[cfg(not(fuzzing))]
@@ -200,33 +302,27 @@ impl RawGeometry {
let digest = ring::digest::digest(&ring::digest::SHA256, copy.as_bytes());
if digest.as_ref() != self.checksum {
return Err(Error::Geometry(format!(
"Expected digest {}, but have {}",
hex::encode(self.checksum),
hex::encode(digest),
)));
return Err(Error::GeometryInvalidDigest {
expected: hex::encode(self.checksum),
actual: hex::encode(digest),
});
}
}
if self.metadata_max_size.get() == 0 || self.metadata_max_size.get() % SECTOR_SIZE != 0 {
return Err(Error::Geometry(format!(
"Maximum metadata size is not sector-aligned: {}",
return Err(Error::MaxMetadataSizeUnaligned(
self.metadata_max_size.get(),
)));
));
} else if self.metadata_max_size.get() > METADATA_MAX_SIZE {
return Err(Error::Geometry(format!(
"Maximum metadata size exceeds limit: {} > {METADATA_MAX_SIZE}",
self.metadata_max_size.get(),
)));
return Err(Error::MaxMetadataSizeTooLarge(self.metadata_max_size.get()));
} else if self.metadata_slot_count.get() == 0 {
return Err(Error::Geometry("No metadata slots defined".into()));
return Err(Error::NoMetadataSlots);
}
if self.logical_block_size.get() % SECTOR_SIZE != 0 {
return Err(Error::Geometry(format!(
"Logical block size is not sector-aligned: {}",
return Err(Error::LogicalBlockSizeUnaligned(
self.logical_block_size.get(),
)));
));
}
Ok(())
@@ -278,11 +374,11 @@ impl RawTableDescriptor {
let num_entries: u32 = items
.len()
.try_into()
.map_err(|_| Error::Descriptor(offset, "Entry count out of bounds".into()))?;
.map_err(|_| Error::DescriptorEntryCountTooLarge(offset))?;
let next_offset = entry_size
.checked_mul(num_entries)
.and_then(|o| o.checked_add(offset))
.ok_or_else(|| Error::Descriptor(offset, "Next entry offset out of bounds".into()))?;
.ok_or(Error::DescriptorNextOffsetTooLarge(offset))?;
self.offset = offset.into();
self.entry_size = entry_size.into();
@@ -390,28 +486,21 @@ impl RawHeader {
/// function is called.
fn validate(&self, geometry: &RawGeometry) -> Result<()> {
if self.magic.get() != HEADER_MAGIC {
return Err(Error::Header(format!(
"Invalid magic: {:#010x}",
self.magic.get(),
)));
return Err(Error::HeaderInvalidMagic(self.magic.get()));
}
if self.major_version.get() != MAJOR_VERSION || self.minor_version.get() > MINOR_VERSION_MAX
{
return Err(Error::Header(format!(
"Unsupported version: {}.{}",
self.major_version.get(),
self.minor_version.get(),
)));
return Err(Error::HeaderUnsupportedVersion {
major: self.major_version.get(),
minor: self.minor_version.get(),
});
}
let expected_size = self.size();
if self.header_size.get() != expected_size as u32 {
return Err(Error::Header(format!(
"Invalid struct size: {}",
self.header_size.get(),
)));
return Err(Error::HeaderInvalidSize(self.header_size.get()));
}
if self.minor_version.get() < VERSION_FOR_EXPANDED_HEADER {
@@ -429,18 +518,21 @@ impl RawHeader {
let digest = ring::digest::digest(&ring::digest::SHA256, portion);
if digest.as_ref() != self.header_checksum {
return Err(Error::Header(format!(
"Expected header digest {}, but have {}",
hex::encode(self.header_checksum),
hex::encode(digest),
)));
return Err(Error::HeaderInvalidDigest {
expected: hex::encode(self.header_checksum),
actual: hex::encode(digest),
});
}
}
// metadata_max_size is guaranteed to be at least one sector, so the
// subtraction cannot overflow.
if self.tables_size.get() > geometry.metadata_max_size.get() - self.header_size.get() {
return Err(Error::Header("Metadata slot exceeds maximum size".into()));
return Err(Error::MetadataTooLarge {
metadata_size: self.tables_size.get(),
max_size: geometry.metadata_max_size.get(),
header_size: self.header_size.get(),
});
}
let mut offset = 0;
@@ -454,12 +546,12 @@ impl RawHeader {
] {
offset = self
.validate_descriptor(descriptor, offset)
.ok_or_else(|| Error::Header("Descriptors out of bounds".into()))?;
.ok_or(Error::DescriptorsTooLargeOrHaveGaps)?;
}
// There cannot be a gap at the end either.
if offset != self.tables_size.get() {
return Err(Error::Header("Gap after last descriptor".into()));
return Err(Error::DescriptorsFinalGap);
}
if self.partitions.entry_size.get() != mem::size_of::<RawPartition>() as u32
@@ -467,7 +559,7 @@ impl RawHeader {
|| self.groups.entry_size.get() != mem::size_of::<RawPartitionGroup>() as u32
|| self.block_devices.entry_size.get() != mem::size_of::<RawBlockDevice>() as u32
{
return Err(Error::Header("Invalid descriptor entry sizes".into()));
return Err(Error::DescriptorsInvalidEntrySizes);
}
Ok(())
@@ -494,10 +586,10 @@ impl fmt::Debug for PartitionName {
impl PartitionName {
fn split(&self) -> (&[u8], &[u8]) {
match self.0.iter().position(|b| *b == 0) {
Some(i) => self.0.split_at(i),
None => (&self.0, &[]),
}
self.0
.iter()
.position(|b| *b == 0)
.map_or((&self.0, &[]), |i| self.0.split_at(i))
}
fn validate(&self) -> Result<()> {
@@ -598,10 +690,10 @@ impl RawPartition {
let attributes = PartitionAttributes::from_bits_retain(self.attributes.get());
if !(attributes - valid_attributes).is_empty() {
return Err(Error::Partition(
format!("{:?}", self.name),
format!("Invalid attributes: {}", attributes.0),
));
return Err(Error::PartitionInvalidAttributes {
name: DebugString::new(self.name),
attributes,
});
}
match image_type {
@@ -612,27 +704,25 @@ impl RawPartition {
.checked_add(self.num_extents.get())
.map_or(true, |n| n as usize > extents.len())
{
return Err(Error::Partition(
format!("{:?}", self.name),
"Extent indices out of bounds".into(),
));
return Err(Error::PartitionExtentIndicesTooLarge {
name: DebugString::new(self.name),
});
}
}
ImageType::Empty => {
if self.first_extent_index.get() != 0 || self.num_extents.get() != 0 {
return Err(Error::Partition(
format!("{:?}", self.name),
"Extent indices set on empty image".into(),
));
return Err(Error::PartitionExtentIndicesEmptyImage {
name: DebugString::new(self.name),
});
}
}
}
if self.group_index.get() as usize >= groups.len() {
return Err(Error::Partition(
format!("{:?}", self.name),
format!("Invalid partition group index: {}", self.group_index.get()),
));
return Err(Error::PartitionInvalidGroupIndex {
name: DebugString::new(self.name),
index: self.group_index.get(),
});
}
Ok(())
@@ -680,49 +770,51 @@ impl RawExtent {
match self.target_type.get() {
Self::TARGET_TYPE_LINEAR => {
let Some(device) = block_devices.get(self.target_source.get() as usize) else {
return Err(Error::Extent(
return Err(Error::ExtentInvalidDeviceIndex {
index,
format!("Invalid block device index: {}", self.target_source.get()),
));
device_index: self.target_source.get(),
});
};
let count = self.num_sectors.get();
let start = self.target_data.get();
let end = start.checked_add(count).ok_or_else(|| {
Error::Extent(
let end = start.checked_add(count).ok_or({
Error::ExtentEndSectorTooLarge {
index,
format!("End sector out of bounds: {start} + {count}"),
)
start,
count,
}
})?;
if start < device.first_logical_sector.get() {
return Err(Error::Extent(
return Err(Error::ExtentStartBeforeDeviceStart {
index,
format!(
"{start} starts before block device's first logical sector {}",
device.first_logical_sector,
),
));
start,
sector: device.first_logical_sector.get(),
});
}
let device_sectors = device.size.get() / u64::from(SECTOR_SIZE);
if end > device_sectors {
return Err(Error::Extent(
return Err(Error::ExtentEndsAfterDeviceEnd {
index,
format!("{end} ends after block device's sector size {device_sectors}"),
));
end,
sector: device_sectors,
});
}
}
Self::TARGET_TYPE_ZERO => {
if self.target_data.get() != 0 || self.target_source.get() != 0 {
return Err(Error::Extent(
index,
"Type zero extents cannot have non-zero sector or device".into(),
));
return Err(Error::ExtentTypeZeroNotEmpty { index });
}
}
n => return Err(Error::Extent(index, format!("Invalid type: {n}"))),
n => {
return Err(Error::ExtentInvalidType {
index,
extent_type: n,
})
}
}
Ok(())
@@ -776,24 +868,19 @@ impl RawPartitionGroup {
for extent in &extents[first..][..count] {
total_size = total_size
.checked_add(extent.num_sectors.get())
.ok_or_else(|| {
Error::PartitionGroup(
format!("{:?}", self.name),
"Size of group's partitions out of bounds".into(),
)
.ok_or_else(|| Error::GroupTotalSizeTooLarge {
name: DebugString::new(self.name),
})?;
}
}
}
if total_size > self.maximum_size.get() {
return Err(Error::PartitionGroup(
format!("{:?}", self.name),
format!(
"Total partition size {total_size} exceeds limit {}",
self.maximum_size.get(),
),
));
return Err(Error::GroupTotalSizeExceedsLimit {
name: DebugString::new(self.name),
size: total_size,
limit: self.maximum_size.get(),
});
}
}
@@ -840,37 +927,32 @@ impl RawBlockDevice {
/// further checks.
fn validate(&self) -> Result<()> {
if self.alignment.get() == 0 {
return Err(Error::BlockDevice(
format!("{:?}", self.partition_name),
"Alignment is 0".into(),
));
return Err(Error::DeviceAlignmentIsZero {
name: DebugString::new(self.partition_name),
});
} else if self.alignment.get() % SECTOR_SIZE != 0 {
return Err(Error::BlockDevice(
format!("{:?}", self.partition_name),
"Partition alignment is not sector-aligned".into(),
));
return Err(Error::DeviceAlignmentNotSectorAligned {
name: DebugString::new(self.partition_name),
});
}
let alignment_sectors = u64::from(self.alignment.get() / SECTOR_SIZE);
if self.first_logical_sector.get() % alignment_sectors != 0 {
return Err(Error::BlockDevice(
format!("{:?}", self.partition_name),
"First logical sector is not partition-aligned".into(),
));
return Err(Error::DeviceFirstSectorNotAligned {
name: DebugString::new(self.partition_name),
});
}
if self.alignment_offset.get() % SECTOR_SIZE != 0 {
return Err(Error::BlockDevice(
format!("{:?}", self.partition_name),
"Alignment offset is not sector-aligned".into(),
));
return Err(Error::DeviceOffsetNotSectorAligned {
name: DebugString::new(self.partition_name),
});
}
if self.size.get() % u64::from(SECTOR_SIZE) != 0 {
return Err(Error::BlockDevice(
format!("{:?}", self.partition_name),
"Size is not sector-aligned".into(),
));
return Err(Error::DeviceSizeNotSectorAligned {
name: DebugString::new(self.partition_name),
});
}
self.partition_name.validate()
@@ -904,10 +986,11 @@ impl RawMetadataSlot {
),
] {
if len != descriptor.num_entries.get() as usize {
return Err(Error::Header(format!(
"Descriptor entries {} does not match {name} table length {len}",
descriptor.num_entries.get(),
)));
return Err(Error::DescriptorMismatchedEntryCount {
name,
entry_count: descriptor.num_entries.get(),
table_len: len,
});
}
}
@@ -933,14 +1016,11 @@ impl RawMetadataSlot {
match a.target_source.get().cmp(&b.target_source.get()) {
Ordering::Equal => {
if a.target_data.get() + a.num_sectors.get() > b.target_data.get() {
return Err(Error::Extent(i, "Overlaps previous extent".into()));
return Err(Error::ExtentOverlapsPrevious { index: i });
}
}
Ordering::Greater => {
return Err(Error::Extent(
i,
"Earlier block device index than previous extent".into(),
));
return Err(Error::ExtentDeviceNotConsecutive { index: i });
}
Ordering::Less => {}
}
@@ -991,18 +1071,15 @@ impl RawMetadata {
let mut geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
match geometry.validate() {
Ok(_) => {
// Skip the backup copy.
reader.read_discard_exact(GEOMETRY_SIZE.into())?;
}
Err(_) => {
// Try to parse the backup copy.
reader.read_exact(&mut buf)?;
if geometry.validate().is_ok() {
// Skip the backup copy.
reader.read_discard_exact(GEOMETRY_SIZE.into())?;
} else {
// Try to parse the backup copy.
reader.read_exact(&mut buf)?;
geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
geometry.validate()?;
}
geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
geometry.validate()?;
}
geometry
@@ -1078,10 +1155,10 @@ impl RawMetadata {
ImageType::Empty => 1,
};
if self.slots.len() != expected_slots {
return Err(Error::Metadata(format!(
"Expected slot count {expected_slots}, but have {}",
self.slots.len(),
)));
return Err(Error::MismatchedSlotCount {
expected: expected_slots,
actual: self.slots.len(),
});
}
for slot in &self.slots {
@@ -1095,11 +1172,10 @@ impl RawMetadata {
let digest = context.finish();
if digest.as_ref() != slot.header.tables_checksum {
return Err(Error::Header(format!(
"Expected tables digest {}, but have {}",
hex::encode(slot.header.tables_checksum),
hex::encode(digest),
)));
return Err(Error::HeaderInvalidTablesDigest {
expected: hex::encode(slot.header.tables_checksum),
actual: hex::encode(digest),
});
}
}
@@ -1262,7 +1338,9 @@ impl Partition {
self.extents
.iter()
.try_fold(0u64, |total, e| total.checked_add(e.num_sectors))
.ok_or_else(|| Error::Partition(self.name.clone(), "Sector count overflow".into()))
.ok_or_else(|| Error::PartitionSectorCountTooLarge {
name: DebugString::new(&self.name),
})
}
/// Compute the number of bytes covered by the extents.
@@ -1270,7 +1348,9 @@ impl Partition {
self.num_sectors()
.ok()
.and_then(|n| n.checked_mul(SECTOR_SIZE.into()))
.ok_or_else(|| Error::Partition(self.name.clone(), "Byte count overflow".into()))
.ok_or_else(|| Error::PartitionByteCountTooLarge {
name: DebugString::new(&self.name),
})
}
}
@@ -1621,16 +1701,24 @@ impl TryFrom<&MetadataSlot> for RawMetadataSlot {
};
let group_index: u32 =
raw_slot.groups.len().try_into().map_err(|_| {
Error::PartitionGroup(group.name.clone(), "Index too large".into())
})?;
raw_slot
.groups
.len()
.try_into()
.map_err(|_| Error::GroupIndexTooLarge {
name: DebugString::new(&group.name),
})?;
for partition in &group.partitions {
let extent_index: u32 = raw_slot.extents.len().try_into().map_err(|_| {
Error::Partition(partition.name.clone(), "Extent index too large".into())
Error::PartitionExtentIndexTooLarge {
name: DebugString::new(&partition.name),
}
})?;
let num_extents: u32 = partition.extents.len().try_into().map_err(|_| {
Error::Partition(partition.name.clone(), "Too many extents".into())
Error::PartitionExtentCountTooLarge {
name: DebugString::new(&partition.name),
}
})?;
let raw_partition = RawPartition {
@@ -1649,10 +1737,9 @@ impl TryFrom<&MetadataSlot> for RawMetadataSlot {
} => {
let block_device_index: u32 =
block_device_index.try_into().map_err(|_| {
Error::Extent(
raw_slot.extents.len(),
"Block device index too large".into(),
)
Error::ExtentDeviceIndexTooLarge {
index: raw_slot.extents.len(),
}
})?;
(
+32 -15
View File
@@ -135,7 +135,7 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
}
"ota-wipe" => metadata.wipe = parse_yes()?,
"ota-retrofit-dynamic-partitions" => {
metadata.retrofit_dynamic_partitions = parse_yes()?
metadata.retrofit_dynamic_partitions = parse_yes()?;
}
"ota-downgrade" => metadata.downgrade = parse_yes()?,
"ota-required-cache" => {
@@ -190,7 +190,7 @@ pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
/// Generate the legacy plain-text and modern protobuf serializations of the
/// given metadata instance.
fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
fn serialize_metadata(metadata: &OtaMetadata) -> (String, Vec<u8>) {
use std::fmt::Write;
let mut pairs = BTreeMap::<String, String>::new();
@@ -254,7 +254,7 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
});
let modern_metadata = metadata.encode_to_vec();
Ok((legacy_metadata, modern_metadata))
(legacy_metadata, modern_metadata)
}
#[derive(Clone, Debug)]
@@ -300,6 +300,7 @@ fn compute_property_files(
pf_name: &str,
entries: &[ZipEntry],
max_length: Option<usize>,
want_pb: bool,
) -> Result<String> {
let compute = |path: &'static str| -> Result<String> {
let entry = entries
@@ -334,10 +335,14 @@ fn compute_property_files(
if max_length.is_none() {
tokens.push(format!("metadata:{}", " ".repeat(15)));
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
if want_pb {
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
}
} else {
tokens.push(compute(PATH_METADATA)?);
tokens.push(compute(PATH_METADATA_PB)?);
if want_pb {
tokens.push(compute(PATH_METADATA_PB)?);
}
}
let mut joined = tokens.join(",");
@@ -415,13 +420,13 @@ pub fn add_metadata(
for pf in [PF_NAME, PF_STREAMING_NAME] {
metadata.property_files.insert(
pf.to_owned(),
compute_property_files(pf, &zip_entries, None)?,
compute_property_files(pf, &zip_entries, None, true)?,
);
}
// Add the placeholders to a temporary zip to compute final property files.
let (temp_legacy_offset, temp_modern_offset) = {
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
let raw_writer = Cursor::new(Vec::new());
let mut writer = match zip_mode {
ZipMode::Streaming => ZipWriter::new_streaming(raw_writer),
@@ -452,12 +457,12 @@ pub fn add_metadata(
// Compute the final property files using the offsets of the fake entries.
for (key, value) in &mut metadata.property_files {
*value = compute_property_files(key, &zip_entries, Some(value.len()))?;
*value = compute_property_files(key, &zip_entries, Some(value.len()), true)?;
}
// Add the final metadata files to the real zip.
{
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
zip_writer.start_file_with_extra_data(PATH_METADATA, options)?;
let legacy_offset = zip_writer.end_extra_data()?;
@@ -494,8 +499,11 @@ pub fn verify_metadata(
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
let metadata_pb = zip_entries.iter().find(|e| e.name == PATH_METADATA_PB);
for (key, value) in &metadata.property_files {
let new_value = compute_property_files(key, &zip_entries, Some(value.len()))?;
let new_value =
compute_property_files(key, &zip_entries, Some(value.len()), metadata_pb.is_some())?;
if *value != new_value {
return Err(Error::MismatchedPropertyFiles {
expected: value.clone(),
@@ -627,11 +635,20 @@ pub fn parse_zip_ota_info(
) -> Result<(OtaMetadata, Certificate, PayloadHeader, String)> {
let mut zip = ZipArchive::new(reader)?;
let metadata = {
let mut entry = zip.by_name(PATH_METADATA_PB)?;
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
OtaMetadata::decode(buf.as_slice())?
let metadata = match zip.by_name(PATH_METADATA_PB) {
Ok(mut entry) => {
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
parse_protobuf_metadata(&buf)?
}
e @ Err(ZipError::FileNotFound) => {
drop(e);
let mut entry = zip.by_name(PATH_METADATA)?;
let mut buf = String::new();
entry.read_to_string(&mut buf)?;
parse_legacy_metadata(&buf)?
}
Err(e) => return Err(e.into()),
};
let certificate = {
+33 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Read, Seek, Write};
@@ -45,3 +45,35 @@ pub fn write_zeros(mut writer: impl Write + Seek, page_size: u64) -> io::Result<
Ok(padding)
}
pub trait ZeroPadding {
/// Trim trailing zeros. Intermediate zeros before the last non-zero byte
/// are kept.
fn trim_end_padding(&self) -> &[u8];
/// Return the slice as an array padded with zeros at the end.
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]>;
}
impl ZeroPadding for [u8] {
fn trim_end_padding(&self) -> &[u8] {
let first_ending_zero = self
.iter()
.rposition(|b| *b != 0)
.map(|pos| pos + 1)
.unwrap_or_default();
&self[..first_ending_zero]
}
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]> {
if self.len() > N {
return None;
}
let mut result = [0u8; N];
result[..self.len()].copy_from_slice(self);
Some(result)
}
}
+58 -61
View File
@@ -11,14 +11,12 @@ use std::{
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use byteorder::{BigEndian, ReadBytesExt};
use bzip2::write::BzDecoder;
use liblzma::{
stream::{Check, Stream},
write::XzDecoder,
write::XzEncoder,
};
use num_traits::ToPrimitive;
use prost::Message;
use rayon::{
iter::{IndexedParallelIterator, IntoParallelRefMutIterator},
@@ -28,6 +26,8 @@ use ring::digest::{Context, Digest};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use x509_cert::Certificate;
use zerocopy::{big_endian, FromBytes, IntoBytes};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
@@ -39,11 +39,11 @@ use crate::{
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadSeekReopen, WriteSeek,
WriteSeekReopen,
},
util,
util::{self, OutOfBoundsError},
};
const OTA_MAGIC: &[u8; 4] = b"CrAU";
const OTA_HEADER_SIZE: usize = OTA_MAGIC.len() + 8 + 8 + 4;
const PAYLOAD_MAGIC: &[u8; 4] = b"CrAU";
const PAYLOAD_VERSION: u64 = 2;
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
@@ -91,7 +91,9 @@ pub enum Error {
#[error("{0:?} field is missing")]
MissingField(&'static str),
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("Crypto error")]
Crypto(#[from] crypto::Error),
#[error("Failed to decode protobuf message")]
@@ -104,6 +106,20 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>;
/// Raw on-disk layout for the payload header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
struct RawHeader {
/// Magic value. This should be equal to [`PAYLOAD_MAGIC`].
magic: [u8; 4],
/// Image version. This should be equal to [`PAYLOAD_VERSION`].
file_format_version: big_endian::U64,
/// Size of the [`DeltaArchiveManifest`] blob.
manifest_size: big_endian::U64,
/// Size of the [`Signatures`] blob.
metadata_signature_size: big_endian::U32,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PayloadHeader {
pub version: u64,
@@ -131,41 +147,31 @@ impl<R: Read> FromReader<R> for PayloadHeader {
fn from_reader(reader: R) -> Result<Self> {
let mut reader = CountingReader::new(reader);
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if magic != *OTA_MAGIC {
return Err(Error::UnknownMagic(magic));
let header = RawHeader::read_from_io(&mut reader)?;
if header.magic != *PAYLOAD_MAGIC {
return Err(Error::UnknownMagic(header.magic));
}
let version = reader.read_u64::<BigEndian>()?;
if version != 2 {
return Err(Error::UnsupportedVersion(version));
if header.file_format_version != PAYLOAD_VERSION {
return Err(Error::UnsupportedVersion(header.file_format_version.get()));
}
let manifest_size = reader
.read_u64::<BigEndian>()?
.to_usize()
.and_then(|s| {
if s <= MANIFEST_MAX_SIZE {
Some(s)
} else {
None
}
})
.ok_or_else(|| Error::FieldOutOfBounds("manifest_size"))?;
let metadata_signature_size = reader.read_u32::<BigEndian>()?;
let manifest_size: usize = util::try_cast(header.manifest_size.get())
.and_then(|s| util::check_bounds(s, ..=MANIFEST_MAX_SIZE))
.map_err(|e| Error::IntOutOfBounds("manifest_size", e))?;
let mut manifest_raw = vec![0u8; manifest_size];
reader.read_exact(&mut manifest_raw)?;
let manifest = DeltaArchiveManifest::decode(manifest_raw.as_slice())?;
// Skip manifest signatures.
reader.read_discard_exact(metadata_signature_size.into())?;
reader.read_discard_exact(header.metadata_signature_size.into())?;
Ok(Self {
version,
version: header.file_format_version.get(),
manifest,
metadata_signature_size,
metadata_signature_size: header.metadata_signature_size.get(),
blob_offset: reader.stream_position()?,
})
}
@@ -206,13 +212,13 @@ fn verify_digest(digest: &[u8], signatures: &Signatures, cert: &Certificate) ->
let Some(data) = &signature.data else {
continue;
};
let Some(size) = signature.unpadded_signature_size else {
continue;
};
let without_padding = &data[..size as usize];
let size = signature
.unpadded_signature_size
.map_or(data.len(), |s| s as usize);
let without_padding = &data[..size];
match public_key.verify_sig(SignatureAlgorithm::Sha256WithRsa, digest, without_padding) {
Ok(_) => return Ok(()),
Ok(()) => return Ok(()),
Err(e) => last_error = Some(e),
}
}
@@ -347,18 +353,13 @@ impl<W: Write> PayloadWriter<W> {
let mut h_full = Context::new(&ring::digest::SHA256);
// Write header to output file.
write_hash!(inner, [h_partial, h_full], OTA_MAGIC)?;
write_hash!(inner, [h_partial, h_full], &header.version.to_be_bytes())?;
write_hash!(
inner,
[h_partial, h_full],
&(manifest_raw_new.len() as u64).to_be_bytes(),
)?;
write_hash!(
inner,
[h_partial, h_full],
&(dummy_sig_size as u32).to_be_bytes()
)?;
let raw_header = RawHeader {
magic: *PAYLOAD_MAGIC,
file_format_version: header.version.into(),
manifest_size: (manifest_raw_new.len() as u64).into(),
metadata_signature_size: (dummy_sig_size as u32).into(),
};
write_hash!(inner, [h_partial, h_full], raw_header.as_bytes())?;
// Write new manifest.
write_hash!(inner, [h_partial, h_full], &manifest_raw_new)?;
@@ -374,7 +375,7 @@ impl<W: Write> PayloadWriter<W> {
inner,
header,
metadata_hash,
metadata_size: OTA_HEADER_SIZE + manifest_raw_new.len(),
metadata_size: raw_header.as_bytes().len() + manifest_raw_new.len(),
partition_index: None,
operation_index: None,
done: false,
@@ -693,10 +694,10 @@ pub fn apply_operation(
let out_offset = start_block
.checked_mul(block_size.into())
.ok_or_else(|| Error::FieldOutOfBounds("out_offset"))?;
.ok_or_else(|| Error::IntOverflow("out_offset"))?;
let out_data_length = num_blocks
.checked_mul(block_size.into())
.ok_or_else(|| Error::FieldOutOfBounds("out_data_length"))?;
.ok_or_else(|| Error::IntOverflow("out_data_length"))?;
writer.seek(SeekFrom::Start(out_offset))?;
@@ -723,7 +724,7 @@ pub fn apply_operation(
.ok_or_else(|| Error::MissingField("data_length"))?;
let in_offset = blob_offset
.checked_add(data_offset)
.ok_or_else(|| Error::FieldOutOfBounds("in_offset"))?;
.ok_or_else(|| Error::IntOverflow("in_offset"))?;
reader.seek(SeekFrom::Start(in_offset))?;
@@ -901,7 +902,7 @@ impl VabcAlgo {
}
}
fn compressed_size(&self, mut raw_data: &[u8], block_size: u32) -> u64 {
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> u64 {
let mut total = 0;
while !raw_data.is_empty() {
@@ -1010,11 +1011,8 @@ pub fn compress_image(
.map(
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
let cow_size = if let Some(algo) = vabc_algo {
algo.compressed_size(&raw_data, block_size)
} else {
0
};
let cow_size =
vabc_algo.map_or(0, |a| a.compressed_size(&raw_data, block_size));
let extent = Extent {
start_block: Some(raw_offset / u64::from(block_size)),
@@ -1158,20 +1156,19 @@ pub fn compress_modified_image(
let extents_start = operation.dst_extents[0]
.start_block()
.checked_mul(u64::from(block_size))
.ok_or_else(|| Error::FieldOutOfBounds("extents_start"))?;
.ok_or_else(|| Error::IntOverflow("extents_start"))?;
let extents_size = operation
.dst_extents
.iter()
.map(|e| e.num_blocks())
.try_fold(0u64, |acc, n| acc.checked_add(n))
.and_then(|n| n.checked_mul(u64::from(block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
.ok_or_else(|| Error::IntOverflow("extents_size"))?;
let extents_end = extents_start
.checked_add(extents_size)
.ok_or_else(|| Error::FieldOutOfBounds("extents_end"))?;
let extents_size = extents_size
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
.ok_or_else(|| Error::IntOverflow("extents_end"))?;
let extents_size: usize = util::try_cast(extents_size)
.map_err(|e| Error::IntOutOfBounds("extents_size", e))?;
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(extents_start))?;
+133 -111
View File
@@ -11,9 +11,11 @@ use std::{
use crc32fast::Hasher;
use dlv_list::{Index, VecList};
use thiserror::Error;
use zerocopy::{byteorder::little_endian, FromZeros, IntoBytes};
use zerocopy::{byteorder::little_endian, FromBytes, IntoBytes};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::stream::ReadDiscardExt;
/// Magic value for [`RawHeader::magic`].
const HEADER_MAGIC: u32 = 0xed26ff3a;
@@ -33,14 +35,67 @@ pub const MINOR_VERSION: u16 = 0;
#[derive(Debug, Error)]
pub enum Error {
#[error("Sparse header: {0}")]
Header(String),
#[error("Sparse chunk #{0}: {1}")]
Chunk(u32, String),
#[error("Sparse reader: {0}")]
Reader(String),
#[error("Sparse writer: {0}")]
Writer(String),
// Header errors.
#[error("Invalid magic: {0:#010x}")]
InvalidMagic(u32),
#[error("Unsupported major version: {0}")]
UnsupportedMajorVersion(u16),
#[error("Invalid file header size: {0} < {size}", size = mem::size_of::<RawHeader>())]
InvalidFileHeaderSize(u16),
#[error("Invalid chunk header size: {0} < {size}", size = mem::size_of::<RawChunk>())]
InvalidChunkHeaderSize(u16),
#[error("Invalid block size (must be a non-zero multiple of 4): {0}")]
InvalidBlockSize(u32),
// Chunk errors.
#[error("Chunk #{index}: Size overflow: {chunk_size} * {block_size}")]
ChunkSizeOverflow {
index: u32,
chunk_size: u32,
block_size: u32,
},
#[error("Chunk #{index}: Invalid type: {chunk_type}")]
InvalidChunkType { index: u32, chunk_type: u16 },
#[error("Chunk #{index}: Data size too large: {data_size}")]
DataSizeTooLarge { index: u32, data_size: u32 },
#[error("Chunk #{index}: Block count overflow: {start_block} + {chunk_size}")]
BlockCountOverflow {
index: u32,
start_block: u32,
chunk_size: u32,
},
#[error("Chunk #{index}: End block {end_block} exceeds total blocks {total_blocks}")]
EndBlockExceedsTotal {
index: u32,
end_block: u32,
total_blocks: u32,
},
#[error("Chunk #{index}: CRC32 chunk is not empty")]
Crc32ChunkNotEmpty { index: u32, chunk_size: u32 },
#[error("Chunk #{index}: Expected total size {expected_size}, but have {total_size}")]
InvalidChunkSize {
index: u32,
expected_size: u32,
total_size: u32,
},
// Reader errors.
#[error("Must fully consume data when CRC validation is enabled")]
Crc32RandomRead,
#[error("Previous chunk still has {0} unread bytes")]
UnreadChunkData(u32),
#[error("Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}")]
MismatchedCrc32Checkpoint { expected: u32, actual: u32 },
#[error("Expected final CRC32 {expected:08x}, but have {actual:08x}")]
MismatchedCrc32Final { expected: u32, actual: u32 },
// Writer errors.
#[error("Minor version not supported for writing: {0}")]
UnsupportedMinorVersion(u16),
#[error("Previous chunk still has {0} unwritten bytes")]
UnwrittenChunkData(u32),
#[error("Already wrote all chunk headers")]
TooManyChunks,
#[error("Gap between end of last chunk {prev_end} and start of new chunk {cur_start}")]
GapBetweenChunks { prev_end: u32, cur_start: u32 },
// Wrapped errors.
#[error("I/O error")]
Io(#[from] io::Error),
}
@@ -95,40 +150,33 @@ impl fmt::Debug for RawHeader {
impl RawHeader {
fn validate(&self) -> Result<()> {
if self.magic.get() != HEADER_MAGIC {
return Err(Error::Header(format!(
"Invalid magic: {:#010x}",
self.magic.get(),
)));
return Err(Error::InvalidMagic(self.magic.get()));
}
if self.major_version.get() != MAJOR_VERSION {
return Err(Error::Header(format!(
"Unsupported major version: {}",
self.major_version.get(),
)));
return Err(Error::UnsupportedMajorVersion(self.major_version.get()));
}
if self.file_hdr_sz.get() != mem::size_of::<RawHeader>() as u16 {
return Err(Error::Header(format!(
"Invalid file header size: {}",
self.file_hdr_sz.get(),
)));
} else if self.chunk_hdr_sz.get() != mem::size_of::<RawChunk>() as u16 {
return Err(Error::Header(format!(
"Invalid chunk header size: {}",
self.chunk_hdr_sz.get(),
)));
if self.file_hdr_sz.get() < mem::size_of::<RawHeader>() as u16 {
return Err(Error::InvalidFileHeaderSize(self.file_hdr_sz.get()));
} else if self.chunk_hdr_sz.get() < mem::size_of::<RawChunk>() as u16 {
return Err(Error::InvalidChunkHeaderSize(self.chunk_hdr_sz.get()));
}
if self.blk_sz.get() == 0 || self.blk_sz.get() % 4 != 0 {
return Err(Error::Header(format!(
"Invalid block size: {}",
self.blk_sz.get(),
)));
return Err(Error::InvalidBlockSize(self.blk_sz.get()));
}
Ok(())
}
fn excess_raw_header_bytes(&self) -> u16 {
self.file_hdr_sz.get() - mem::size_of::<RawHeader>() as u16
}
fn excess_raw_chunk_bytes(&self) -> u16 {
self.chunk_hdr_sz.get() - mem::size_of::<RawChunk>() as u16
}
}
/// Raw on-disk layout for the chunk header.
@@ -164,69 +212,58 @@ impl RawChunk {
.chunk_sz
.get()
.checked_mul(header.blk_sz.get())
.ok_or_else(|| {
Error::Chunk(
index,
format!(
"Chunk size overflow: {} * {}",
self.chunk_sz.get(),
header.blk_sz.get(),
),
)
.ok_or_else(|| Error::ChunkSizeOverflow {
index,
chunk_size: self.chunk_sz.get(),
block_size: header.blk_sz.get(),
})?,
CHUNK_TYPE_FILL | CHUNK_TYPE_CRC32 => 4,
CHUNK_TYPE_DONT_CARE => 0,
t => return Err(Error::Chunk(index, format!("Invalid chunk type: {t}"))),
t => {
return Err(Error::InvalidChunkType {
index,
chunk_type: t,
})
}
};
data_size
.checked_add(mem::size_of::<Self>() as u32)
.ok_or_else(|| Error::Chunk(index, format!("Data size too large: {data_size}")))
.checked_add(header.chunk_hdr_sz.into())
.ok_or(Error::DataSizeTooLarge { index, data_size })
}
fn validate(&self, index: u32, header: &RawHeader, start_block: u32) -> Result<()> {
let end_block = start_block
.checked_add(self.chunk_sz.get())
.ok_or_else(|| {
Error::Chunk(
index,
format!(
"Block count overflow: {start_block} + {}",
self.chunk_sz.get(),
),
)
.ok_or_else(|| Error::BlockCountOverflow {
index,
start_block,
chunk_size: self.chunk_sz.get(),
})?;
if end_block > header.total_blks.get() {
return Err(Error::Chunk(
return Err(Error::EndBlockExceedsTotal {
index,
format!(
"End block {end_block} exceeds total blocks {}",
header.total_blks.get(),
),
))?;
end_block,
total_blocks: header.total_blks.get(),
})?;
}
if self.chunk_type.get() == CHUNK_TYPE_CRC32 && self.chunk_sz.get() != 0 {
return Err(Error::Chunk(
return Err(Error::Crc32ChunkNotEmpty {
index,
format!(
"CRC32 chunk has non-zero blocks: {:?}",
start_block..end_block,
),
));
chunk_size: self.chunk_sz.get(),
});
}
let expected_size = self.expected_size(index, header)?;
if expected_size != self.total_sz.get() {
return Err(Error::Chunk(
return Err(Error::InvalidChunkSize {
index,
format!(
"Expected total size {expected_size}, but have {}",
self.total_sz.get(),
),
));
expected_size,
total_size: self.total_sz.get(),
});
}
Ok(())
@@ -657,11 +694,12 @@ impl<R: Read> SparseReader<R> {
/// data chunks if they are not needed. If the underlying file is seekable
/// and skipping chunks is needed, use [`Self::new_seekable`] instead.
pub fn new(mut inner: R, crc_mode: CrcMode) -> Result<Self> {
let mut header = RawHeader::new_zeroed();
inner.read_exact(header.as_mut_bytes())?;
let header = RawHeader::read_from_io(&mut inner)?;
header.validate()?;
inner.read_discard(header.excess_raw_header_bytes().into())?;
Ok(Self {
inner,
seek: None,
@@ -700,18 +738,13 @@ impl<R: Read> SparseReader<R> {
if self.data_remain != 0 {
if let Some(seek) = self.seek {
if self.hasher.is_some() {
return Err(Error::Reader(
"Cannot skip data when CRC validation is enabled".into(),
));
return Err(Error::Crc32RandomRead);
}
seek(&mut self.inner, SeekFrom::Current(self.data_remain.into()))?;
self.data_remain = 0;
} else {
return Err(Error::Reader(format!(
"Previous chunk still has {} bytes remaining",
self.data_remain,
)));
return Err(Error::UnreadChunkData(self.data_remain));
}
}
@@ -719,11 +752,13 @@ impl<R: Read> SparseReader<R> {
return Ok(None);
}
let mut raw_chunk = RawChunk::new_zeroed();
self.inner.read_exact(raw_chunk.as_mut_bytes())?;
let raw_chunk = RawChunk::read_from_io(&mut self.inner)?;
raw_chunk.validate(self.chunk, &self.header, self.block)?;
self.inner
.read_discard(self.header.excess_raw_chunk_bytes().into())?;
let data: ChunkData;
match raw_chunk.chunk_type.get() {
@@ -734,8 +769,7 @@ impl<R: Read> SparseReader<R> {
data = ChunkData::Data;
}
CHUNK_TYPE_FILL => {
let mut fill_value = little_endian::U32::new_zeroed();
self.inner.read_exact(fill_value.as_mut_bytes())?;
let fill_value = little_endian::U32::read_from_io(&mut self.inner)?;
if let Some(hasher) = &mut self.hasher {
hash_fill_chunk(&raw_chunk, fill_value, &self.header, hasher);
@@ -751,16 +785,16 @@ impl<R: Read> SparseReader<R> {
data = ChunkData::Hole;
}
CHUNK_TYPE_CRC32 => {
let mut expected = little_endian::U32::new_zeroed();
self.inner.read_exact(expected.as_mut_bytes())?;
let expected = little_endian::U32::read_from_io(&mut self.inner)?;
if let Some(hasher) = &mut self.hasher {
let actual = hasher.clone().finalize();
if actual != expected.get() {
return Err(Error::Reader(format!(
"Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}",
)));
return Err(Error::MismatchedCrc32Checkpoint {
expected: expected.get(),
actual,
});
}
}
@@ -791,9 +825,7 @@ impl<R: Read> SparseReader<R> {
let actual = hasher.finalize();
if actual != expected {
return Err(Error::Reader(format!(
"Expected final CRC32 {expected:08x}, but have {actual:08x}",
)));
return Err(Error::MismatchedCrc32Final { expected, actual });
}
}
}
@@ -837,10 +869,7 @@ impl<W: Write> SparseWriter<W> {
/// file to be seekable, so the [`Header`] must be fully known up front.
pub fn new(mut inner: W, header: Header) -> Result<Self> {
if header.minor_version != MINOR_VERSION {
return Err(Error::Writer(format!(
"Minor version not supported for writing: {}",
header.minor_version,
)));
return Err(Error::UnsupportedMinorVersion(header.minor_version));
}
let header = RawHeader {
@@ -857,7 +886,7 @@ impl<W: Write> SparseWriter<W> {
header.validate()?;
inner.write_all(header.as_bytes())?;
header.write_to_io(&mut inner)?;
Ok(Self {
inner,
@@ -875,21 +904,18 @@ impl<W: Write> SparseWriter<W> {
/// [`ChunkData::Data`], the data must be fully written first.
pub fn start_chunk(&mut self, chunk: Chunk) -> Result<()> {
if self.data_remain != 0 {
return Err(Error::Writer(format!(
"Previous chunk still has {} bytes remaining",
self.data_remain,
)));
return Err(Error::UnwrittenChunkData(self.data_remain));
}
if self.chunk == self.header.total_chunks.get() {
return Err(Error::Writer("Already wrote all chunk headers".into()));
return Err(Error::TooManyChunks);
}
if chunk.bounds.start != self.block {
return Err(Error::Writer(format!(
"Gap between end of last chunk {} and start of new chunk {}",
self.block, chunk.bounds.start,
)));
return Err(Error::GapBetweenChunks {
prev_end: self.block,
cur_start: chunk.bounds.start,
});
}
let mut raw_chunk = RawChunk {
@@ -911,7 +937,7 @@ impl<W: Write> SparseWriter<W> {
self.chunk += 1;
self.block = chunk.bounds.end;
self.inner.write_all(raw_chunk.as_bytes())?;
raw_chunk.write_to_io(&mut self.inner)?;
match chunk.data {
ChunkData::Data => {
@@ -936,9 +962,7 @@ impl<W: Write> SparseWriter<W> {
let actual = self.hasher.clone().finalize();
if actual != expected {
return Err(Error::Reader(format!(
"Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}",
)));
return Err(Error::MismatchedCrc32Checkpoint { expected, actual });
}
}
}
@@ -953,9 +977,7 @@ impl<W: Write> SparseWriter<W> {
let actual = self.hasher.finalize();
if actual != expected {
return Err(Error::Reader(format!(
"Expected final CRC32 {expected:08x}, but have {actual:08x}",
)));
return Err(Error::MismatchedCrc32Final { expected, actual });
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ fn main() -> ExitCode {
}
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
Ok(_) => ExitCode::SUCCESS,
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
error!("{e:?}");
+1 -1
View File
@@ -28,7 +28,7 @@ where
{
struct OctalStrVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
impl<T> Visitor<'_> for OctalStrVisitor<T>
where
T: PrimInt,
<T as Num>::FromStrRadixErr: fmt::Display,
+9 -5
View File
@@ -151,7 +151,7 @@ impl MagiskRootPatcher {
// RULESDEVICE config option, which stored the writable block device as an
// rdev major/minor pair, which was not consistent across reboots and was
// replaced by PREINITDEVICE
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..28100];
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..28200];
const VER_PREINIT_DEVICE: Range<u32> =
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
@@ -610,7 +610,6 @@ impl OtaCertPatcher {
}
fn patch_ramdisk(
&self,
ramdisk: &mut Vec<u8>,
zip: &[u8],
cancel_signal: &AtomicBool,
@@ -681,7 +680,7 @@ impl BootImagePatch for OtaCertPatcher {
continue;
}
if self.patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
if Self::patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
return Ok(());
}
}
@@ -794,7 +793,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
// For builds that don't trust any DSU keys, pick the first boot
// image that contains a first stage ramdisk directory.
if !first_stage_targets.is_empty() {
first_stage_targets.sort();
first_stage_targets.sort_unstable();
first_stage_targets.resize(1, "");
}
@@ -1134,6 +1133,11 @@ pub fn patch_boot_images<'a>(
) -> Result<HashSet<&'a str>> {
let parent_span = Span::current();
if patchers.is_empty() {
debug!("Skip loading boot images; nothing to patch");
return Ok(HashSet::new());
}
// Preparse all images. Some patchers need to inspect every candidate.
let mut images = load_boot_images(names, open_input)?;
@@ -1223,5 +1227,5 @@ pub fn patch_boot_images<'a>(
})
.collect::<Result<()>>()?;
Ok(groups.keys().cloned().collect())
Ok(groups.keys().copied().collect())
}
+5 -5
View File
@@ -33,8 +33,8 @@ pub enum Error {
NoFooter,
#[error("No hash tree descriptor found in vbmeta header")]
NoHashTreeDescriptor,
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("AVB error")]
Avb(#[from] avb::Error),
#[error("OTA certificate error")]
@@ -215,15 +215,15 @@ pub fn patch_system_image(
let hash_tree_end = descriptor
.tree_offset
.checked_add(descriptor.tree_size)
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
.ok_or_else(|| Error::IntOverflow("hash_tree_end"))?;
let fec_data_end = descriptor
.fec_offset
.checked_add(descriptor.fec_size)
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
.ok_or_else(|| Error::IntOverflow("fec_data_end"))?;
let header_end = footer
.vbmeta_offset
.checked_add(footer.vbmeta_size)
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
.ok_or_else(|| Error::IntOverflow("avb_end"))?;
let footer_start = image_size - Footer::SIZE as u64;
let other_ranges = util::merge_overlapping(&[
+3
View File
@@ -1,3 +1,6 @@
#![allow(clippy::nursery)]
#![allow(clippy::pedantic)]
pub mod build {
pub mod tools {
pub mod releasetools {
+19 -91
View File
@@ -10,7 +10,6 @@ use std::{
},
};
use bstr::ByteSlice;
use num_traits::ToPrimitive;
use ring::digest::Context;
@@ -122,69 +121,26 @@ impl<W: Write> WriteZerosExt for W {
}
}
/// Extensions for readers to read strings.
pub trait ReadStringExt {
/// Read exact sized string.
fn read_string_exact(&mut self, size: usize) -> io::Result<String>;
/// Extensions for readers to read fixed-size buffers.
pub trait ReadFixedSizeExt {
/// Read fixed-size array.
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]>;
/// Read string with maximum size and trim trailing zeros.
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String>;
/// Read fixed-sized [`Vec`].
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>>;
}
impl<R: Read> ReadStringExt for R {
fn read_string_exact(&mut self, size: usize) -> io::Result<String> {
impl<R: Read> ReadFixedSizeExt for R {
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]> {
let mut buf = [0u8; N];
self.read_exact(&mut buf)?;
Ok(buf)
}
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; size];
self.read_exact(&mut buf)?;
String::from_utf8(buf).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
)
})
}
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String> {
let mut buf = vec![0u8; max_size];
self.read_exact(&mut buf)?;
let after_last_non_zero = buf
.iter()
.rev()
.position(|&b| b != 0)
.map_or(0, |i| buf.len() - i);
buf.resize(after_last_non_zero, 0);
buf.shrink_to_fit();
String::from_utf8(buf).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
)
})
}
}
/// Extensions for writers to write strings.
pub trait WriteStringExt {
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()>;
}
impl<W: Write> WriteStringExt for W {
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()> {
if data.len() > max_size {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("{data:?} exceeds maximum size of {max_size} bytes"),
));
}
self.write_all(data.as_bytes())?;
let num_zeros = (max_size - data.len()) as u64;
self.write_zeros_exact(num_zeros)?;
Ok(())
Ok(buf)
}
}
@@ -197,13 +153,13 @@ pub trait Reopen: Sized {
impl<R: Read + Reopen> Reopen for BufReader<R> {
fn reopen(&self) -> io::Result<Self> {
Ok(BufReader::new(self.get_ref().reopen()?))
Ok(Self::new(self.get_ref().reopen()?))
}
}
impl<W: Write + Reopen> Reopen for BufWriter<W> {
fn reopen(&self) -> io::Result<Self> {
Ok(BufWriter::new(self.get_ref().reopen()?))
Ok(Self::new(self.get_ref().reopen()?))
}
}
@@ -543,9 +499,7 @@ pub struct SharedCursor {
impl SharedCursor {
pub fn new() -> Self {
Self {
..Default::default()
}
Self::default()
}
}
@@ -687,7 +641,7 @@ mod tests {
use super::{
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
ReadStringExt, Reopen, SectionReader, SharedCursor, WriteStringExt, WriteZerosExt,
Reopen, SectionReader, SharedCursor, WriteZerosExt,
};
const FOOBAR_SHA256: [u8; 32] = [
@@ -724,32 +678,6 @@ mod tests {
assert_eq!(&writer.into_inner(), b"\0\0foo\0");
}
#[test]
fn read_string() {
let mut reader = Cursor::new(b"foo\0\0bar\0\0");
assert_eq!(reader.read_string_exact(3).unwrap(), "foo");
assert_eq!(reader.read_string_exact(0).unwrap(), "");
reader.rewind().unwrap();
assert_eq!(reader.read_string_padded(3).unwrap(), "foo");
reader.rewind().unwrap();
assert_eq!(reader.read_string_padded(10).unwrap(), "foo\0\0bar");
}
#[test]
fn write_string() {
let mut writer = Cursor::new([0xffu8; 8]);
writer.write_string_padded("foobar", 8).unwrap();
assert_eq!(writer.get_ref(), b"foobar\0\0");
writer.rewind().unwrap();
writer.write_string_padded("foobarhi", 8).unwrap();
assert_eq!(writer.get_ref(), b"foobarhi");
}
#[test]
fn counting_reader() {
let raw_reader = Cursor::new(b"foobar");
+249 -17
View File
@@ -1,9 +1,15 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
use std::{
cmp::Ordering,
fmt, mem,
ops::{Bound, Range, RangeBounds},
path::Path,
};
use num_traits::PrimInt;
use num_traits::{NumCast, PrimInt};
use thiserror::Error;
pub const ZEROS: [u8; 16384] = [0u8; 16384];
@@ -21,6 +27,172 @@ impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
}
}
/// Stores a precomputed [`Debug`] string.
#[derive(Clone)]
pub struct DebugString(String);
impl DebugString {
pub fn new(value: impl fmt::Debug) -> Self {
Self(format!("{value:?}"))
}
}
impl fmt::Debug for DebugString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
/// A single bound in a range.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IntBound<T: PrimInt> {
Included(T),
Excluded(T),
}
/// A bounded primitive integer range. Unlike std's range types, this is a
/// single type that can represent open, closed, and half-open intervals.
#[derive(Clone, Copy, Debug)]
pub struct IntRange<T: PrimInt> {
pub start: IntBound<T>,
pub end: IntBound<T>,
}
impl<T: PrimInt> IntRange<T> {
/// Returns [`None`] if the range bounds cannot be represented by `T`. If
/// the start and end of `range` are unbounded, then it gets converted to
/// [`IntBound::Included`] with `N`'s minimum or maximum value.
pub fn new<N: PrimInt, R: RangeBounds<N>>(range: R) -> Option<Self> {
let start = match range.start_bound() {
Bound::Included(n) => IntBound::Included(T::from(*n)?),
Bound::Excluded(n) => IntBound::Excluded(T::from(*n)?),
Bound::Unbounded => IntBound::Included(T::from(N::min_value())?),
};
let end = match range.end_bound() {
Bound::Included(n) => IntBound::Included(T::from(*n)?),
Bound::Excluded(n) => IntBound::Excluded(T::from(*n)?),
Bound::Unbounded => IntBound::Included(T::from(N::max_value())?),
};
Some(Self { start, end })
}
}
impl<T: PrimInt + fmt::Display> fmt::Display for IntRange<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.start {
IntBound::Included(n) => write!(f, "[{n}, ")?,
IntBound::Excluded(n) => write!(f, "({n}, ")?,
}
match self.end {
IntBound::Included(n) => write!(f, "{n}]"),
IntBound::Excluded(n) => write!(f, "{n})"),
}
}
}
/// A non-generic type that can represent any 64-bit or smaller primitive
/// integer.
#[derive(Clone, Copy, Debug)]
pub enum LargeInt {
Signed(i64),
Unsigned(u64),
}
impl fmt::Display for LargeInt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Signed(n) => n.fmt(f),
Self::Unsigned(n) => n.fmt(f),
}
}
}
/// A non-generic type that can represent any 64-bit or smaller primitive
/// integer range.
#[derive(Clone, Copy, Debug)]
pub enum LargeIntRange {
Signed(IntRange<i64>),
Unsigned(IntRange<u64>),
}
impl fmt::Display for LargeIntRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Signed(r) => r.fmt(f),
Self::Unsigned(r) => r.fmt(f),
}
}
}
/// An error returned when a value is not within a specific range.
#[derive(Clone, Copy, Debug, Error)]
#[error("Integer value {value} not in bounds: {range}")]
pub struct OutOfBoundsError {
value: LargeInt,
range: LargeIntRange,
}
/// Verify that `value` is within `bounds` and then return `value` if it is.
pub fn check_bounds<T: PrimInt>(
value: T,
bounds: impl RangeBounds<T>,
) -> Result<T, OutOfBoundsError> {
const {
assert!(
mem::size_of::<T>() <= 8,
"Integer must be 64 bits or smaller"
);
}
if !bounds.contains(&value) {
let value = if T::min_value() != T::zero() {
LargeInt::Signed(NumCast::from(value).unwrap())
} else {
LargeInt::Unsigned(NumCast::from(value).unwrap())
};
let range = if T::min_value() != T::zero() {
LargeIntRange::Signed(IntRange::new(bounds).unwrap())
} else {
LargeIntRange::Unsigned(IntRange::new(bounds).unwrap())
};
return Err(OutOfBoundsError { value, range });
}
Ok(value)
}
/// Try to cast `value` to primitive integer type `T`. If it does not fit, the
/// error will indicate the valid range of values.
pub fn try_cast<T: PrimInt, V: PrimInt>(value: V) -> Result<T, OutOfBoundsError> {
const {
assert!(
mem::size_of::<T>() <= 8,
"Integer must be 64 bits or smaller"
);
}
NumCast::from(value).ok_or_else(|| {
let value = if V::min_value() != V::zero() {
LargeInt::Signed(NumCast::from(value).unwrap())
} else {
LargeInt::Unsigned(NumCast::from(value).unwrap())
};
let range = if T::min_value() != T::zero() {
LargeIntRange::Signed(IntRange::new::<T, _>(..).unwrap())
} else {
LargeIntRange::Unsigned(IntRange::new::<T, _>(..).unwrap())
};
OutOfBoundsError { value, range }
})
}
/// Check if a byte slice is all zeros.
pub fn is_zero(mut buf: &[u8]) -> bool {
while !buf.is_empty() {
@@ -115,27 +287,87 @@ where
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::*;
#[test]
fn test_int_range() {
let range = IntRange::new::<u8, _>(..).unwrap();
assert_eq!(range.start, IntBound::Included(u8::MIN));
assert_eq!(range.end, IntBound::Included(u8::MAX));
let range = IntRange::<i8>::new(-2i16..2i16).unwrap();
assert_eq!(range.start, IntBound::Included(-2));
assert_eq!(range.end, IntBound::Excluded(2));
assert!(IntRange::<u8>::new::<u16, _>(..).is_none());
}
#[test]
fn test_check_bounds() {
check_bounds(i64::MIN, ..).unwrap();
check_bounds(i64::MAX, ..).unwrap();
check_bounds(u64::MIN, ..).unwrap();
check_bounds(u64::MAX, ..).unwrap();
check_bounds(0, -1..=1).unwrap();
let err = check_bounds(i8::MAX, 0..=0).unwrap_err();
assert_matches!(err.value, LargeInt::Signed(127));
assert_matches!(
err.range,
LargeIntRange::Signed(IntRange {
start: IntBound::Included(0),
end: IntBound::Included(0),
})
);
let err = check_bounds(u8::MAX, 0..=0).unwrap_err();
assert_matches!(err.value, LargeInt::Unsigned(255));
assert_matches!(
err.range,
LargeIntRange::Unsigned(IntRange {
start: IntBound::Included(0),
end: IntBound::Included(0),
})
);
}
#[test]
fn test_try_cast() {
let value: u8 = try_cast(255u16).unwrap();
assert_eq!(value, 255);
let err = try_cast::<i8, _>(256u16).unwrap_err();
assert_matches!(err.value, LargeInt::Unsigned(256));
assert_matches!(
err.range,
LargeIntRange::Signed(IntRange {
start: IntBound::Included(-128),
end: IntBound::Included(127),
})
);
}
#[test]
fn test_ranges_overlaps() {
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
assert!(!ranges_overlaps(&[0..4], &(0..0)));
assert!(ranges_overlaps(&[0..4], &(0..4)));
assert!(ranges_overlaps(&[0..4], &(1..4)));
assert!(ranges_overlaps(&[0..4], &(0..3)));
assert!(!ranges_overlaps(&[0..4], &(4..5)));
assert!(ranges_overlaps(&[5..8], &(5..9)));
assert!(ranges_overlaps(&[5..8], &(4..8)));
assert!(ranges_overlaps(&[5..8], &(4..9)));
assert!(ranges_overlaps(&[0..4, 5..8], &(4..5)));
assert!(ranges_overlaps(&[0..4, 5..8], &(0..9)));
}
#[test]
fn test_ranges_contains() {
assert_eq!(ranges_contains(&[0..4], &0), true);
assert_eq!(ranges_contains(&[0..4], &4), false);
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
assert!(ranges_contains(&[0..4], &0));
assert!(!ranges_contains(&[0..4], &4));
assert!(!ranges_contains(&[0..4, 5..8], &4));
assert!(ranges_contains(&[0..4, 5..8], &6));
}
}
+24 -24
View File
@@ -118,7 +118,7 @@ fn round_trip_root_image() {
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
@@ -136,11 +136,11 @@ fn round_trip_root_image() {
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xc4, 0xa5, 0xda, 0x3e, 0x09, 0xa2, 0xc8, 0x70, 0xcb, 0xf0, 0x96, 0x79, 0x0e, 0x1e,
0x80, 0xae, 0x5e, 0x37, 0x81, 0x27, 0x24, 0xc3, 0x6c, 0xa9, 0x42, 0x9e, 0x2c, 0xb1,
0x81, 0xad, 0xce, 0xee, 0x8d, 0x4f, 0x76, 0x45, 0x54, 0xc1, 0x31, 0x6a, 0xa7, 0x81,
0x5c, 0x59, 0xa8, 0xe8, 0x76, 0xab, 0xed, 0x5b, 0x07, 0x07, 0x38, 0xdd, 0x09, 0x86,
0x05, 0x39, 0x23, 0x2d, 0x7b, 0xcc, 0x57, 0x06,
0x3b, 0x01, 0xf6, 0x04, 0x04, 0x6e, 0x6f, 0x60, 0x9c, 0xb0, 0x8b, 0x8a, 0x43, 0xf7,
0x91, 0x2e, 0xc4, 0x1b, 0xc0, 0x7f, 0xa1, 0xe4, 0xe6, 0x59, 0x14, 0x08, 0xbe, 0x83,
0xae, 0x0a, 0x0f, 0x0a, 0x4a, 0x15, 0x91, 0x0e, 0x4d, 0x18, 0x31, 0x48, 0x20, 0xe8,
0x44, 0x62, 0x07, 0x98, 0x43, 0x30, 0xee, 0x2d, 0x20, 0x28, 0xc3, 0x94, 0xc6, 0x0e,
0x86, 0xa3, 0xa7, 0x17, 0x36, 0xfd, 0x50, 0x7c,
],
);
@@ -186,7 +186,7 @@ fn round_trip_appended_hash_image() {
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
@@ -235,11 +235,11 @@ fn round_trip_appended_hash_image() {
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0x09, 0x98, 0x0c, 0x9d, 0x11, 0x50, 0xde, 0xb1, 0x55, 0x3b, 0x00, 0x76, 0xbe, 0x25,
0xfd, 0xe6, 0x46, 0x22, 0xbd, 0x9a, 0x05, 0x86, 0xea, 0x07, 0x4d, 0x8f, 0x7b, 0x15,
0x36, 0x20, 0x0d, 0xf0, 0x7e, 0x96, 0xd2, 0x58, 0xde, 0xf2, 0xa6, 0x91, 0x6d, 0x01,
0x7b, 0x03, 0x96, 0x70, 0xf8, 0x3b, 0x76, 0x74, 0xf0, 0xbf, 0x47, 0xe0, 0xd2, 0xd4,
0x5d, 0xbf, 0xb7, 0x9c, 0xf5, 0xf8, 0xaf, 0x3c,
0x91, 0x38, 0x61, 0xc0, 0x68, 0x2a, 0x8b, 0xd8, 0x01, 0xa6, 0xe4, 0x4c, 0x1d, 0x27,
0x93, 0x1b, 0xa4, 0x63, 0xd1, 0xbb, 0xf1, 0x64, 0x05, 0xf2, 0xa1, 0xa0, 0xb3, 0x35,
0xe1, 0xc5, 0xac, 0x4f, 0x98, 0xb3, 0x0a, 0xed, 0xfc, 0xee, 0xa2, 0x6a, 0x77, 0xf4,
0xe5, 0x69, 0xa0, 0xcd, 0x7a, 0xd1, 0xfe, 0x1d, 0x07, 0xd1, 0x25, 0xc6, 0x22, 0xe0,
0x25, 0xcb, 0xe9, 0x75, 0x50, 0xe4, 0xae, 0x59,
],
);
@@ -294,7 +294,7 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
@@ -343,11 +343,11 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xb5, 0x56, 0x65, 0x81, 0x5a, 0x16, 0x65, 0xa9, 0xa6, 0xc6, 0x9e, 0x41, 0x89, 0x9f,
0xe9, 0xbc, 0xea, 0x59, 0x4d, 0x14, 0x8a, 0x9e, 0x2b, 0x13, 0xa0, 0x3a, 0x8e, 0xd4,
0x59, 0xcd, 0x74, 0xe7, 0x99, 0xbd, 0xa3, 0x58, 0x4b, 0x84, 0xf2, 0x04, 0xe2, 0x12,
0x48, 0xfe, 0x4f, 0x67, 0x1f, 0x2a, 0xaa, 0x22, 0x51, 0x19, 0x83, 0x95, 0xa8, 0x03,
0xf5, 0x87, 0x12, 0x05, 0x8e, 0x14, 0xd9, 0xbd
0x92, 0xdd, 0x4d, 0xc5, 0xb0, 0x5b, 0x4f, 0x65, 0x97, 0x5a, 0x72, 0x66, 0xde, 0x82,
0xc2, 0x2f, 0x33, 0x86, 0x8b, 0x65, 0x67, 0x80, 0x1d, 0xca, 0xd6, 0x2c, 0xfc, 0xca,
0xaf, 0x4c, 0x56, 0x64, 0x3a, 0xd1, 0x06, 0x01, 0xda, 0x2e, 0x05, 0x67, 0xd1, 0x01,
0xe3, 0xcb, 0x7b, 0x1e, 0xeb, 0x05, 0x89, 0xeb, 0x80, 0xcc, 0x17, 0x0c, 0x24, 0x73,
0x0d, 0xcb, 0x36, 0xfa, 0x17, 0xbd, 0x20, 0x7e,
],
);
@@ -401,7 +401,7 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
@@ -450,11 +450,11 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xd6, 0x69, 0x19, 0x6a, 0x36, 0xc8, 0x1c, 0xe9, 0xc4, 0x85, 0xbe, 0xff, 0x43, 0xb1,
0x9f, 0xd4, 0x1d, 0x6c, 0xf9, 0xd2, 0xf3, 0xa6, 0x5f, 0x66, 0x41, 0xd5, 0xf3, 0xfd,
0x28, 0xdb, 0x14, 0x67, 0xc6, 0xa8, 0xef, 0xc4, 0xd4, 0x67, 0x6c, 0xb8, 0x66, 0xbb,
0x56, 0x5a, 0x4a, 0xf5, 0xd8, 0x92, 0x7c, 0x42, 0xbc, 0x47, 0xdb, 0x94, 0x38, 0x15,
0x4b, 0x2d, 0xd0, 0x28, 0x1f, 0xd1, 0x45, 0xa9,
0xcf, 0x6b, 0x90, 0xcf, 0x77, 0x76, 0x62, 0x12, 0xc2, 0x22, 0xe6, 0xd5, 0x5b, 0xab,
0x82, 0xd8, 0x6c, 0x93, 0xa3, 0x35, 0x5b, 0x77, 0xe0, 0x38, 0x12, 0x48, 0x90, 0x0c,
0xee, 0xbf, 0x95, 0x31, 0xff, 0xc7, 0xf5, 0xb9, 0x4f, 0x18, 0x57, 0x46, 0x37, 0xbb,
0xce, 0x7b, 0xa7, 0x26, 0x18, 0x5a, 0x3c, 0x41, 0xb2, 0x2e, 0xb7, 0x86, 0x51, 0xdc,
0xf6, 0x26, 0x86, 0xf3, 0xc7, 0x96, 0x23, 0xed,
],
);
+1 -2
View File
@@ -33,12 +33,11 @@ allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-3-Clause",
"CC0-1.0",
"GPL-3.0",
"ISC",
"MIT",
"OpenSSL",
"Unicode-DFS-2016",
"Unicode-3.0",
]
[[licenses.clarify]]
+3
View File
@@ -32,3 +32,6 @@ default-features = false
[features]
static = ["avbroot/static"]
[lints]
workspace = true
+2
View File
@@ -625,6 +625,7 @@ fn create_payload(
version: None,
merge_operations: vec![],
estimate_cow_size: cow_estimate,
estimate_op_count_max: None,
});
}
@@ -648,6 +649,7 @@ fn create_payload(
vabc_compression_param: profile.vabc_algo.map(|a| a.to_string()),
cow_version: Some(2),
vabc_feature_set: None,
compression_factor: None,
}),
partial_update: None,
apex_info: vec![],
+3
View File
@@ -16,3 +16,6 @@ publish = false
[target.'cfg(unix)'.dependencies]
avbroot = { path = "../avbroot" }
honggfuzz = "0.5.55"
[lints]
workspace = true
+3
View File
@@ -13,3 +13,6 @@ anyhow = "1.0.75"
clap = { version = "4.4.1", features = ["derive"] }
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
toml_edit = "0.22.9"
[lints]
workspace = true