mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dac4e8d328 | |||
| 3cc078ccfb | |||
| b47b51114f | |||
| 09048a00d2 | |||
| ca0e80521f | |||
| 6491106df0 | |||
| 2e8fa31207 | |||
| d69fdaadae | |||
| f0a631951a | |||
| 83265e584a | |||
| ddb8911efd | |||
| 72be84b2fc | |||
| 81d77d8610 | |||
| 4651b755ba | |||
| 979699e3dc | |||
| beee2c7c90 | |||
| 206916e544 | |||
| aaed44ad73 | |||
| 37b33f803a | |||
| e72b55dc57 | |||
| 5b01feb344 | |||
| 2f7556b530 | |||
| f25196f0a2 | |||
| 2735a6558e | |||
| 0c03fceb96 | |||
| 164274eb97 | |||
| 61129e8ec7 | |||
| e1f4fa1ee8 | |||
| d15a29e9a7 | |||
| 26a3bafc3d | |||
| 999e31cdf3 | |||
| ae2ebbf2aa | |||
| fea860a382 | |||
| 98339aee13 | |||
| f8772ef327 | |||
| 7d62ddc64e | |||
| d93378cc24 | |||
| 4885e978ee | |||
| ecb2caf47d | |||
| 525e2a409e | |||
| 7e89e9081a | |||
| 2a57343094 | |||
| cd779e62cb | |||
| 96fb8ddc9c | |||
| 0aba185c85 | |||
| 36dcbe092f | |||
| aab9f5216f | |||
| 2a2515c5ef | |||
| 111ec6ff1e | |||
| cfd1dfb2eb | |||
| d7977bab93 | |||
| c29d78181a | |||
| 42a9a9a12b | |||
| e293331952 | |||
| bf97444f4b | |||
| 1fa068b465 | |||
| f85b6eec46 | |||
| 07f2e30430 | |||
| 9f34198f07 | |||
| 9d8e82651e | |||
| 5b22d7b603 | |||
| 11d19de0a1 | |||
| fc4c2e63da | |||
| ee28a57a74 | |||
| 101512b894 | |||
| 50913db375 | |||
| 699ea92dbf | |||
| 667b99254c | |||
| e41814a425 | |||
| dff2fff028 | |||
| 67bca2e58c | |||
| 1f08cb6d40 | |||
| ed68f09cb9 | |||
| 64c11c2dc3 | |||
| 5115134b5a | |||
| a2d81d6bfe | |||
| c5709b90f6 | |||
| 2928b96f1f | |||
| 5638a8bb4d | |||
| a582d0b3e6 | |||
| 1d08ffa527 | |||
| a95ce56246 | |||
| ca6ce1fb37 | |||
| d7a46dcaaf | |||
| a4978aaac0 | |||
| 6ca4023b0d | |||
| acc3308d37 | |||
| 9ca3a690df | |||
| 335c04711e | |||
| fb7be77480 | |||
| e683fce4ab | |||
| 7493cdc5fa | |||
| 8f49f49154 | |||
| bc6fdd8513 | |||
| f6c86473c3 | |||
| d0d51f562c | |||
| f31c4134e4 | |||
| dd0d7f334b | |||
| e4a2a4c24c | |||
| cc9aab197c | |||
| 92da30f53c | |||
| 26142b0271 | |||
| c8df3286df | |||
| 65979beff4 | |||
| 47cf015f15 | |||
| e40e036159 | |||
| 0d2a158ead | |||
| ddabb19b4a | |||
| 3c05eb300c | |||
| 2ffb1dfdbd | |||
| f93dc55f70 | |||
| 393e65f6b4 | |||
| 3f86a67038 | |||
| 4337170f57 | |||
| a73365592f | |||
| 0018e8a5ba | |||
| adf0014da2 |
@@ -5,9 +5,10 @@ on:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs, but
|
||||
# only in pull requests.
|
||||
concurrency:
|
||||
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
|
||||
group: '${{ github.workflow }} @ ${{ github.head_ref || github.sha }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
name: cargo-deny
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
name: Modules
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
name: Github Release
|
||||
on:
|
||||
push:
|
||||
# Uncomment to test against a branch
|
||||
|
||||
+107
-3
@@ -1,15 +1,119 @@
|
||||
<!--
|
||||
When adding new changelog entries, use [Issue #0] to link to issues and
|
||||
[PR #0 @user] to link to pull requests. Then run:
|
||||
[PR #0] to link to pull requests. Then run:
|
||||
|
||||
cargo xtask update-changelog
|
||||
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 2.2.0
|
||||
|
||||
It's Android 14 release day! All versions of avbroot, including the old Python version, are compatible with Android 14 OTAs.
|
||||
|
||||
Changes:
|
||||
|
||||
* Add new unpack and pack commands for cpio archives (ramdisks) ([PR #173], [PR #178])
|
||||
* Rename `header.toml` to `boot.toml` for the boot image unpack and pack commands ([PR #175])
|
||||
* Also changes the file format a bit to make it more readable.
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Add streaming CPIO reader and writer ([PR #172])
|
||||
* Update dependencies ([PR #174], [PR #181])
|
||||
* Switch to prost for protobuf encoding/decoding ([PR #176])
|
||||
|
||||
### Version 2.1.1
|
||||
|
||||
This release is all about hardening avbroot against untrusted (or corrupted) inputs. While all of avbroot's parsers are memory-safe, it's still possible for crashes to occur due to logic issues like allocating too much memory or dividing by zero. With this release, most of these potential issues have been fixed and fuzz tests have been added to help find more of these situations.
|
||||
|
||||
On the filesystem side of things, it is no longer possible for a nefarious program to cause avbroot to write to unintended locations by eg. swapping out an output directory or temp directory with a symlink while it is running.
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Consolidate logic for handling `--pass-file` and `--pass-env-var` ([PR #156])
|
||||
* cargo-deny: Block executables in dependencies ([PR #133])
|
||||
* Implement size limits for parsers to prevent allocating too much memory ([Issue #157], [PR #158], [PR #159], [PR #164], [PR #168], [PR #169], [PR #170])
|
||||
* Add fuzzers to help catch panics/crashes ([Issue #160], [PR #161], [PR #162], [PR #163], [PR #165], [PR #167])
|
||||
* Use handle-based directory operations instead of path-based directory operations ([Issue #166], [PR #171])
|
||||
|
||||
### Version 2.1.0
|
||||
|
||||
* Add support for dm-verify FEC (forward error correction) ([Issue #145], [PR #146])
|
||||
* `ota verify` and `avb verify` will now check the FEC data.
|
||||
* Print status and warning messages to stderr ([PR #149])
|
||||
* Add new `avb unpack`, `avb pack`, and `avb repack` commands for AVB images ([Issue #144], [Issue #148], [PR #152])
|
||||
* `avb verify` now optionally accepts `--repair` to fix corrupted dm-verity images.
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Remove unnecessary use of `Arc` ([PR #147])
|
||||
* Use bstr crate to escape mostly UTF-8 binary data ([PR #150])
|
||||
* Improve error fields and error contest ([PR #153])
|
||||
|
||||
### Version 2.0.3
|
||||
|
||||
* Upgrade xz version in precompiled binaries ([Issue #138], [PR #139])
|
||||
* This fixes the `ota extract` and `ota verify` commands in some multithreaded situations.
|
||||
* Add `--version` option to print out avbroot's version ([Issue #138], [PR #140])
|
||||
|
||||
### Version 2.0.2
|
||||
|
||||
* Fix `data_offset` being set for payload operations that don't need it ([PR #136])
|
||||
* This fixes patched stock OnePlus images from being rejected when flashing.
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Move full OTA check to CLI functions to allow library functions to parse delta OTAs ([PR #135])
|
||||
* Remove unnecessary use of `anyhow` macro ([PR #137])
|
||||
|
||||
### Version 2.0.1
|
||||
|
||||
* Add support for Magisk 263xx ([PR #132])
|
||||
|
||||
### Version 2.0.0
|
||||
|
||||
* Initial Rust release. The old Python implementation can be found in the `python` branch. ([PR #130 @chenxiaolong])
|
||||
* Initial Rust release. The old Python implementation can be found in the `python` branch. ([PR #130])
|
||||
|
||||
<!-- Do not manually edit the lines below. Use `cargo xtask update-changelog` to regenerate. -->
|
||||
[PR #130 @chenxiaolong]: https://github.com/chenxiaolong/avbroot/pull/130
|
||||
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
|
||||
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
|
||||
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
|
||||
[Issue #148]: https://github.com/chenxiaolong/avbroot/issues/148
|
||||
[Issue #157]: https://github.com/chenxiaolong/avbroot/issues/157
|
||||
[Issue #160]: https://github.com/chenxiaolong/avbroot/issues/160
|
||||
[Issue #166]: https://github.com/chenxiaolong/avbroot/issues/166
|
||||
[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
|
||||
[PR #135]: https://github.com/chenxiaolong/avbroot/pull/135
|
||||
[PR #136]: https://github.com/chenxiaolong/avbroot/pull/136
|
||||
[PR #137]: https://github.com/chenxiaolong/avbroot/pull/137
|
||||
[PR #139]: https://github.com/chenxiaolong/avbroot/pull/139
|
||||
[PR #140]: https://github.com/chenxiaolong/avbroot/pull/140
|
||||
[PR #146]: https://github.com/chenxiaolong/avbroot/pull/146
|
||||
[PR #147]: https://github.com/chenxiaolong/avbroot/pull/147
|
||||
[PR #149]: https://github.com/chenxiaolong/avbroot/pull/149
|
||||
[PR #150]: https://github.com/chenxiaolong/avbroot/pull/150
|
||||
[PR #152]: https://github.com/chenxiaolong/avbroot/pull/152
|
||||
[PR #153]: https://github.com/chenxiaolong/avbroot/pull/153
|
||||
[PR #156]: https://github.com/chenxiaolong/avbroot/pull/156
|
||||
[PR #158]: https://github.com/chenxiaolong/avbroot/pull/158
|
||||
[PR #159]: https://github.com/chenxiaolong/avbroot/pull/159
|
||||
[PR #161]: https://github.com/chenxiaolong/avbroot/pull/161
|
||||
[PR #162]: https://github.com/chenxiaolong/avbroot/pull/162
|
||||
[PR #163]: https://github.com/chenxiaolong/avbroot/pull/163
|
||||
[PR #164]: https://github.com/chenxiaolong/avbroot/pull/164
|
||||
[PR #165]: https://github.com/chenxiaolong/avbroot/pull/165
|
||||
[PR #167]: https://github.com/chenxiaolong/avbroot/pull/167
|
||||
[PR #168]: https://github.com/chenxiaolong/avbroot/pull/168
|
||||
[PR #169]: https://github.com/chenxiaolong/avbroot/pull/169
|
||||
[PR #170]: https://github.com/chenxiaolong/avbroot/pull/170
|
||||
[PR #171]: https://github.com/chenxiaolong/avbroot/pull/171
|
||||
[PR #172]: https://github.com/chenxiaolong/avbroot/pull/172
|
||||
[PR #173]: https://github.com/chenxiaolong/avbroot/pull/173
|
||||
[PR #174]: https://github.com/chenxiaolong/avbroot/pull/174
|
||||
[PR #175]: https://github.com/chenxiaolong/avbroot/pull/175
|
||||
[PR #176]: https://github.com/chenxiaolong/avbroot/pull/176
|
||||
[PR #178]: https://github.com/chenxiaolong/avbroot/pull/178
|
||||
[PR #181]: https://github.com/chenxiaolong/avbroot/pull/181
|
||||
|
||||
Generated
+560
-697
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,10 +1,10 @@
|
||||
[workspace]
|
||||
default-members = ["avbroot"]
|
||||
members = ["avbroot", "e2e", "xtask"]
|
||||
members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "2.0.0"
|
||||
version = "2.2.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
+124
-13
@@ -6,10 +6,44 @@ Note that while avbroot maintains a stable command line interface for the patchi
|
||||
|
||||
## `avbroot avb`
|
||||
|
||||
### Unpacking an AVB image
|
||||
|
||||
```bash
|
||||
avbroot avb unpack -i <input AVB image>
|
||||
```
|
||||
|
||||
This subcommand unpacks the vbmeta header and footer into `avb.toml`. If a footer is present, then the corresponding raw partition image is extracted into `raw.img`. Root vbmeta images (eg. `vbmeta` and `vbmeta_vendor`) do not have footers, while appended vbmeta images (eg. `boot` and `system`) do.
|
||||
|
||||
The vbmeta descriptor digests are validated during unpacking. For dm-verity images, if the image is corrupt, avbroot will attempt to use the FEC data to repair the file. If there is unrepairable data corruption, the command will fail, though the corrupted `raw.img` will still be fully written. If, for whatever reason, a successful exit status of 0 is needed even for corrupted files, use `--ignore-invalid`.
|
||||
|
||||
### Packing an AVB image
|
||||
|
||||
```bash
|
||||
avbroot avb pack -o <output AVB image> [--key <AVB private key>]
|
||||
```
|
||||
|
||||
This subcommand packs a new AVB image from the `avb.toml` file and, for appended vbmeta images, the `raw.img` file.
|
||||
|
||||
* If the original image was signed and the new data is unmodified, then the original signature is used as-is. (This means just unpacking and packing an image will always result in a byte-for-byte identical file.)
|
||||
* If the original image was signed and the new data is modified, then the newly packed image will be signed with the `--key`.
|
||||
* If the original image was not signed, then the newly packed image is not signed.
|
||||
* To force an image to be signed, use `--key <path> --force`.
|
||||
* To force an image to be unsigned, use `--force` without specifying `--key`.
|
||||
|
||||
### Repacking an AVB image
|
||||
|
||||
```bash
|
||||
avbroot avb repack -i <input AVB image> -o <output AVB image>
|
||||
```
|
||||
|
||||
This subcommand is equivalent to `avbroot avb unpack` followed by `avbroot avb pack`, except it doesn't need to write any intermediate files to disk.
|
||||
|
||||
This is useful for repairing a dm-verify image or for re-signing any image with a specific key.
|
||||
|
||||
### Showing vbmeta header and footer information
|
||||
|
||||
```bash
|
||||
avbroot avb dump -i <image>
|
||||
avbroot avb info -i <image>
|
||||
```
|
||||
|
||||
This subcommand shows all of the vbmeta header and footer fields. `vbmeta` partition images will only have a header, while partitions with actual data (eg. boot images) will have both a header and a footer.
|
||||
@@ -20,10 +54,12 @@ This subcommand shows all of the vbmeta header and footer fields. `vbmeta` parti
|
||||
avbroot avb verify -i <root vbmeta image> -p <public key>
|
||||
```
|
||||
|
||||
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hashtree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
|
||||
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
|
||||
|
||||
If `-p` is omitted, the signatures and hashes are checked only for validity, not that they are trusted.
|
||||
|
||||
By default, this command will not write to any file and fails if an image is corrupt or invalid. To attempt to repair corrupted dm-verity images, pass in `--repair`.
|
||||
|
||||
## `avbroot boot`
|
||||
|
||||
### Unpacking a boot image
|
||||
@@ -32,7 +68,7 @@ If `-p` is omitted, the signatures and hashes are checked only for validity, not
|
||||
avbroot boot unpack -i <input boot image>
|
||||
```
|
||||
|
||||
This subcommand unpacks all of the components of the boot image into the current directory by default (see `--help`). The header fields are saved to `header.toml` and each blob section is saved to a separate file. Each blob is written to disk as-is, without decompression.
|
||||
This subcommand unpacks all of the components of the boot image into the current directory by default (see `--help`). The header fields are saved to `boot.toml` and each blob section is saved to a separate file. Each blob is written to disk as-is, without decompression.
|
||||
|
||||
### Packing a boot image
|
||||
|
||||
@@ -58,26 +94,101 @@ avbroot boot info -i <input boot image>
|
||||
|
||||
All of the `boot` subcommands show the boot image information. This specific subcommand just does it without performing any other operation. To show avbroot's internal representation of the information, pass in `-d`.
|
||||
|
||||
## `avbroot ramdisk`
|
||||
## `avbroot cpio`
|
||||
|
||||
### Dumping a cpio archive
|
||||
### Unpacking a cpio archive
|
||||
|
||||
```bash
|
||||
avbroot ramdisk dump -i <cpio archive>
|
||||
avbroot cpio unpack -i <input cpio archive>
|
||||
```
|
||||
|
||||
This subcommand dumps all information about a cpio archive to stdout. This includes the compression format, all header fields (including the trailer entry), and all data. If an entry's data can be decoded as UTF-8, then it is printed out as text. Otherwise, the binary data is printed out `\x##`-encoded for non-ASCII bytes. The escape-encoded data is truncated to 512 bytes by default to avoid outputting too much data, but this behavior can be disabled with `--no-truncate`.
|
||||
This subcommand unpacks the cpio archive. The list of file entries and their metadata, like permissions, are written to `cpio.toml`. The contents of regular files are extracted to `cpio_tree/`. Other file types, like symlinks and block devices, are not extracted at all. Their information only exists in the TOML file.
|
||||
|
||||
The files inside the tree will have default permissions, ownership, and modification timestamps. This metadata exists only inside the TOML file in order to ensure that the behavior is the same across all platforms.
|
||||
|
||||
Both uncompressed archives and compressed archives (gzip or legacy lz4) are supported.
|
||||
|
||||
### Packing a cpio archive
|
||||
|
||||
```bash
|
||||
avbroot cpio pack -o <output cpio archive>
|
||||
```
|
||||
|
||||
This subcommand packs a new cpio archive from the file entries listed in `cpio.toml` and the file contents in `cpio_tree/`. Note that **only entries listed in the TOML file are packed**. Extra files inside the tree are silently ignored.
|
||||
|
||||
The files are packed in the order listed in the TOML file. When packing ramdisks specifically, it's important to ensure that files are listed after their parent directories (which also **must** exist). Otherwise, the kernel will ignore them. As long as the file paths don't contain anything weird (eg. `a//b` or `a/./b`), sorting the entires with `--sort` should do the trick.
|
||||
|
||||
The new archive will be written in the same format (compressed or uncompressed) as the original archive.
|
||||
|
||||
### Repacking a cpio archive
|
||||
|
||||
```bash
|
||||
avbroot ramdisk repack -i <input cpio archive> -o <output cpio archive>
|
||||
avbroot cpio repack -i <input cpio archive> -o <output cpio archive>
|
||||
```
|
||||
|
||||
This subcommand repacks a cpio archive, including recompression if needed. This is useful for roundtrip testing of avbroot's cpio parser and compression handling. The uncompressed output should be identical to the uncompressed input, except:
|
||||
This is almost equivalent to running `avbroot cpio unpack` followed by `avbroot cpio pack`, except inode numbers will not be reassigned.
|
||||
|
||||
* files are sorted by name
|
||||
* inodes are reassigned, starting from 300000
|
||||
* there is no excess padding at the end of the file
|
||||
### Showing information about a cpio archive
|
||||
|
||||
The compressed output may differ from what other tools produce due to differences in compression levels and header metadata. avbroot avoids specifying header information where possible (eg. gzip timestamp) for reproducibility.
|
||||
```bash
|
||||
avbroot cpio info -i <input cpio archive>
|
||||
```
|
||||
|
||||
All of the `cpio` subcommands show details about all the entries in the archive. This specific subcommand just does it without performing any other operation.
|
||||
|
||||
## `avbroot fec`
|
||||
|
||||
This set of commands is for working with dm-verity FEC (forward error correction) data. The FEC data allows small errors in partition data to be corrected. This increases reliability of the system because when dm-verity encounters data that doesn't match the expected checksum, it will either trigger a kernel panic or reboot the system.
|
||||
|
||||
The same raw FEC data can be stored in several ways:
|
||||
|
||||
* cryptsetup's `veritysetup` does not use any file format at all. It must be told the FEC location and parameters using the `--fec-*` options.
|
||||
* AOSP's AVB 2.0 stores the FEC data inside the partition as `[Partition data][Hash tree][FEC data]`. The location and parameters are stored in the vbmeta hash tree descriptors.
|
||||
* AOSP's `fec` tool stores the FEC data in a standalone file with a header containing the FEC parameters.
|
||||
|
||||
The `avbroot fec` commands use AOSP's standalone FEC file format.
|
||||
|
||||
The FEC data is not generated from a sequential read of the input file, but rather from an interleaved read. If the input file's offsets are visualized as a 2D table:
|
||||
|
||||
```
|
||||
| 0 1 2 3 ... 4095 |
|
||||
| 4096 4097 4098 4099 ... 8191 |
|
||||
| 8192 8193 8194 8195 ... 12287 |
|
||||
| .... .... .... .... ... ..... |
|
||||
```
|
||||
|
||||
then the file access pattern can be thought of as being column-by-column instead of row-by-row.
|
||||
|
||||
Data correction happens at the codeword level. A Reed-Solomon codeword is 255 bytes where some portion is file data and the rest is parity data. AOSP and avbroot both default to 253 bytes of data and 2 bytes of parity information. Each column in the table represents the 253-byte data portion of the codeword. Larger files have more columns.
|
||||
|
||||
A contiguous sequence of corrupted data will span multiple columns. Since error correction happens at the column level, this interleaving increases the chances of recovery. For more details about the specifics, see the implementation in [`fec.rs`](./avbroot/src/format/fec.rs).
|
||||
|
||||
### Generating FEC data
|
||||
|
||||
```bash
|
||||
avbroot fec generate -i <input data file> -f <output FEC file>
|
||||
```
|
||||
|
||||
The default behavior is to use 2 bytes of parity information per 253 bytes of input data. Within each 253-byte column described above, this is sufficient for correcting a single corrupted byte in the column (`⌊parity / 2⌋` bytes in general).
|
||||
|
||||
The number of parity bytes (between 2 and 24, inclusive) can be configured using `--parity`.
|
||||
|
||||
### Verifying a file
|
||||
|
||||
```bash
|
||||
avbroot fec verify -i <input data file> -f <input FEC file>
|
||||
```
|
||||
|
||||
This will check if the input file has any corrupted bytes. This command runs significantly faster than `avbroot fec repair` and is useful if only detection of corrupted data is needed.
|
||||
|
||||
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, there can be false positives where the corrupted data is reported as being valid.
|
||||
|
||||
### Repairing a file
|
||||
|
||||
```bash
|
||||
avbroot fec repair -i <input/output data file> -f <input FEC file>
|
||||
```
|
||||
|
||||
This will repair the file in place. As described above, in each column, up to `parity / 2` bytes can be corrected.
|
||||
|
||||
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, the file can potentially be "successfully repaired" to some incorrect data.
|
||||
|
||||
@@ -64,6 +64,8 @@ The boot-related components are signed with an AVB key and OTA-related component
|
||||
|
||||
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and X509 certificate (eg. like those generated by openssl).
|
||||
|
||||
If you lose your AVB or OTA signing key, you will no longer be able to sign new OTA zips. You will have to generate new signing keys and unlock your bootloader again (including a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Make sure the caveats listed above are understood. It is possible to hard brick by doing the wrong thing!
|
||||
@@ -153,6 +155,8 @@ Unpatched OTA updates are already blocked in recovery because the original OTA c
|
||||
|
||||
The `clearotacerts` module additionally makes A/B OTAs fail while booted into Android to prevent accidental manual updates. The module simply overrides `/system/etc/security/otacerts.zip` at runtime with an empty zip so that even if an OTA is downloaded, signature verification will fail.
|
||||
|
||||
At least in CalyxOS, the Updater app does not respect the `Automatic system updates` setting and may enter an infinite loop downloading the OTA update and restarting the download when signature verification fails. If this happens on your ROM, you can try to either remove network access from the Updater app or disable the Updater app altogether (if your ROM allows you to do so). In CalyxOS, it is possible to go to `Settings > Apps > See all apps`, open the three-dot menu, `Show system`, then find the `System updater` app and disable it.
|
||||
|
||||
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
|
||||
|
||||
### `oemunlockonboot`: Enable OEM unlocking on every boot
|
||||
@@ -185,7 +189,7 @@ Magisk versions 25211 and newer require a writable partition for storing custom
|
||||
Alternatively, avbroot can print out what Magisk detected by running:
|
||||
|
||||
```bash
|
||||
avbroot ota magisk-info \
|
||||
avbroot boot magisk-info \
|
||||
--image magisk_patched-*.img
|
||||
```
|
||||
|
||||
|
||||
+18
-7
@@ -11,21 +11,25 @@ publish = false
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
base64 = "0.21.3"
|
||||
bstr = "1.6.2"
|
||||
byteorder = "1.4.3"
|
||||
cap-std = "2.0.0"
|
||||
cap-tempfile = "2.0.0"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
clap_complete = "4.4.0"
|
||||
cms = { version = "0.2.2", features = ["std"] }
|
||||
const-oid = "0.9.5"
|
||||
ctrlc = "3.4.0"
|
||||
flate2 = "1.0.27"
|
||||
hex = "0.4.3"
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
lz4_flex = "0.11.1"
|
||||
memchr = "2.6.0"
|
||||
num-bigint-dig = "0.8.4"
|
||||
num-traits = "0.2.16"
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
|
||||
quick-protobuf = "0.8.1"
|
||||
prost = "0.12.1"
|
||||
rand = "0.8.5"
|
||||
rayon = "1.7.0"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
@@ -33,7 +37,7 @@ regex = { version = "1.9.4", default-features = false, features = ["perf", "std"
|
||||
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
|
||||
# instructions. sha2 is still used for signing purposes.
|
||||
# https://github.com/RustCrypto/hashes/issues/327
|
||||
ring = "0.16.20"
|
||||
ring = "0.17.0"
|
||||
rpassword = "7.2.0"
|
||||
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
@@ -41,10 +45,9 @@ sha1 = "0.10.5"
|
||||
sha2 = "0.10.7"
|
||||
tempfile = "3.8.0"
|
||||
thiserror = "1.0.47"
|
||||
toml_edit = { version = "0.19.14", features = ["serde"] }
|
||||
toml_edit = { version = "0.20.1", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
xz2 = "0.1.7"
|
||||
|
||||
# There's an upstream bug that causes an infinite loop in the write::BzDecoder
|
||||
# destructor if the decoder is fed invalid data. While this never happens during
|
||||
@@ -55,6 +58,14 @@ xz2 = "0.1.7"
|
||||
git = "https://github.com/jongiddy/bzip2-rs"
|
||||
rev = "2aefcb4d3634de1df226c73d93f758d65228bb8c"
|
||||
|
||||
# The upstream xz2 crate uses an old version of liblzma when compiling with the
|
||||
# `static` feature and doesn't enable all of the encoders and decoders. This
|
||||
# causes certain payload data to fail to decompress.
|
||||
# https://github.com/chenxiaolong/avbroot/issues/138
|
||||
[dependencies.xz2]
|
||||
git = "https://github.com/chenxiaolong/xz2-rs"
|
||||
rev = "fe2050b9c3395db15d8610f1dabb505440c1a556"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
@@ -66,8 +77,8 @@ features = ["deflate"]
|
||||
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
|
||||
|
||||
[build-dependencies]
|
||||
# Disable the clap feature since it pulls in an ancient version of clap.
|
||||
pb-rs = { version = "0.10.0", default-features = false }
|
||||
prost-build = "0.12.1"
|
||||
protox = "0.5.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
+5
-11
@@ -5,8 +5,6 @@
|
||||
|
||||
use std::{env, ffi::OsStr, fs, io, path::Path};
|
||||
|
||||
use pb_rs::{types::FileDescriptor, ConfigBuilder};
|
||||
|
||||
fn main() {
|
||||
let out_dir = Path::new(&env::var("OUT_DIR").unwrap()).join("protobuf");
|
||||
let in_dir = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("protobuf");
|
||||
@@ -30,14 +28,10 @@ fn main() {
|
||||
|
||||
fs::create_dir_all(&out_dir).unwrap();
|
||||
|
||||
let config = ConfigBuilder::new(&protos, None, Some(&out_dir), &[in_dir])
|
||||
.unwrap()
|
||||
.dont_use_cow(true)
|
||||
// We're using this as a means to force quick-protobuf to use BTreeMap
|
||||
// instead of HashMap so that the serialized messages are reproducible.
|
||||
// https://github.com/tafia/quick-protobuf/issues/251
|
||||
.nostd(true)
|
||||
.build();
|
||||
let file_descriptors = protox::compile(&protos, [&in_dir]).unwrap();
|
||||
|
||||
FileDescriptor::run(&config).unwrap();
|
||||
prost_build::Config::new()
|
||||
.btree_map(["."])
|
||||
.compile_fds(file_descriptors)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
+90
-75
@@ -11,9 +11,10 @@ use std::{
|
||||
num::ParseIntError,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use regex::bytes::Regex;
|
||||
use ring::digest::Context;
|
||||
use rsa::RsaPrivateKey;
|
||||
@@ -28,22 +29,21 @@ use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, Z
|
||||
use crate::{
|
||||
crypto,
|
||||
format::{
|
||||
avb::{self, AlgorithmType, Descriptor},
|
||||
avb::{self, Descriptor},
|
||||
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
|
||||
compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntryNew},
|
||||
cpio::{self, CpioEntry, CpioEntryData},
|
||||
},
|
||||
stream::{self, FromReader, HashingWriter, SectionReader, ToWriter},
|
||||
util::EscapedString,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Boot image has no vbmeta footer")]
|
||||
NoFooter,
|
||||
#[error("No hash descriptor found in vbmeta footer")]
|
||||
#[error("No hash descriptor found in vbmeta header")]
|
||||
NoHashDescriptor,
|
||||
#[error("Found multiple hash descriptors in vbmeta footer")]
|
||||
#[error("Found multiple hash descriptors in vbmeta header")]
|
||||
MultipleHashDescriptors,
|
||||
#[error("Validation error: {0}")]
|
||||
Validation(String),
|
||||
@@ -66,30 +66,39 @@ pub enum Error {
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] io::Error),
|
||||
Io(#[from] io::Error),
|
||||
#[error("File I/O error")]
|
||||
File(PathBuf, #[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
fn load_ramdisk(data: &[u8]) -> Result<(Vec<CpioEntryNew>, CompressedFormat)> {
|
||||
fn load_ramdisk(
|
||||
data: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<CpioEntry>, CompressedFormat)> {
|
||||
let raw_reader = Cursor::new(data);
|
||||
let mut reader = CompressedReader::new(raw_reader, false)?;
|
||||
let entries = cpio::load(&mut reader, false)?;
|
||||
let entries = cpio::load(&mut reader, false, cancel_signal)?;
|
||||
|
||||
Ok((entries, reader.format()))
|
||||
}
|
||||
|
||||
fn save_ramdisk(entries: &[CpioEntryNew], format: CompressedFormat) -> Result<Vec<u8>> {
|
||||
fn save_ramdisk(
|
||||
entries: &[CpioEntry],
|
||||
format: CompressedFormat,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let mut writer = CompressedWriter::new(raw_writer, format)?;
|
||||
cpio::save(&mut writer, entries, false)?;
|
||||
cpio::save(&mut writer, entries, false, cancel_signal)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
pub trait BootImagePatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &Arc<AtomicBool>) -> Result<()>;
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()>;
|
||||
}
|
||||
|
||||
/// Root a boot image with Magisk.
|
||||
@@ -108,7 +117,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: &[Range<u32>] = &[25102..25207, 25211..26300];
|
||||
const VERS_SUPPORTED: &[Range<u32>] = &[25102..25207, 25211..26400];
|
||||
const VER_PREINIT_DEVICE: Range<u32> =
|
||||
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
@@ -161,7 +170,7 @@ impl MagiskRootPatcher {
|
||||
}
|
||||
|
||||
fn get_version(path: &Path) -> Result<u32> {
|
||||
let reader = File::open(path)?;
|
||||
let reader = File::open(path).map_err(|e| Error::File(path.to_owned(), e))?;
|
||||
let reader = BufReader::new(reader);
|
||||
let mut zip = ZipArchive::new(reader)?;
|
||||
let entry = zip.by_name("assets/util_functions.sh")?;
|
||||
@@ -191,7 +200,7 @@ impl MagiskRootPatcher {
|
||||
/// entries as `.backup/<path>`.
|
||||
///
|
||||
/// Both lists and entries within the lists may be mutated.
|
||||
fn apply_magisk_backup(old_entries: &mut [CpioEntryNew], new_entries: &mut Vec<CpioEntryNew>) {
|
||||
fn apply_magisk_backup(old_entries: &mut [CpioEntry], new_entries: &mut Vec<CpioEntry>) {
|
||||
cpio::sort(old_entries);
|
||||
cpio::sort(new_entries);
|
||||
|
||||
@@ -203,20 +212,20 @@ impl MagiskRootPatcher {
|
||||
|
||||
loop {
|
||||
match (old_iter.peek(), new_iter.peek()) {
|
||||
(Some(&old), Some(&new)) => match old.name.cmp(&new.name) {
|
||||
(Some(&old), Some(&new)) => match old.path.cmp(&new.path) {
|
||||
Ordering::Less => {
|
||||
to_back_up.push(old);
|
||||
old_iter.next();
|
||||
}
|
||||
Ordering::Equal => {
|
||||
if old.content != new.content {
|
||||
if old.data != new.data {
|
||||
to_back_up.push(old);
|
||||
}
|
||||
old_iter.next();
|
||||
new_iter.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
rm_list.extend(&new.name);
|
||||
rm_list.extend(&new.path);
|
||||
rm_list.push(b'\0');
|
||||
new_iter.next();
|
||||
}
|
||||
@@ -226,7 +235,7 @@ impl MagiskRootPatcher {
|
||||
old_iter.next();
|
||||
}
|
||||
(None, Some(new)) => {
|
||||
rm_list.extend(&new.name);
|
||||
rm_list.extend(&new.path);
|
||||
rm_list.push(b'\0');
|
||||
new_iter.next();
|
||||
}
|
||||
@@ -234,28 +243,27 @@ impl MagiskRootPatcher {
|
||||
}
|
||||
}
|
||||
|
||||
// Intentially using 000 permissions to match Magisk.
|
||||
new_entries.push(CpioEntryNew::new_directory(b".backup"));
|
||||
new_entries.push(CpioEntry::new_directory(b".backup", 0));
|
||||
|
||||
for old_entry in to_back_up {
|
||||
let mut new_entry = old_entry.clone();
|
||||
new_entry.name = b".backup/".to_vec();
|
||||
new_entry.name.extend(&old_entry.name);
|
||||
new_entry.path = b".backup/".to_vec();
|
||||
new_entry.path.extend(&old_entry.path);
|
||||
new_entries.push(new_entry);
|
||||
}
|
||||
|
||||
{
|
||||
// Intentially using 000 permissions to match Magisk.
|
||||
let mut entry = CpioEntryNew::new_file(b".backup/.rmlist");
|
||||
entry.content = rm_list;
|
||||
new_entries.push(entry);
|
||||
}
|
||||
new_entries.push(CpioEntry::new_file(
|
||||
b".backup/.rmlist",
|
||||
0,
|
||||
CpioEntryData::Data(rm_list),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for MagiskRootPatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
let zip_reader = File::open(&self.apk_path)?;
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let zip_reader =
|
||||
File::open(&self.apk_path).map_err(|e| Error::File(self.apk_path.clone(), e))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(zip_reader))?;
|
||||
|
||||
// Load the first ramdisk. If it doesn't exist, we have to generate one
|
||||
@@ -266,7 +274,7 @@ impl BootImagePatcher for MagiskRootPatcher {
|
||||
BootImage::VendorV3Through4(b) => b.ramdisks.first(),
|
||||
};
|
||||
let (mut entries, ramdisk_format) = match ramdisk {
|
||||
Some(r) if !r.is_empty() => load_ramdisk(r)?,
|
||||
Some(r) if !r.is_empty() => load_ramdisk(r, cancel_signal)?,
|
||||
_ => (vec![], CompressedFormat::Lz4Legacy),
|
||||
};
|
||||
|
||||
@@ -277,13 +285,11 @@ impl BootImagePatcher for MagiskRootPatcher {
|
||||
(b"overlay.d".as_slice(), 0o750),
|
||||
(b"overlay.d/sbin".as_slice(), 0o750),
|
||||
] {
|
||||
let mut entry = CpioEntryNew::new_directory(path);
|
||||
entry.mode |= perms;
|
||||
entries.push(entry);
|
||||
entries.push(CpioEntry::new_directory(path, perms));
|
||||
}
|
||||
|
||||
// Delete the original init.
|
||||
entries.retain(|e| e.name != b"init");
|
||||
entries.retain(|e| e.path != b"init");
|
||||
|
||||
// Add magiskinit.
|
||||
{
|
||||
@@ -291,10 +297,11 @@ impl BootImagePatcher for MagiskRootPatcher {
|
||||
let mut data = vec![];
|
||||
zip_entry.read_to_end(&mut data)?;
|
||||
|
||||
let mut entry = CpioEntryNew::new_file(b"init");
|
||||
entry.mode |= 0o750;
|
||||
entry.content = data;
|
||||
entries.push(entry);
|
||||
entries.push(CpioEntry::new_file(
|
||||
b"init",
|
||||
0o750,
|
||||
CpioEntryData::Data(data),
|
||||
));
|
||||
}
|
||||
|
||||
// Add xz-compressed magisk32 and magisk64.
|
||||
@@ -323,10 +330,12 @@ impl BootImagePatcher for MagiskRootPatcher {
|
||||
stream::copy(reader, &mut writer, cancel_signal)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
let mut entry = CpioEntryNew::new_file(target);
|
||||
entry.mode |= 0o644;
|
||||
entry.content = raw_writer.into_inner();
|
||||
entries.push(entry);
|
||||
|
||||
entries.push(CpioEntry::new_file(
|
||||
target,
|
||||
0o644,
|
||||
CpioEntryData::Data(raw_writer.into_inner()),
|
||||
));
|
||||
}
|
||||
|
||||
// Create Magisk .backup directory structure.
|
||||
@@ -356,17 +365,16 @@ impl BootImagePatcher for MagiskRootPatcher {
|
||||
magisk_config.push_str(&format!("RANDOMSEED={:#x}\n", self.random_seed));
|
||||
}
|
||||
|
||||
{
|
||||
// Intentially using 000 permissions to match Magisk.
|
||||
let mut entry = CpioEntryNew::new_file(b".backup/.magisk");
|
||||
entry.content = magisk_config.into_bytes();
|
||||
entries.push(entry);
|
||||
}
|
||||
entries.push(CpioEntry::new_file(
|
||||
b".backup/.magisk",
|
||||
0,
|
||||
CpioEntryData::Data(magisk_config.into_bytes()),
|
||||
));
|
||||
|
||||
// Repack ramdisk.
|
||||
cpio::sort(&mut entries);
|
||||
cpio::reassign_inodes(&mut entries);
|
||||
let new_ramdisk = save_ramdisk(&entries, ramdisk_format)?;
|
||||
cpio::assign_inodes(&mut entries, false)?;
|
||||
let new_ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
|
||||
match boot_image {
|
||||
BootImage::V0Through2(b) => b.ramdisk = new_ramdisk,
|
||||
@@ -405,7 +413,10 @@ impl OtaCertPatcher {
|
||||
Self { cert }
|
||||
}
|
||||
|
||||
pub fn get_certificates(boot_image: &BootImage) -> Result<Vec<Certificate>> {
|
||||
pub fn get_certificates(
|
||||
boot_image: &BootImage,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<Certificate>> {
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
match boot_image {
|
||||
@@ -417,12 +428,15 @@ impl OtaCertPatcher {
|
||||
let mut certificates = vec![];
|
||||
|
||||
for ramdisk in ramdisks {
|
||||
let (entries, _) = load_ramdisk(ramdisk)?;
|
||||
let Some(entry) = entries.iter().find(|e| e.name == Self::OTACERTS_PATH) else {
|
||||
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
|
||||
let Some(entry) = entries.iter().find(|e| e.path == Self::OTACERTS_PATH) else {
|
||||
continue;
|
||||
};
|
||||
let CpioEntryData::Data(data) = &entry.data else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut zip = ZipArchive::new(Cursor::new(&entry.content))?;
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data))?;
|
||||
|
||||
for index in 0..zip.len() {
|
||||
let zip_entry = zip.by_index(index)?;
|
||||
@@ -438,16 +452,16 @@ impl OtaCertPatcher {
|
||||
Ok(certificates)
|
||||
}
|
||||
|
||||
fn patch_ramdisk(&self, data: &mut Vec<u8>) -> Result<bool> {
|
||||
let (mut entries, ramdisk_format) = load_ramdisk(data)?;
|
||||
let Some(entry) = entries.iter_mut().find(|e| e.name == Self::OTACERTS_PATH) else {
|
||||
fn patch_ramdisk(&self, data: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
|
||||
let (mut entries, ramdisk_format) = load_ramdisk(data, cancel_signal)?;
|
||||
let Some(entry) = entries.iter_mut().find(|e| e.path == Self::OTACERTS_PATH) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
// Create a new otacerts archive. The old certs are ignored since
|
||||
// flashing a stock OTA will render the device unbootable.
|
||||
{
|
||||
let raw_writer = Cursor::new(vec![]);
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = ZipWriter::new(raw_writer);
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
@@ -455,26 +469,26 @@ impl OtaCertPatcher {
|
||||
crypto::write_pem_cert(&mut writer, &self.cert)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
entry.content = raw_writer.into_inner();
|
||||
entry.data = CpioEntryData::Data(raw_writer.into_inner());
|
||||
}
|
||||
|
||||
// Repack ramdisk.
|
||||
*data = save_ramdisk(&entries, ramdisk_format)?;
|
||||
*data = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for OtaCertPatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let patched_any = match boot_image {
|
||||
BootImage::V0Through2(b) => self.patch_ramdisk(&mut b.ramdisk)?,
|
||||
BootImage::V3Through4(b) => self.patch_ramdisk(&mut b.ramdisk)?,
|
||||
BootImage::V0Through2(b) => self.patch_ramdisk(&mut b.ramdisk, cancel_signal)?,
|
||||
BootImage::V3Through4(b) => self.patch_ramdisk(&mut b.ramdisk, cancel_signal)?,
|
||||
BootImage::VendorV3Through4(b) => {
|
||||
let mut patched = false;
|
||||
|
||||
for ramdisk in &mut b.ramdisks {
|
||||
if self.patch_ramdisk(ramdisk)? {
|
||||
if self.patch_ramdisk(ramdisk, cancel_signal)? {
|
||||
patched = true;
|
||||
break;
|
||||
}
|
||||
@@ -488,8 +502,8 @@ impl BootImagePatcher for OtaCertPatcher {
|
||||
// out of future updates if the OTA certificate mechanism has changed.
|
||||
if !patched_any {
|
||||
return Err(Error::Validation(format!(
|
||||
"No ramdisk contains {}",
|
||||
EscapedString::new(Self::OTACERTS_PATH),
|
||||
"No ramdisk contains {:?}",
|
||||
Self::OTACERTS_PATH.as_bstr(),
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -557,9 +571,10 @@ impl PrepatchedImagePatcher {
|
||||
}
|
||||
|
||||
impl BootImagePatcher for PrepatchedImagePatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let prepatched_image = {
|
||||
let raw_reader = File::open(&self.prepatched)?;
|
||||
let raw_reader = File::open(&self.prepatched)
|
||||
.map_err(|e| Error::File(self.prepatched.clone(), e))?;
|
||||
BootImage::from_reader(BufReader::new(raw_reader))?
|
||||
};
|
||||
|
||||
@@ -735,10 +750,10 @@ pub fn patch_boot(
|
||||
writer: impl Write + Seek,
|
||||
key: &RsaPrivateKey,
|
||||
patchers: &[Box<dyn BootImagePatcher + Send>],
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let (mut header, footer, image_size) = avb::load_image(&mut reader)?;
|
||||
let Some(footer) = footer else {
|
||||
let Some(mut footer) = footer else {
|
||||
return Err(Error::NoFooter);
|
||||
};
|
||||
|
||||
@@ -749,6 +764,8 @@ pub fn patch_boot(
|
||||
patcher.patch(&mut boot_image, cancel_signal)?;
|
||||
}
|
||||
|
||||
header.set_algo_for_key(key)?;
|
||||
|
||||
let mut descriptor_iter = header.descriptors.iter_mut().filter_map(|d| {
|
||||
if let Descriptor::Hash(h) = d {
|
||||
Some(h)
|
||||
@@ -768,8 +785,6 @@ pub fn patch_boot(
|
||||
boot_image.to_writer(&mut hashing_writer)?;
|
||||
let (mut writer, context) = hashing_writer.finish();
|
||||
|
||||
header.algorithm_type = AlgorithmType::Sha256Rsa4096;
|
||||
|
||||
descriptor.image_size = writer.stream_position()?;
|
||||
descriptor.hash_algorithm = "sha256".to_owned();
|
||||
descriptor.root_digest = context.finish().as_ref().to_vec();
|
||||
@@ -782,7 +797,7 @@ pub fn patch_boot(
|
||||
header.sign(key)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(writer, &header, &footer, image_size)?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, image_size)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::cli::{avb, boot, completion, key, ota, ramdisk};
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, key, ota};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -16,9 +16,10 @@ pub enum Command {
|
||||
Avb(avb::AvbCli),
|
||||
Boot(boot::BootCli),
|
||||
Completion(completion::CompletionCli),
|
||||
Cpio(cpio::CpioCli),
|
||||
Fec(fec::FecCli),
|
||||
Key(key::KeyCli),
|
||||
Ota(ota::OtaCli),
|
||||
Ramdisk(ramdisk::RamdiskCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
@@ -28,21 +29,23 @@ pub enum Command {
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
pub fn main(cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
pub fn main(cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Command::Avb(c) => avb::avb_main(&c, cancel_signal),
|
||||
Command::Boot(c) => boot::boot_main(&c),
|
||||
Command::Completion(c) => completion::completion_main(&c),
|
||||
Command::Cpio(c) => cpio::cpio_main(&c, cancel_signal),
|
||||
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
|
||||
Command::Key(c) => key::key_main(&c),
|
||||
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
|
||||
Command::Ramdisk(c) => ramdisk::ramdisk_main(&c),
|
||||
// Deprecated aliases.
|
||||
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
|
||||
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
|
||||
|
||||
+640
-87
@@ -5,25 +5,284 @@
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::OsStr,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File},
|
||||
io::{self, BufReader},
|
||||
io::{self, BufReader, BufWriter, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use cap_std::{
|
||||
ambient_authority,
|
||||
fs::{Dir, OpenOptions},
|
||||
};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use rsa::RsaPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
cli::{status, warning},
|
||||
format::avb::{self, Descriptor},
|
||||
stream::PSeekFile,
|
||||
crypto::{self, PassphraseSource},
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
Header,
|
||||
},
|
||||
stream::{self, PSeekFile},
|
||||
util,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
struct AvbInfo {
|
||||
header: Header,
|
||||
footer: Option<Footer>,
|
||||
image_size: u64,
|
||||
}
|
||||
|
||||
fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
|
||||
let file = File::open(path)
|
||||
.with_context(|| format!("Failed to open AVB image for reading: {path:?}"))?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let (header, footer, image_size) = avb::load_image(&mut reader)
|
||||
.with_context(|| format!("Failed to load AVB image: {path:?}"))?;
|
||||
|
||||
let info = AvbInfo {
|
||||
header,
|
||||
footer,
|
||||
image_size,
|
||||
};
|
||||
|
||||
Ok((info, reader))
|
||||
}
|
||||
|
||||
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo) -> Result<()> {
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
if let Some(f) = &mut info.footer {
|
||||
avb::write_appended_image(&mut writer, &info.header, f, info.image_size)
|
||||
.context("Failed to write appended AVB image")?;
|
||||
} else {
|
||||
avb::write_root_image(&mut writer, &info.header, 4096)
|
||||
.context("Failed to write root AVB image")?;
|
||||
}
|
||||
|
||||
writer.flush().context("Failed to flush writes")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read AVB information from TOML file.
|
||||
fn read_info(path: &Path) -> Result<AvbInfo> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read AVB info TOML: {path:?}"))?;
|
||||
let info = toml_edit::de::from_str(&data)
|
||||
.with_context(|| format!("Failed to parse AVB info TOML: {path:?}"))?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Write AVB information to TOML file.
|
||||
fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string_pretty(info)
|
||||
.with_context(|| format!("Failed to serialize AVB info TOML: {path:?}"))?;
|
||||
fs::write(path, data).with_context(|| format!("Failed to write AVB info TOML: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy `size` bytes from `reader` into a new file `path` that's opened as
|
||||
/// both readable and writable.
|
||||
fn write_raw(
|
||||
path: &Path,
|
||||
reader: &mut BufReader<File>,
|
||||
size: u64,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
let file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open raw image for writing: {path:?}"))?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
reader
|
||||
.rewind()
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
stream::copy_n(reader, &mut writer, size, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy raw image: {path:?}"))?;
|
||||
|
||||
let file = writer
|
||||
.into_inner()
|
||||
.with_context(|| format!("Failed to flush writes: {path:?}"))?;
|
||||
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Copy a raw image into `path` and verify the descriptor. If corruption is
|
||||
/// detected and FEC data is available, then a repair is attempted.
|
||||
fn write_raw_and_verify(
|
||||
path: &Path,
|
||||
reader: &mut BufReader<File>,
|
||||
info: &AvbInfo,
|
||||
ignore_invalid: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
let f = info.footer.as_ref().expect("Not an appended image");
|
||||
|
||||
let descriptor = info.header.appended_descriptor()?;
|
||||
|
||||
// The hash tree and FEC need to be present for verification. We don't
|
||||
// verify against the original file because a repair would require writes.
|
||||
let copy_size = match descriptor {
|
||||
AppendedDescriptorRef::HashTree(d) => d.image_size + d.tree_size + d.fec_size,
|
||||
AppendedDescriptorRef::Hash(d) => d.image_size,
|
||||
};
|
||||
|
||||
let raw_file = write_raw(path, reader, copy_size, cancel_signal)?;
|
||||
|
||||
let result = verify_and_repair(None, raw_file.reopen(), descriptor, true, cancel_signal);
|
||||
|
||||
// Chop off the old hash tree and FEC data.
|
||||
raw_file.set_len(f.original_image_size)?;
|
||||
|
||||
if let Err(e) = result {
|
||||
if ignore_invalid {
|
||||
warning!("{e:?}");
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(raw_file)
|
||||
}
|
||||
|
||||
/// Copy a raw image into `path` and update the appended descriptor with the new
|
||||
/// digests, offsets, and sizes.
|
||||
fn write_raw_and_update(
|
||||
path: &Path,
|
||||
reader: &mut BufReader<File>,
|
||||
info: &mut AvbInfo,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PSeekFile> {
|
||||
assert!(info.footer.is_some(), "Not an appended image");
|
||||
|
||||
let image_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.context("Failed to get input data size")?;
|
||||
|
||||
let mut raw_file = write_raw(path, reader, image_size, cancel_signal)?;
|
||||
|
||||
match info.header.appended_descriptor_mut()? {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.image_size = image_size;
|
||||
d.update(
|
||||
|| Ok(Box::new(raw_file.reopen())),
|
||||
|| Ok(Box::new(raw_file.reopen())),
|
||||
cancel_signal,
|
||||
)
|
||||
.context("Failed to update hash tree descriptor")?;
|
||||
}
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
d.image_size = image_size;
|
||||
raw_file.rewind()?;
|
||||
d.update(&mut raw_file, cancel_signal)
|
||||
.context("Failed to update hash descriptor")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(raw_file)
|
||||
}
|
||||
|
||||
/// Sign or clear header signatures based on whether the original header was
|
||||
/// signed. If the original header was signed and is unchanged, then the
|
||||
/// original signature is used as-is. If the force option is specified, then
|
||||
/// the header is signed or cleared based on the presence of the private key
|
||||
/// parameter.
|
||||
fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup) -> Result<()> {
|
||||
enum SignAction {
|
||||
None,
|
||||
Sign,
|
||||
Clear,
|
||||
}
|
||||
|
||||
let originally_signed = !info.header.signature.is_empty();
|
||||
let mut sign_action = if originally_signed && &info.header != orig_header {
|
||||
SignAction::Sign
|
||||
} else {
|
||||
// If the original image was signed, we can preserve the existing
|
||||
// signature since no changes were made. If the original image was not
|
||||
// signed, then there's nothing to do anyway.
|
||||
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 {
|
||||
status!("Preserving original AVB header signature");
|
||||
} else {
|
||||
status!("Leaving AVB header unsigned");
|
||||
}
|
||||
}
|
||||
SignAction::Sign => {
|
||||
if originally_signed {
|
||||
status!("Replacing AVB header signature");
|
||||
} else {
|
||||
status!("Signing AVB header");
|
||||
}
|
||||
|
||||
let Some(key_path) = &key_group.key else {
|
||||
bail!("Need to sign new AVB header, but no private key was specified");
|
||||
};
|
||||
|
||||
let source = PassphraseSource::new(
|
||||
key_path,
|
||||
key_group.pass_file.as_deref(),
|
||||
key_group.pass_env_var.as_deref(),
|
||||
);
|
||||
let private_key = crypto::read_pem_key_file(key_path, &source)
|
||||
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
|
||||
|
||||
info.header.set_algo_for_key(&private_key)?;
|
||||
info.header
|
||||
.sign(&private_key)
|
||||
.context("Failed to sign new AVB header")?;
|
||||
}
|
||||
SignAction::Clear => {
|
||||
if originally_signed {
|
||||
status!("Clearing AVB header signature");
|
||||
} else {
|
||||
status!("Leaving AVB header unsigned");
|
||||
}
|
||||
|
||||
info.header.algorithm_type = AlgorithmType::None;
|
||||
info.header.clear_sig();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dump AVB information to stdout.
|
||||
fn display_info(display: &DisplayGroup, info: &AvbInfo) {
|
||||
if !display.quiet {
|
||||
println!("{info:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that the partition name won't cause directory traversals.
|
||||
fn ensure_name_is_safe(name: &str) -> Result<()> {
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
bail!("Unsafe partition name: {name}");
|
||||
@@ -34,9 +293,9 @@ fn ensure_name_is_safe(name: &str) -> Result<()> {
|
||||
|
||||
/// Recursively verify an image's vbmeta header and all of the chained images.
|
||||
/// `seen` is used to prevent cycles. `descriptors` will contain all of the hash
|
||||
/// and hashtree descriptors that need to be verified.
|
||||
/// and hash tree descriptors that need to be verified.
|
||||
pub fn verify_headers(
|
||||
directory: &Path,
|
||||
directory: &Dir,
|
||||
name: &str,
|
||||
expected_key: Option<&RsaPublicKey>,
|
||||
seen: &mut HashSet<String>,
|
||||
@@ -48,16 +307,17 @@ pub fn verify_headers(
|
||||
|
||||
ensure_name_is_safe(name)?;
|
||||
|
||||
let path = directory.join(format!("{name}.img"));
|
||||
let raw_reader =
|
||||
File::open(&path).with_context(|| anyhow!("Failed to open for reading: {path:?}"))?;
|
||||
let path = format!("{name}.img");
|
||||
let raw_reader = directory
|
||||
.open(&path)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let (header, _, _) = avb::load_image(BufReader::new(raw_reader))
|
||||
.with_context(|| anyhow!("Failed to load vbmeta structures: {path:?}"))?;
|
||||
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
|
||||
|
||||
// Verify the header's signature.
|
||||
let public_key = header
|
||||
.verify()
|
||||
.with_context(|| anyhow!("Failed to verify header signature: {path:?}"))?;
|
||||
.with_context(|| format!("Failed to verify header signature: {path:?}"))?;
|
||||
|
||||
if let Some(k) = &public_key {
|
||||
let prefix = format!("{name} has a signed vbmeta header");
|
||||
@@ -81,7 +341,7 @@ pub fn verify_headers(
|
||||
};
|
||||
|
||||
match descriptor {
|
||||
avb::Descriptor::Hashtree(_) | avb::Descriptor::Hash(_) => {
|
||||
avb::Descriptor::HashTree(_) | avb::Descriptor::Hash(_) => {
|
||||
if let Some(prev) = descriptors.get(target_name) {
|
||||
if prev != descriptor {
|
||||
bail!("{name} descriptor does not match previous encounter");
|
||||
@@ -92,7 +352,7 @@ pub fn verify_headers(
|
||||
}
|
||||
avb::Descriptor::ChainPartition(d) => {
|
||||
let target_key = avb::decode_public_key(&d.public_key).with_context(|| {
|
||||
anyhow!("Failed to decode chained public key for: {target_name}")
|
||||
format!("Failed to decode chained public key for: {target_name}")
|
||||
})?;
|
||||
|
||||
verify_headers(directory, target_name, Some(&target_key), seen, descriptors)?;
|
||||
@@ -104,16 +364,78 @@ pub fn verify_headers(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the descriptor for a file. For hash tree descriptors, if FEC data is
|
||||
/// available and `repair` is true, then attempt to repair data in the event of
|
||||
/// corruption. `file` must be opened as read-write for the repair operation to
|
||||
/// work.
|
||||
fn verify_and_repair(
|
||||
name: Option<&str>,
|
||||
mut file: PSeekFile,
|
||||
descriptor: AppendedDescriptorRef,
|
||||
repair: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let suffix = match name {
|
||||
Some(n) => format!(" for: {n}"),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
match descriptor {
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
status!("Verifying hash tree descriptor{suffix}");
|
||||
|
||||
match d.verify(|| Ok(Box::new(file.reopen())), cancel_signal) {
|
||||
Err(
|
||||
e @ avb::Error::InvalidRootDigest { .. }
|
||||
| e @ avb::Error::InvalidHashTree { .. },
|
||||
) if repair => {
|
||||
warning!("Failed to verify hash tree descriptor{suffix}: {e}");
|
||||
warning!("Attempting to repair using FEC data{suffix}");
|
||||
|
||||
d.repair(
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| format!("Failed to repair data{suffix}"))?;
|
||||
|
||||
d.verify(|| Ok(Box::new(file.reopen())), cancel_signal)
|
||||
.map(|_| {
|
||||
status!("Successfully repaired data{suffix}");
|
||||
})
|
||||
}
|
||||
ret => ret,
|
||||
}
|
||||
.with_context(|| format!("Failed to verify hash tree descriptor{suffix}"))?;
|
||||
}
|
||||
AppendedDescriptorRef::Hash(d) => {
|
||||
status!("Verifying hash descriptor{suffix}");
|
||||
|
||||
file.rewind()?;
|
||||
d.verify(file, cancel_signal)
|
||||
.with_context(|| format!("Failed to verify hash descriptor{suffix}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify hash and hash tree descriptor digests and FEC data against their
|
||||
/// corresponding input files.
|
||||
pub fn verify_descriptors(
|
||||
directory: &Path,
|
||||
directory: &Dir,
|
||||
descriptors: &HashMap<String, Descriptor>,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
repair: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
descriptors
|
||||
.par_iter()
|
||||
.map(|(name, descriptor)| {
|
||||
let path = directory.join(format!("{name}.img"));
|
||||
let reader = match File::open(&path).map(PSeekFile::new) {
|
||||
let path = format!("{name}.img");
|
||||
let file = match directory
|
||||
.open_with(&path, OpenOptions::new().read(true).write(repair))
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
{
|
||||
Ok(f) => f,
|
||||
// Some devices, like bluejay, have vbmeta descriptors that
|
||||
// refer to partitions that exist on the device, but not in the
|
||||
@@ -127,91 +449,312 @@ pub fn verify_descriptors(
|
||||
}
|
||||
};
|
||||
|
||||
match descriptor {
|
||||
Descriptor::Hashtree(d) => {
|
||||
status!("Verifying hashtree descriptor for: {name}");
|
||||
d.verify(
|
||||
|| Ok(Box::new(BufReader::new(reader.clone()))),
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to verify hashtree descriptor for: {name}"))?;
|
||||
}
|
||||
Descriptor::Hash(d) => {
|
||||
status!("Verifying hash descriptor for: {name}");
|
||||
d.verify(BufReader::new(reader), cancel_signal)
|
||||
.with_context(|| anyhow!("Failed to verify hash descriptor for: {name}"))?;
|
||||
}
|
||||
_ => unreachable!("Non-verifiable descriptor: {descriptor:?}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
verify_and_repair(
|
||||
Some(name),
|
||||
file,
|
||||
descriptor.try_into()?,
|
||||
repair,
|
||||
cancel_signal,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn avb_main(cli: &AvbCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
match &cli.command {
|
||||
AvbCommand::Dump(c) => {
|
||||
let raw_reader = File::open(&c.input)
|
||||
.with_context(|| anyhow!("Failed to open for reading: {:?}", c.input))?;
|
||||
let reader = BufReader::new(raw_reader);
|
||||
let (header, footer, image_size) = avb::load_image(reader)
|
||||
.with_context(|| anyhow!("Failed to load vbmeta structures: {:?}", c.input))?;
|
||||
fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (info, mut reader) = read_avb_image(&cli.input)?;
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
println!("Image size: {image_size}");
|
||||
println!("Header: {header:#?}");
|
||||
println!("Footer: {footer:#?}");
|
||||
}
|
||||
AvbCommand::Verify(c) => {
|
||||
let public_key = if let Some(p) = &c.public_key {
|
||||
let data = fs::read(p).with_context(|| anyhow!("Failed to read file: {p:?}"))?;
|
||||
let key = avb::decode_public_key(&data)
|
||||
.with_context(|| anyhow!("Failed to decode public key: {p:?}"))?;
|
||||
write_info(&cli.output_info, &info)?;
|
||||
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let directory = c.input.parent().unwrap_or_else(|| Path::new("."));
|
||||
let name = c
|
||||
.input
|
||||
.file_stem()
|
||||
.with_context(|| anyhow!("Path is not a file: {:?}", c.input))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", c.input))?;
|
||||
|
||||
let mut seen = HashSet::<String>::new();
|
||||
let mut descriptors = HashMap::<String, Descriptor>::new();
|
||||
|
||||
verify_headers(
|
||||
directory,
|
||||
name,
|
||||
public_key.as_ref(),
|
||||
&mut seen,
|
||||
&mut descriptors,
|
||||
)?;
|
||||
verify_descriptors(directory, &descriptors, cancel_signal)?;
|
||||
|
||||
status!("Successfully verified all vbmeta signatures and hashes");
|
||||
}
|
||||
if info.footer.is_some() {
|
||||
write_raw_and_verify(
|
||||
&cli.output_raw,
|
||||
&mut reader,
|
||||
&info,
|
||||
cli.ignore_invalid,
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dump AVB header and footer information.
|
||||
fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let mut info = read_info(&cli.input_info)?;
|
||||
let orig_header = info.header.clone();
|
||||
|
||||
let file = if info.footer.is_some() {
|
||||
let mut reader = File::open(&cli.input_raw)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| {
|
||||
format!("Failed to open raw image for reading: {:?}", cli.input_raw)
|
||||
})?;
|
||||
|
||||
write_raw_and_update(&cli.output, &mut reader, &mut info, cancel_signal)?
|
||||
} else {
|
||||
File::create(&cli.output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open output for writing: {:?}", cli.output))?
|
||||
};
|
||||
|
||||
sign_or_clear(&mut info, &orig_header, &cli.key)?;
|
||||
|
||||
write_avb_image(file, &mut info)?;
|
||||
|
||||
// We display the info at the very end after both the header and footer are
|
||||
// updated so that incorrect/incomplete information isn't shown.
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (mut info, mut reader) = read_avb_image(&cli.input)?;
|
||||
let orig_header = info.header.clone();
|
||||
|
||||
let file = if info.footer.is_some() {
|
||||
let file = write_raw_and_verify(&cli.output, &mut reader, &info, false, cancel_signal)?;
|
||||
|
||||
// Write new hash tree and FEC data instead of copying the original.
|
||||
// THere could have been errors in the original FEC data itself.
|
||||
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
|
||||
d.update(
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
|
||||
file
|
||||
} else {
|
||||
File::create(&cli.output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for writing: {:?}", cli.output))?
|
||||
};
|
||||
|
||||
sign_or_clear(&mut info, &orig_header, &cli.key)?;
|
||||
|
||||
write_avb_image(file, &mut info)?;
|
||||
|
||||
// We display the info at the very end after both the header and footer are
|
||||
// updated so that incorrect/incomplete information isn't shown.
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(cli: &InfoCli) -> Result<()> {
|
||||
let (info, _) = read_avb_image(&cli.input)?;
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let public_key = if let Some(p) = &cli.public_key {
|
||||
let data = fs::read(p).with_context(|| format!("Failed to read file: {p:?}"))?;
|
||||
let key = avb::decode_public_key(&data)
|
||||
.with_context(|| format!("Failed to decode public key: {p:?}"))?;
|
||||
|
||||
Some(key)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let authority = ambient_authority();
|
||||
let parent_path = util::parent_path(&cli.input);
|
||||
let directory = Dir::open_ambient_dir(parent_path, authority)
|
||||
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
|
||||
let name = cli
|
||||
.input
|
||||
.file_stem()
|
||||
.with_context(|| format!("Path is not a file: {:?}", cli.input))?
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
|
||||
|
||||
let mut seen = HashSet::<String>::new();
|
||||
let mut descriptors = HashMap::<String, Descriptor>::new();
|
||||
|
||||
verify_headers(
|
||||
&directory,
|
||||
name,
|
||||
public_key.as_ref(),
|
||||
&mut seen,
|
||||
&mut descriptors,
|
||||
)?;
|
||||
verify_descriptors(&directory, &descriptors, cli.repair, cancel_signal)?;
|
||||
|
||||
status!("Successfully verified all vbmeta signatures and hashes");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
AvbCommand::Unpack(c) => unpack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Pack(c) => pack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Repack(c) => repack_subcommand(c, cancel_signal),
|
||||
AvbCommand::Info(c) => info_subcommand(c),
|
||||
AvbCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct DisplayGroup {
|
||||
/// Don't print AVB image information.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct KeyGroup {
|
||||
/// Path to private key for signing.
|
||||
///
|
||||
/// A private key is needed if packing an image where the original header
|
||||
/// was signed and the header needs to be modified (eg. for a new checksum).
|
||||
/// If the header was originally not signed, then the private key is not
|
||||
/// used, unless --force is specified.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: Option<PathBuf>,
|
||||
|
||||
/// Force signing or clearing signature.
|
||||
///
|
||||
/// If --key is specified, then the output is signed, regardless if the
|
||||
/// original image was signed. Similarly, if --key is not specified, then
|
||||
/// the output is left unsigned, even if the image is unchanged and it's
|
||||
/// possible to preserve an existing signature.
|
||||
#[arg(short, long)]
|
||||
force: bool,
|
||||
|
||||
/// Environment variable containing private key passphrase.
|
||||
#[arg(long, value_name = "ENV_VAR", value_parser, group = "pass")]
|
||||
pass_env_var: Option<OsString>,
|
||||
|
||||
/// File containing private key passphrase.
|
||||
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
|
||||
pass_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Unpack an AVB image.
|
||||
///
|
||||
/// This command splits an AVB image into an AVB info TOML file and a raw image.
|
||||
/// For root AVB images (eg. vbmeta), only the info file is created, which will
|
||||
/// contain a header section. For appended AVB images (eg. boot), the raw image
|
||||
/// will be written and the info file will additionally have a footer section.
|
||||
///
|
||||
/// For appended AVB images that contain a hash tree descriptor (eg. dm-verity
|
||||
/// protected system image), FEC data, if available, will be used to repair the
|
||||
/// extracted raw image in the event of data corruption.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DumpCli {
|
||||
/// Path to input image.
|
||||
struct UnpackCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output AVB info TOML.
|
||||
///
|
||||
/// The file will always contain a header section, but a footer section will
|
||||
/// only be present if the input is an appended AVB image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
|
||||
output_info: PathBuf,
|
||||
|
||||
/// Path to output raw image.
|
||||
///
|
||||
/// Only appended AVB images will have a raw image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
|
||||
output_raw: PathBuf,
|
||||
|
||||
// Ignore invalid digests or FEC data.
|
||||
#[arg(long)]
|
||||
ignore_invalid: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
display: DisplayGroup,
|
||||
}
|
||||
|
||||
/// Pack an AVB image.
|
||||
///
|
||||
/// This command creates an AVB image from the AVB info toml file and the raw
|
||||
/// image components. If the info file does not contain a footer, then a root
|
||||
/// image is created. Otherwise, an appended image is created and a raw image
|
||||
/// must be specified.
|
||||
///
|
||||
/// For appended images, the checksums in the header's descriptors and the
|
||||
/// offsets in the footer will be updated. If the header fields are updated and
|
||||
/// the header was originally signed, then the newly created image will also be
|
||||
/// signed. If a signed image is repacked without modification, then the
|
||||
/// original signature is used as-is.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input AVB info TOML.
|
||||
///
|
||||
/// If an appended image is being created, then the hash or hash tree
|
||||
/// descriptor in the header will be updated with the new digest, offset,
|
||||
/// and size field values. There must be exactly one hash or hash tree
|
||||
/// descriptor. The offset and size fields in the footer will be updated as
|
||||
/// well.
|
||||
///
|
||||
/// For root images, all header descriptor fields are left unmodified.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
|
||||
input_info: PathBuf,
|
||||
|
||||
/// Path to input raw image.
|
||||
///
|
||||
/// Appended AVB images require a raw image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
|
||||
input_raw: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
key: KeyGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
display: DisplayGroup,
|
||||
}
|
||||
|
||||
/// Repack an AVB image.
|
||||
///
|
||||
/// This command is equivalent to running `unpack` and `pack`, except without
|
||||
/// storing the unpacked data to disk.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
key: KeyGroup,
|
||||
|
||||
#[command(flatten)]
|
||||
display: DisplayGroup,
|
||||
}
|
||||
|
||||
/// Display AVB header and footer information.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
display: DisplayGroup,
|
||||
}
|
||||
|
||||
/// Verify vbmeta signatures.
|
||||
///
|
||||
/// If the header contains chain descriptors, then those images will be
|
||||
/// recursively verified. For hash tree descriptors, the FEC (forward error
|
||||
/// correction) data will also be verified.
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyCli {
|
||||
/// Path to input image.
|
||||
/// Path to input AVB image.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
@@ -221,15 +764,25 @@ struct VerifyCli {
|
||||
/// validity, not whether they are trusted.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
public_key: Option<PathBuf>,
|
||||
|
||||
/// Repair corrupted files using FEC data if possible.
|
||||
///
|
||||
/// Only images with hash tree descriptors can contain FEC data.
|
||||
#[arg(short, long)]
|
||||
repair: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum AvbCommand {
|
||||
Dump(DumpCli),
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
#[command(alias = "dump")]
|
||||
Info(InfoCli),
|
||||
Verify(VerifyCli),
|
||||
}
|
||||
|
||||
/// Show information about AVB-protected images.
|
||||
/// Pack, unpack, and inspect AVB-protected images.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AvbCli {
|
||||
#[command(subcommand)]
|
||||
|
||||
+22
-16
@@ -9,11 +9,11 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::{avb::Header, bootimage::BootImage, compression::CompressedReader, cpio},
|
||||
format::{avb::Header, bootimage::BootImage, compression::CompressedReader, cpio::CpioReader},
|
||||
stream::{FromReader, ToWriter},
|
||||
};
|
||||
|
||||
@@ -33,7 +33,9 @@ fn write_image(path: &Path, image: &BootImage) -> Result<()> {
|
||||
image
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write boot image: {path:?}"))?;
|
||||
writer.flush()?;
|
||||
writer
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush boot image: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -82,7 +84,7 @@ fn read_avb_header_if_exists(path: &Path) -> Result<Option<Header>> {
|
||||
Err(e) => Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?,
|
||||
};
|
||||
let header = Header::from_reader(BufReader::new(file))
|
||||
.with_context(|| anyhow!("Failed to read vbmeta header: {path:?}"))?;
|
||||
.with_context(|| format!("Failed to read vbmeta header: {path:?}"))?;
|
||||
|
||||
Ok(Some(header))
|
||||
}
|
||||
@@ -106,7 +108,7 @@ fn write_text_if_not_empty(path: &Path, text: &str) -> Result<()> {
|
||||
|
||||
fn write_avb_header(path: &Path, header: &Header) -> Result<()> {
|
||||
let file =
|
||||
File::create(path).with_context(|| anyhow!("Failed to open for writing: {path:?}"))?;
|
||||
File::create(path).with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
header.to_writer(BufWriter::new(file))?;
|
||||
|
||||
Ok(())
|
||||
@@ -272,9 +274,9 @@ fn info_subcommand(boot_cli: &BootCli, cli: &InfoCli) -> Result<()> {
|
||||
|
||||
pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
|
||||
let raw_reader = File::open(&cli.image)
|
||||
.with_context(|| anyhow!("Failed to open for reading: {:?}", cli.image))?;
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.image))?;
|
||||
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))
|
||||
.with_context(|| anyhow!("Failed to load boot image: {:?}", cli.image))?;
|
||||
.with_context(|| format!("Failed to load boot image: {:?}", cli.image))?;
|
||||
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
@@ -297,13 +299,17 @@ pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
|
||||
for (i, ramdisk) in ramdisks.iter().enumerate() {
|
||||
let reader = Cursor::new(ramdisk);
|
||||
let reader = CompressedReader::new(reader, true)
|
||||
.with_context(|| anyhow!("Failed to load ramdisk #{i}"))?;
|
||||
let entries = cpio::load(reader, false)
|
||||
.with_context(|| anyhow!("Failed to load ramdisk #{i} cpio"))?;
|
||||
.with_context(|| format!("Failed to load ramdisk #{i}"))?;
|
||||
let mut cpio_reader = CpioReader::new(reader, false);
|
||||
|
||||
if let Some(e) = entries.iter().find(|e| e.name == b".backup/.magisk") {
|
||||
io::stdout().write_all(&e.content)?;
|
||||
return Ok(());
|
||||
while let Some(entry) = cpio_reader
|
||||
.next_entry()
|
||||
.with_context(|| format!("Failed to read ramdisk #{i} cpio entry"))?
|
||||
{
|
||||
if entry.path == b".backup/.magisk" {
|
||||
io::copy(&mut cpio_reader, &mut io::stdout())?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +334,7 @@ struct UnpackCli {
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output header TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "header.toml")]
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "boot.toml")]
|
||||
output_header: PathBuf,
|
||||
|
||||
/// Path to output kernel image.
|
||||
@@ -388,7 +394,7 @@ struct PackCli {
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input header TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "header.toml")]
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "boot.toml")]
|
||||
input_header: PathBuf,
|
||||
|
||||
/// Path to input kernel image.
|
||||
@@ -477,7 +483,7 @@ enum BootCommand {
|
||||
MagiskInfo(MagiskInfoCli),
|
||||
}
|
||||
|
||||
/// Pack or unpack boot images.
|
||||
/// Pack, unpack, and inspect boot images.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct BootCli {
|
||||
#[command(subcommand)]
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{BufReader, BufWriter, Seek},
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use bstr::ByteSlice;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{Parser, Subcommand};
|
||||
use num_traits::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
compression::{CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntry, CpioEntryData, CpioEntryType, CpioReader, CpioWriter},
|
||||
},
|
||||
stream, util,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
struct CpioInfo {
|
||||
format: CompressedFormat,
|
||||
entries: Vec<CpioEntry>,
|
||||
}
|
||||
|
||||
fn open_reader(
|
||||
path: &Path,
|
||||
include_trailer: bool,
|
||||
) -> Result<(
|
||||
CpioReader<CompressedReader<BufReader<File>>>,
|
||||
CompressedFormat,
|
||||
)> {
|
||||
let file =
|
||||
File::open(path).with_context(|| format!("Failed to open cpio for reading: {path:?}"))?;
|
||||
let reader = CompressedReader::new(BufReader::new(file), true)
|
||||
.with_context(|| format!("Failed to open decompressor: {path:?}"))?;
|
||||
let format = reader.format();
|
||||
let cpio_reader = CpioReader::new(reader, include_trailer);
|
||||
|
||||
Ok((cpio_reader, format))
|
||||
}
|
||||
|
||||
fn open_writer(
|
||||
path: &Path,
|
||||
format: CompressedFormat,
|
||||
) -> Result<CpioWriter<CompressedWriter<BufWriter<File>>>> {
|
||||
let file =
|
||||
File::create(path).with_context(|| format!("Failed to open cpio for writing: {path:?}"))?;
|
||||
let writer = CompressedWriter::new(BufWriter::new(file), format)
|
||||
.with_context(|| format!("Failed to open compressor: {path:?}"))?;
|
||||
let cpio_writer = CpioWriter::new(writer, false);
|
||||
|
||||
Ok(cpio_writer)
|
||||
}
|
||||
|
||||
fn flush_writer(writer: CpioWriter<CompressedWriter<BufWriter<File>>>) -> Result<()> {
|
||||
let compressed_writer = writer.finish().context("Failed to flush cpio writer")?;
|
||||
let buf_writer = compressed_writer
|
||||
.finish()
|
||||
.context("Failed to flush compressor")?;
|
||||
buf_writer.into_inner().context("Failed to flush file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read cpio information from TOML file.
|
||||
fn read_info(path: &Path) -> Result<CpioInfo> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read cpio info TOML: {path:?}"))?;
|
||||
let info = toml_edit::de::from_str(&data)
|
||||
.with_context(|| format!("Failed to parse cpio info TOML: {path:?}"))?;
|
||||
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
/// Write cpio information to TOML file.
|
||||
fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string_pretty(info)
|
||||
.with_context(|| format!("Failed to serialize cpio info TOML: {path:?}"))?;
|
||||
fs::write(path, data).with_context(|| format!("Failed to write cpio info TOML: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Open reader to the corresponding file inside the tree if the entry is a
|
||||
/// regular file. Unsafe paths will result in an error.
|
||||
fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
|
||||
if entry.file_type == CpioEntryType::Regular {
|
||||
let path = entry
|
||||
.path
|
||||
.as_bstr()
|
||||
.to_path()
|
||||
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
|
||||
|
||||
let mut reader = tree
|
||||
.open(path)
|
||||
.map(|f| BufReader::new(f.into_std()))
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
|
||||
let file_size = reader
|
||||
.seek(std::io::SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {path:?}"))?
|
||||
.to_u32()
|
||||
.ok_or_else(|| anyhow!("File is too large: {path:?}"))?;
|
||||
reader
|
||||
.rewind()
|
||||
.with_context(|| format!("Failed to seek file: {path:?}"))?;
|
||||
|
||||
Ok(Some((reader, file_size)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open writer to the corresponding file inside the tree if the entry is a
|
||||
/// regular file. Intermediate directories are automatically created as needed.
|
||||
/// Unsafe paths will result in an error.
|
||||
fn create_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
|
||||
if entry.file_type == CpioEntryType::Regular {
|
||||
let path = entry
|
||||
.path
|
||||
.as_bstr()
|
||||
.to_path()
|
||||
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
|
||||
let parent = util::parent_path(path);
|
||||
|
||||
tree.create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
|
||||
|
||||
let writer = tree
|
||||
.create(path)
|
||||
.map(|f| BufWriter::new(f.into_std()))
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
|
||||
Ok(Some(writer))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn display_format(cli: &CpioCli, format: CompressedFormat) {
|
||||
if !cli.quiet {
|
||||
println!("Compression format: {format:?}");
|
||||
}
|
||||
}
|
||||
|
||||
fn display_entry(cli: &CpioCli, entry: &CpioEntry) {
|
||||
if !cli.quiet {
|
||||
println!();
|
||||
println!("{entry}");
|
||||
}
|
||||
}
|
||||
|
||||
fn unpack_subcommand(
|
||||
cpio_cli: &CpioCli,
|
||||
cli: &UnpackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let (mut reader, format) = open_reader(&cli.input, false)?;
|
||||
let mut info = CpioInfo {
|
||||
format,
|
||||
entries: vec![],
|
||||
};
|
||||
|
||||
display_format(cpio_cli, format);
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.output_tree, authority)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
|
||||
let tree = Dir::open_ambient_dir(&cli.output_tree, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.output_tree))?;
|
||||
|
||||
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
|
||||
display_entry(cpio_cli, &entry);
|
||||
|
||||
if let Some(mut writer) = create_tree_file(&tree, &entry)? {
|
||||
let file_size = entry.data.size()?;
|
||||
|
||||
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
|
||||
.context("Failed to copy data")?;
|
||||
|
||||
writer.into_inner().context("Failed to flush data")?;
|
||||
}
|
||||
|
||||
info.entries.push(entry);
|
||||
}
|
||||
|
||||
write_info(&cli.output_info, &info)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(cpio_cli: &CpioCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let mut info = read_info(&cli.input_info)?;
|
||||
let mut writer = open_writer(&cli.output, info.format)?;
|
||||
|
||||
display_format(cpio_cli, info.format);
|
||||
|
||||
if cli.sort {
|
||||
cpio::sort(&mut info.entries);
|
||||
}
|
||||
|
||||
cpio::assign_inodes(&mut info.entries, true)?;
|
||||
|
||||
let authority = ambient_authority();
|
||||
let tree = Dir::open_ambient_dir(&cli.input_tree, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.input_tree))?;
|
||||
|
||||
for entry in &mut info.entries {
|
||||
let out = open_tree_file(&tree, entry)?;
|
||||
|
||||
if let Some((_, file_size)) = &out {
|
||||
entry.data = CpioEntryData::Size(*file_size);
|
||||
}
|
||||
|
||||
display_entry(cpio_cli, entry);
|
||||
|
||||
writer
|
||||
.start_entry(entry)
|
||||
.context("Failed to write cpio entry")?;
|
||||
|
||||
if let Some((mut reader, file_size)) = out {
|
||||
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
|
||||
.context("Failed to copy data")?;
|
||||
}
|
||||
}
|
||||
|
||||
flush_writer(writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repack_subcommand(
|
||||
cpio_cli: &CpioCli,
|
||||
cli: &RepackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let (mut reader, format) = open_reader(&cli.input, false)?;
|
||||
let mut writer = open_writer(&cli.output, format)?;
|
||||
|
||||
display_format(cpio_cli, format);
|
||||
|
||||
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
|
||||
display_entry(cpio_cli, &entry);
|
||||
|
||||
writer
|
||||
.start_entry(&entry)
|
||||
.context("Failed to write cpio entry")?;
|
||||
|
||||
if let CpioEntryData::Size(s) = &entry.data {
|
||||
stream::copy_n(&mut reader, &mut writer, u64::from(*s), cancel_signal)
|
||||
.context("Failed to copy cpio entry data")?;
|
||||
}
|
||||
}
|
||||
|
||||
flush_writer(writer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn info_subcommand(cpio_cli: &CpioCli, cli: &InfoCli) -> Result<()> {
|
||||
let (mut reader, format) = open_reader(&cli.input, cli.trailer)?;
|
||||
|
||||
display_format(cpio_cli, format);
|
||||
|
||||
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
|
||||
display_entry(cpio_cli, &entry);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cpio_main(cli: &CpioCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
CpioCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
|
||||
CpioCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
|
||||
CpioCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
|
||||
CpioCommand::Info(c) => info_subcommand(cli, c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unpack a cpio archive.
|
||||
///
|
||||
/// Regular files will be extracted to the output tree directory, but not any
|
||||
/// other type of file (eg. symlinks). All file metadata is written to the info
|
||||
/// TOML file, like the UID/GID, permissions, and symlink targets.
|
||||
///
|
||||
/// If any paths inside the cpio archive are unsafe, the extraction process will
|
||||
/// fail and exit. Extracted files are never written outside of the tree
|
||||
/// directory, even if an external process tries to interfere.
|
||||
#[derive(Debug, Parser)]
|
||||
struct UnpackCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output cpio info TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
|
||||
output_info: PathBuf,
|
||||
|
||||
/// Path to output files directory.
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
|
||||
output_tree: PathBuf,
|
||||
}
|
||||
|
||||
/// Pack a cpio archive.
|
||||
///
|
||||
/// The new cpio archive will *only* contain files listed in the info TOML file.
|
||||
/// Extra files inside the input tree directory that aren't listed will be
|
||||
/// silently ignored. Entries are added to the archive in the order that they
|
||||
/// are listed unless --sort is specified.
|
||||
///
|
||||
/// All fields inside the info TOML are used as-is. Missing fields in entries
|
||||
/// are set to 0, aside from the inode number, which will be assigned a unique
|
||||
/// value.
|
||||
#[derive(Debug, Parser)]
|
||||
struct PackCli {
|
||||
/// Path to output cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to input info TOML.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
|
||||
input_info: PathBuf,
|
||||
|
||||
/// Path to input files directory.
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
|
||||
input_tree: PathBuf,
|
||||
|
||||
/// Sort entries before packing.
|
||||
#[arg(long)]
|
||||
sort: bool,
|
||||
}
|
||||
|
||||
/// Repack a cpio archive.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
/// Display cpio entry information.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Show cpio trailer entry.
|
||||
#[arg(long, global = true)]
|
||||
trailer: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum CpioCommand {
|
||||
Unpack(UnpackCli),
|
||||
Pack(PackCli),
|
||||
Repack(RepackCli),
|
||||
Info(InfoCli),
|
||||
}
|
||||
|
||||
/// Pack, unpack, and inspect cpio archives.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CpioCli {
|
||||
#[command(subcommand)]
|
||||
command: CpioCommand,
|
||||
|
||||
/// Don't print cpio entry information.
|
||||
#[arg(short, long, global = true)]
|
||||
quiet: bool,
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::fec::FecImage,
|
||||
stream::{FromReader, PSeekFile, ToWriter},
|
||||
};
|
||||
|
||||
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.write(rw)
|
||||
.open(path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))
|
||||
}
|
||||
|
||||
fn read_fec(path: &Path) -> Result<FecImage> {
|
||||
let reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let fec = FecImage::from_reader(reader)
|
||||
.with_context(|| format!("Failed to read FEC data: {path:?}"))?;
|
||||
|
||||
Ok(fec)
|
||||
}
|
||||
|
||||
fn write_fec(path: &Path, fec: &FecImage) -> Result<()> {
|
||||
let mut writer = File::create(path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
fec.to_writer(&mut writer)
|
||||
.with_context(|| format!("Failed to write FEC data: {path:?}"))?;
|
||||
writer
|
||||
.flush()
|
||||
.with_context(|| format!("Failed to flush FEC data: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
|
||||
let fec = FecImage::generate(|| Ok(Box::new(input.reopen())), cli.parity, cancel_signal)
|
||||
.context("Failed to generate FEC data")?;
|
||||
|
||||
write_fec(&cli.fec, &fec)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, false)?;
|
||||
let fec = read_fec(&cli.fec)?;
|
||||
|
||||
fec.verify(|| Ok(Box::new(input.reopen())), cancel_signal)
|
||||
.context("Failed to verify data")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let input = open_input(&cli.input, true)?;
|
||||
let fec = read_fec(&cli.fec)?;
|
||||
|
||||
// The separate buffered readers and writers are safe because the function
|
||||
// guarantees that every thread touches disjoint offsets and every offset is
|
||||
// read and written at most once.
|
||||
fec.repair(
|
||||
|| Ok(Box::new(input.reopen())),
|
||||
|| Ok(Box::new(input.reopen())),
|
||||
cancel_signal,
|
||||
)
|
||||
.context("Failed to repair file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn fec_main(cli: &FecCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
FecCommand::Generate(c) => generate_subcommand(c, cancel_signal),
|
||||
FecCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
FecCommand::Repair(c) => repair_subcommand(c, cancel_signal),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate FEC data for a file.
|
||||
#[derive(Debug, Parser)]
|
||||
struct GenerateCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output FEC data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
fec: PathBuf,
|
||||
|
||||
/// Number of parity bytes per RS block (min 2, max 24).
|
||||
#[arg(short, long, value_name = "BYTES", default_value = "2")]
|
||||
parity: u8,
|
||||
}
|
||||
|
||||
/// Verify that a file contains no errors.
|
||||
#[derive(Debug, Parser)]
|
||||
struct VerifyCli {
|
||||
/// Path to input data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to input FEC data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
fec: PathBuf,
|
||||
}
|
||||
|
||||
/// Repair a file.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepairCli {
|
||||
/// Path to data.
|
||||
///
|
||||
/// The file will be modified in place.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to input FEC data.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
fec: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum FecCommand {
|
||||
Generate(GenerateCli),
|
||||
Verify(VerifyCli),
|
||||
Repair(RepairCli),
|
||||
}
|
||||
|
||||
/// Generate dm-verity FEC data and verify/repair files.
|
||||
///
|
||||
/// These commands operate on FEC files with AOSP's header format.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct FecCli {
|
||||
#[command(subcommand)]
|
||||
command: FecCommand,
|
||||
}
|
||||
+21
-22
@@ -10,7 +10,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
@@ -18,59 +18,58 @@ use crate::{
|
||||
format::avb,
|
||||
};
|
||||
|
||||
fn get_passphrase(group: &PassphraseGroup, key_path: &Path) -> PassphraseSource {
|
||||
if let Some(v) = &group.pass_env_var {
|
||||
PassphraseSource::EnvVar(v.clone())
|
||||
} else if let Some(p) = &group.pass_file {
|
||||
PassphraseSource::File(p.clone())
|
||||
} else {
|
||||
PassphraseSource::Prompt(format!("Enter passphrase for {key_path:?}: "))
|
||||
}
|
||||
fn get_passphrase_source(group: &PassphraseGroup, key_path: &Path) -> PassphraseSource {
|
||||
PassphraseSource::new(
|
||||
key_path,
|
||||
group.pass_file.as_deref(),
|
||||
group.pass_env_var.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn key_main(cli: &KeyCli) -> Result<()> {
|
||||
match &cli.command {
|
||||
KeyCommand::GenerateKey(c) => {
|
||||
let passphrase = get_passphrase(&c.passphrase, &c.output);
|
||||
let source = get_passphrase_source(&c.passphrase, &c.output);
|
||||
let private_key =
|
||||
crypto::generate_rsa_key_pair().context("Failed to generate RSA keypair")?;
|
||||
|
||||
crypto::write_pem_key_file(&c.output, &private_key, &passphrase)
|
||||
.with_context(|| anyhow!("Failed to write private key: {:?}", c.output))?;
|
||||
crypto::write_pem_key_file(&c.output, &private_key, &source)
|
||||
.with_context(|| format!("Failed to write private key: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::GenerateCert(c) => {
|
||||
let passphrase = get_passphrase(&c.passphrase, &c.key);
|
||||
let private_key = crypto::read_pem_key_file(&c.key, &passphrase)
|
||||
.with_context(|| anyhow!("Failed to load key: {:?}", c.key))?;
|
||||
let source = get_passphrase_source(&c.passphrase, &c.key);
|
||||
let private_key = crypto::read_pem_key_file(&c.key, &source)
|
||||
.with_context(|| format!("Failed to load key: {:?}", c.key))?;
|
||||
|
||||
let validity = Duration::from_secs(c.validity * 24 * 60 * 60);
|
||||
let cert = crypto::generate_cert(&private_key, rand::random(), validity, &c.subject)
|
||||
.context("Failed to generate certificate")?;
|
||||
|
||||
crypto::write_pem_cert_file(&c.output, &cert)
|
||||
.with_context(|| anyhow!("Failed to write certificate: {:?}", c.output))?;
|
||||
.with_context(|| format!("Failed to write certificate: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::ExtractAvb(c) => {
|
||||
let public_key = if let Some(p) = &c.input.key {
|
||||
let passphrase = get_passphrase(&c.passphrase, p);
|
||||
let passphrase = get_passphrase_source(&c.passphrase, p);
|
||||
let private_key = crypto::read_pem_key_file(p, &passphrase)
|
||||
.with_context(|| anyhow!("Failed to load key: {p:?}"))?;
|
||||
.with_context(|| format!("Failed to load key: {p:?}"))?;
|
||||
|
||||
private_key.to_public_key()
|
||||
} else if let Some(p) = &c.input.cert {
|
||||
let certificate = crypto::read_pem_cert_file(p)
|
||||
.with_context(|| anyhow!("Failed to load certificate: {p:?}"))?;
|
||||
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
|
||||
|
||||
crypto::get_public_key(&certificate)?
|
||||
crypto::get_public_key(&certificate)
|
||||
.with_context(|| format!("Failed to extract public key: {p:?}"))?
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let encoded = avb::encode_public_key(&public_key)
|
||||
.with_context(|| anyhow!("Failed to encode public key in AVB format"))?;
|
||||
.context("Failed to encode public key in AVB format")?;
|
||||
|
||||
fs::write(&c.output, encoded)
|
||||
.with_context(|| anyhow!("Failed to write public key: {:?}", c.output))?;
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,19 +7,20 @@ pub mod args;
|
||||
pub mod avb;
|
||||
pub mod boot;
|
||||
pub mod completion;
|
||||
pub mod cpio;
|
||||
pub mod fec;
|
||||
pub mod key;
|
||||
pub mod ota;
|
||||
pub mod ramdisk;
|
||||
|
||||
macro_rules! status {
|
||||
($($arg:tt)*) => {
|
||||
println!("\x1b[1m[*] {}\x1b[0m", format!($($arg)*))
|
||||
eprintln!("\x1b[1m[*] {}\x1b[0m", format!($($arg)*))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! warning {
|
||||
($($arg:tt)*) => {
|
||||
println!("\x1b[1;31m[WARNING] {}\x1b[0m", format!($($arg)+))
|
||||
eprintln!("\x1b[1;31m[WARNING] {}\x1b[0m", format!($($arg)*))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+135
-132
@@ -11,16 +11,18 @@ use std::{
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_tempfile::TempDir;
|
||||
use clap::{value_parser, ArgAction, Args, Parser, Subcommand};
|
||||
use phf::phf_map;
|
||||
use rayon::prelude::{IntoParallelIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||
use rsa::RsaPrivateKey;
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use tempfile::NamedTempFile;
|
||||
use topological_sort::TopologicalSort;
|
||||
use x509_cert::Certificate;
|
||||
use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
@@ -31,7 +33,7 @@ use crate::{
|
||||
crypto::{self, PassphraseSource},
|
||||
format::{
|
||||
avb::Header,
|
||||
avb::{self, AlgorithmType, Descriptor},
|
||||
avb::{self, Descriptor},
|
||||
bootimage::BootImage,
|
||||
ota::{self, SigningWriter, ZipEntry},
|
||||
padding,
|
||||
@@ -44,6 +46,7 @@ use crate::{
|
||||
self, CountingWriter, FromReader, HolePunchingWriter, PSeekFile, ReadSeek, SectionReader,
|
||||
ToWriter,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
||||
static PARTITION_PRIORITIES: phf::Map<&'static str, &[&'static str]> = phf_map! {
|
||||
@@ -143,7 +146,7 @@ fn open_input_streams(
|
||||
required_images: &HashMap<String, String>,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
header: &PayloadHeader,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<HashMap<String, Box<dyn ReadSeek + Send>>> {
|
||||
let mut input_streams = HashMap::<String, Box<dyn ReadSeek + Send>>::new();
|
||||
|
||||
@@ -159,14 +162,14 @@ fn open_input_streams(
|
||||
status!("Opening external image: {name}: {path:?}");
|
||||
|
||||
let file = File::open(path)
|
||||
.with_context(|| anyhow!("Failed to open external image: {path:?}"))?;
|
||||
.with_context(|| format!("Failed to open external image: {path:?}"))?;
|
||||
input_streams.insert(name.clone(), Box::new(file));
|
||||
} else {
|
||||
status!("Extracting from original payload: {name}");
|
||||
|
||||
let stream =
|
||||
payload::extract_image_to_memory(&open_payload, header, name, cancel_signal)
|
||||
.with_context(|| anyhow!("Failed to extract from original payload: {name}"))?;
|
||||
.with_context(|| format!("Failed to extract from original payload: {name}"))?;
|
||||
input_streams.insert(name.clone(), Box::new(stream));
|
||||
}
|
||||
}
|
||||
@@ -185,7 +188,7 @@ fn patch_boot_images(
|
||||
root_patcher: Option<Box<dyn BootImagePatcher + Send>>,
|
||||
key_avb: &RsaPrivateKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let mut boot_patchers = HashMap::<&str, Vec<Box<dyn BootImagePatcher + Send>>>::new();
|
||||
boot_patchers
|
||||
@@ -219,7 +222,7 @@ fn patch_boot_images(
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
|
||||
boot::patch_boot(s, &mut writer, key_avb, &p, cancel_signal)
|
||||
.with_context(|| anyhow!("Failed to patch boot image: {n}"))?;
|
||||
.with_context(|| format!("Failed to patch boot image: {n}"))?;
|
||||
|
||||
Ok((n, writer))
|
||||
})
|
||||
@@ -247,7 +250,7 @@ fn get_vbmeta_patch_order(
|
||||
for name in vbmeta_images {
|
||||
let reader = images.get_mut(name).unwrap();
|
||||
let (header, footer, _) = avb::load_image(reader)
|
||||
.with_context(|| anyhow!("Failed to load vbmeta image: {name}"))?;
|
||||
.with_context(|| format!("Failed to load vbmeta image: {name}"))?;
|
||||
|
||||
if let Some(f) = footer {
|
||||
warning!("{name} is a vbmeta partition, but has a footer: {f:?}");
|
||||
@@ -344,16 +347,7 @@ fn update_vbmeta_descriptors(
|
||||
}
|
||||
}
|
||||
|
||||
// avbroot doesn't support any other types.
|
||||
if parent_header.algorithm_type != AlgorithmType::Sha256Rsa4096 {
|
||||
parent_header.algorithm_type = AlgorithmType::Sha256Rsa4096;
|
||||
|
||||
status!(
|
||||
"{} signature algorithm type changed to {:?}",
|
||||
name,
|
||||
parent_header.algorithm_type
|
||||
);
|
||||
}
|
||||
parent_header.set_algo_for_key(key)?;
|
||||
|
||||
for dep in deps.iter() {
|
||||
// This can't fail since the descriptor must have existed for the
|
||||
@@ -366,7 +360,7 @@ fn update_vbmeta_descriptors(
|
||||
|
||||
let reader = images.get_mut(dep).unwrap();
|
||||
let (header, _, _) = avb::load_image(reader)
|
||||
.with_context(|| anyhow!("Failed to load vbmeta footer from image: {dep}"))?;
|
||||
.with_context(|| format!("Failed to load vbmeta footer from image: {dep}"))?;
|
||||
|
||||
if header.public_key.is_empty() {
|
||||
// vbmeta is unsigned. Use the existing descriptor.
|
||||
@@ -382,7 +376,7 @@ fn update_vbmeta_descriptors(
|
||||
(Descriptor::Hash(pd), Descriptor::Hash(d)) => {
|
||||
*pd = d.clone();
|
||||
}
|
||||
(Descriptor::Hashtree(pd), Descriptor::Hashtree(d)) => {
|
||||
(Descriptor::HashTree(pd), Descriptor::HashTree(d)) => {
|
||||
*pd = d.clone();
|
||||
}
|
||||
_ => {
|
||||
@@ -404,15 +398,15 @@ fn update_vbmeta_descriptors(
|
||||
|
||||
parent_header
|
||||
.sign(key)
|
||||
.with_context(|| anyhow!("Failed to sign vbmeta header for image: {name}"))?;
|
||||
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
parent_header
|
||||
.to_writer(&mut writer)
|
||||
.with_context(|| anyhow!("Failed to write vbmeta image: {name}"))?;
|
||||
.with_context(|| format!("Failed to write vbmeta image: {name}"))?;
|
||||
|
||||
padding::write_zeros(&mut writer, block_size)
|
||||
.with_context(|| anyhow!("Failed to write vbmeta padding: {name}"))?;
|
||||
.with_context(|| format!("Failed to write vbmeta padding: {name}"))?;
|
||||
|
||||
*images.get_mut(name).unwrap() = Box::new(writer);
|
||||
}
|
||||
@@ -426,7 +420,7 @@ fn compress_image(
|
||||
mut stream: &mut Box<dyn ReadSeek + Send>,
|
||||
header: &Mutex<PayloadHeader>,
|
||||
block_size: u32,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
stream.rewind()?;
|
||||
|
||||
@@ -460,10 +454,14 @@ fn patch_ota_payload(
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_ota: &RsaPrivateKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(String, u64)> {
|
||||
let header = PayloadHeader::from_reader(open_payload()?)
|
||||
.with_context(|| anyhow!("Failed to load OTA payload header"))?;
|
||||
let header =
|
||||
PayloadHeader::from_reader(open_payload()?).context("Failed to load OTA payload header")?;
|
||||
if !header.is_full_ota() {
|
||||
bail!("Payload is a delta OTA, not a full OTA");
|
||||
}
|
||||
|
||||
let header = Mutex::new(header);
|
||||
let header_locked = header.lock().unwrap();
|
||||
let all_partitions = header_locked
|
||||
@@ -537,7 +535,7 @@ fn patch_ota_payload(
|
||||
&mut vbmeta_order,
|
||||
clear_vbmeta_flags,
|
||||
key_avb,
|
||||
header_locked.manifest.block_size.into(),
|
||||
header_locked.manifest.block_size().into(),
|
||||
)?;
|
||||
|
||||
status!(
|
||||
@@ -545,14 +543,14 @@ fn patch_ota_payload(
|
||||
joined(sorted(input_streams.keys())),
|
||||
);
|
||||
|
||||
let block_size = header_locked.manifest.block_size;
|
||||
let block_size = header_locked.manifest.block_size();
|
||||
drop(header_locked);
|
||||
|
||||
input_streams
|
||||
.par_iter_mut()
|
||||
.map(|(name, stream)| -> Result<()> {
|
||||
compress_image(name, stream, &header, block_size, cancel_signal)
|
||||
.with_context(|| anyhow!("Failed to compress image: {name}"))
|
||||
.with_context(|| format!("Failed to compress image: {name}"))
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
@@ -560,12 +558,12 @@ fn patch_ota_payload(
|
||||
|
||||
let header_locked = header.lock().unwrap();
|
||||
let mut payload_writer = PayloadWriter::new(writer, header_locked.clone(), key_ota.clone())
|
||||
.with_context(|| anyhow!("Failed to write payload header"))?;
|
||||
let mut orig_payload_reader = open_payload()?;
|
||||
.context("Failed to write payload header")?;
|
||||
let mut orig_payload_reader = open_payload().context("Failed to open payload")?;
|
||||
|
||||
while payload_writer
|
||||
.begin_next_operation()
|
||||
.with_context(|| anyhow!("Failed to begin next payload blob entry"))?
|
||||
.context("Failed to begin next payload blob entry")?
|
||||
{
|
||||
let name = payload_writer.partition().unwrap().partition_name.clone();
|
||||
let operation = payload_writer.operation().unwrap();
|
||||
@@ -577,10 +575,12 @@ fn patch_ota_payload(
|
||||
|
||||
if let Some(mut reader) = input_streams.remove(&name) {
|
||||
// Copy from our replacement image.
|
||||
reader.rewind()?;
|
||||
reader
|
||||
.rewind()
|
||||
.with_context(|| format!("Failed to seek image: {name}"))?;
|
||||
|
||||
stream::copy_n(&mut reader, &mut payload_writer, data_length, cancel_signal)
|
||||
.with_context(|| anyhow!("Failed to copy from replacement image: {name}"))?;
|
||||
.with_context(|| format!("Failed to copy from replacement image: {name}"))?;
|
||||
} else {
|
||||
// Copy from the original payload.
|
||||
let pi = payload_writer.partition_index().unwrap();
|
||||
@@ -595,7 +595,7 @@ fn patch_ota_payload(
|
||||
|
||||
orig_payload_reader
|
||||
.seek(SeekFrom::Start(data_offset))
|
||||
.with_context(|| anyhow!("Failed to seek original payload to {data_offset}"))?;
|
||||
.with_context(|| format!("Failed to seek original payload to {data_offset}"))?;
|
||||
|
||||
stream::copy_n(
|
||||
&mut orig_payload_reader,
|
||||
@@ -603,13 +603,13 @@ fn patch_ota_payload(
|
||||
data_length,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to copy from original payload: {name}"))?;
|
||||
.with_context(|| format!("Failed to copy from original payload: {name}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
let (_, properties, metadata_size) = payload_writer
|
||||
.finish()
|
||||
.with_context(|| anyhow!("Failed to finalize payload"))?;
|
||||
.context("Failed to finalize payload")?;
|
||||
|
||||
Ok((properties, metadata_size))
|
||||
}
|
||||
@@ -626,7 +626,7 @@ fn patch_ota_zip(
|
||||
key_avb: &RsaPrivateKey,
|
||||
key_ota: &RsaPrivateKey,
|
||||
cert_ota: &Certificate,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(OtaMetadata, u64)> {
|
||||
let mut missing = BTreeSet::from([
|
||||
ota::PATH_METADATA_PB,
|
||||
@@ -659,7 +659,7 @@ fn patch_ota_zip(
|
||||
for path in &paths {
|
||||
let mut reader = zip_reader
|
||||
.by_name(path)
|
||||
.with_context(|| anyhow!("Failed to open zip entry: {path}"))?;
|
||||
.with_context(|| format!("Failed to open zip entry: {path}"))?;
|
||||
|
||||
// Android's libarchive parser is broken and only reads data descriptor
|
||||
// size fields as 64-bit integers if the central directory says the file
|
||||
@@ -682,7 +682,7 @@ fn patch_ota_zip(
|
||||
let mut buf = vec![];
|
||||
reader
|
||||
.read_to_end(&mut buf)
|
||||
.with_context(|| anyhow!("Failed to read OTA metadata: {path}"))?;
|
||||
.with_context(|| format!("Failed to read OTA metadata: {path}"))?;
|
||||
metadata_pb_raw = Some(buf);
|
||||
continue;
|
||||
}
|
||||
@@ -692,10 +692,10 @@ fn patch_ota_zip(
|
||||
// All remaining entries are written immediately.
|
||||
zip_writer
|
||||
.start_file_with_extra_data(path, options)
|
||||
.with_context(|| anyhow!("Failed to begin new zip entry: {path}"))?;
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let offset = zip_writer
|
||||
.end_extra_data()
|
||||
.with_context(|| anyhow!("Failed to end new zip entry: {path}"))?;
|
||||
.with_context(|| format!("Failed to end new zip entry: {path}"))?;
|
||||
let mut writer = CountingWriter::new(&mut zip_writer);
|
||||
|
||||
match path.as_str() {
|
||||
@@ -704,7 +704,7 @@ fn patch_ota_zip(
|
||||
status!("Replacing zip entry: {path}");
|
||||
|
||||
crypto::write_pem_cert(&mut writer, cert_ota)
|
||||
.with_context(|| anyhow!("Failed to write entry: {path}"))?;
|
||||
.with_context(|| format!("Failed to write entry: {path}"))?;
|
||||
}
|
||||
ota::PATH_PAYLOAD => {
|
||||
status!("Patching zip entry: {path}");
|
||||
@@ -721,7 +721,7 @@ fn patch_ota_zip(
|
||||
// The zip library doesn't provide us with a seekable
|
||||
// reader, so we make our own from the underlying file.
|
||||
Ok(Box::new(SectionReader::new(
|
||||
BufReader::new(raw_reader.clone()),
|
||||
BufReader::new(raw_reader.reopen()),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)?))
|
||||
@@ -737,7 +737,7 @@ fn patch_ota_zip(
|
||||
cert_ota,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to patch payload: {path}"))?;
|
||||
.with_context(|| format!("Failed to patch payload: {path}"))?;
|
||||
|
||||
properties = Some(p);
|
||||
payload_metadata_size = Some(m);
|
||||
@@ -748,13 +748,13 @@ fn patch_ota_zip(
|
||||
// payload.bin is guaranteed to be patched first.
|
||||
writer
|
||||
.write_all(properties.as_ref().unwrap().as_bytes())
|
||||
.with_context(|| anyhow!("Failed to write payload properties: {path}"))?;
|
||||
.with_context(|| format!("Failed to write payload properties: {path}"))?;
|
||||
}
|
||||
_ => {
|
||||
status!("Copying zip entry: {path}");
|
||||
|
||||
stream::copy(&mut reader, &mut writer, cancel_signal)
|
||||
.with_context(|| anyhow!("Failed to copy zip entry: {path}"))?;
|
||||
.with_context(|| format!("Failed to copy zip entry: {path}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,19 +781,19 @@ fn patch_ota_zip(
|
||||
&metadata_pb_raw.unwrap(),
|
||||
payload_metadata_size.unwrap(),
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to write new OTA metadata"))?;
|
||||
.context("Failed to write new OTA metadata")?;
|
||||
|
||||
Ok((metadata, payload_metadata_size.unwrap()))
|
||||
}
|
||||
|
||||
fn extract_ota_zip(
|
||||
raw_reader: &PSeekFile,
|
||||
directory: &Path,
|
||||
directory: &Dir,
|
||||
payload_offset: u64,
|
||||
payload_size: u64,
|
||||
header: &PayloadHeader,
|
||||
images: &BTreeSet<String>,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
for name in images {
|
||||
if Path::new(name).file_name() != Some(OsStr::new(name)) {
|
||||
@@ -801,19 +801,17 @@ fn extract_ota_zip(
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(directory)
|
||||
.with_context(|| anyhow!("Failed to create directory: {directory:?}"))?;
|
||||
|
||||
status!("Extracting from the payload: {}", joined(images));
|
||||
|
||||
// Pre-open all output files.
|
||||
let output_files = images
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let path = directory.join(format!("{name}.img"));
|
||||
let file = File::create(&path)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| anyhow!("Failed to open for writing: {path:?}"))?;
|
||||
let path = format!("{name}.img");
|
||||
let file = directory
|
||||
.create(&path)
|
||||
.map(|f| PSeekFile::new(f.into_std()))
|
||||
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
|
||||
Ok((name.as_str(), file))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>>>()?;
|
||||
@@ -824,22 +822,22 @@ fn extract_ota_zip(
|
||||
payload::extract_images(
|
||||
|| {
|
||||
Ok(Box::new(SectionReader::new(
|
||||
BufReader::new(raw_reader.clone()),
|
||||
BufReader::new(raw_reader.reopen()),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)?))
|
||||
},
|
||||
|name| Ok(Box::new(BufWriter::new(output_files[name].clone()))),
|
||||
|name| Ok(Box::new(BufWriter::new(output_files[name].reopen()))),
|
||||
header,
|
||||
images.iter().map(|n| n.as_str()),
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to extract images from payload"))?;
|
||||
.context("Failed to extract images from payload")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let output = cli.output.as_ref().map_or_else(
|
||||
|| {
|
||||
let mut s = cli.input.clone().into_os_string();
|
||||
@@ -849,27 +847,23 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &Arc<AtomicBool>) -> Resu
|
||||
Cow::Borrowed,
|
||||
);
|
||||
|
||||
let passphrase_avb = if let Some(v) = &cli.pass_avb_env_var {
|
||||
PassphraseSource::EnvVar(v.clone())
|
||||
} else if let Some(p) = &cli.pass_avb_file {
|
||||
PassphraseSource::File(p.clone())
|
||||
} else {
|
||||
PassphraseSource::Prompt(format!("Enter passphrase for {:?}: ", cli.key_avb))
|
||||
};
|
||||
let passphrase_ota = if let Some(v) = &cli.pass_ota_env_var {
|
||||
PassphraseSource::EnvVar(v.clone())
|
||||
} else if let Some(p) = &cli.pass_ota_file {
|
||||
PassphraseSource::File(p.clone())
|
||||
} else {
|
||||
PassphraseSource::Prompt(format!("Enter passphrase for {:?}: ", cli.key_ota))
|
||||
};
|
||||
let source_avb = PassphraseSource::new(
|
||||
&cli.key_avb,
|
||||
cli.pass_avb_file.as_deref(),
|
||||
cli.pass_avb_env_var.as_deref(),
|
||||
);
|
||||
let source_ota = PassphraseSource::new(
|
||||
&cli.key_ota,
|
||||
cli.pass_ota_file.as_deref(),
|
||||
cli.pass_ota_env_var.as_deref(),
|
||||
);
|
||||
|
||||
let key_avb = crypto::read_pem_key_file(&cli.key_avb, &passphrase_avb)
|
||||
.with_context(|| anyhow!("Failed to load key: {:?}", cli.key_avb))?;
|
||||
let key_ota = crypto::read_pem_key_file(&cli.key_ota, &passphrase_ota)
|
||||
.with_context(|| anyhow!("Failed to load key: {:?}", cli.key_ota))?;
|
||||
let key_avb = crypto::read_pem_key_file(&cli.key_avb, &source_avb)
|
||||
.with_context(|| format!("Failed to load key: {:?}", cli.key_avb))?;
|
||||
let key_ota = crypto::read_pem_key_file(&cli.key_ota, &source_ota)
|
||||
.with_context(|| format!("Failed to load key: {:?}", cli.key_ota))?;
|
||||
let cert_ota = crypto::read_pem_cert_file(&cli.cert_ota)
|
||||
.with_context(|| anyhow!("Failed to load certificate: {:?}", cli.cert_ota))?;
|
||||
.with_context(|| format!("Failed to load certificate: {:?}", cli.cert_ota))?;
|
||||
|
||||
if !crypto::cert_matches_key(&cert_ota, &key_ota)? {
|
||||
bail!(
|
||||
@@ -900,7 +894,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &Arc<AtomicBool>) -> Resu
|
||||
cli.ignore_magisk_warnings,
|
||||
move |s| warning!("{s}"),
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to create Magisk boot image patcher"))?;
|
||||
.context("Failed to create Magisk boot image patcher")?;
|
||||
|
||||
Some(Box::new(patcher))
|
||||
} else if let Some(prepatched) = &cli.root.prepatched {
|
||||
@@ -918,18 +912,18 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &Arc<AtomicBool>) -> Resu
|
||||
|
||||
let raw_reader = File::open(&cli.input)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| anyhow!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.clone()))
|
||||
.with_context(|| anyhow!("Failed to read zip: {:?}", cli.input))?;
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()))
|
||||
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
|
||||
|
||||
// Open the output file for reading too, so we can verify offsets later.
|
||||
let temp_writer = NamedTempFile::with_prefix_in(
|
||||
output
|
||||
.file_name()
|
||||
.unwrap_or_else(|| OsStr::new("avbroot.tmp")),
|
||||
output.parent().unwrap_or_else(|| Path::new(".")),
|
||||
util::parent_path(&output),
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to open temporary output file"))?;
|
||||
.context("Failed to open temporary output file")?;
|
||||
let temp_path = temp_writer.path().to_owned();
|
||||
let hole_punching_writer = HolePunchingWriter::new(temp_writer);
|
||||
let buffered_writer = BufWriter::new(hole_punching_writer);
|
||||
@@ -949,31 +943,29 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &Arc<AtomicBool>) -> Resu
|
||||
&cert_ota,
|
||||
cancel_signal,
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to patch OTA zip"))?;
|
||||
.context("Failed to patch OTA zip")?;
|
||||
|
||||
let sign_writer = zip_writer
|
||||
let signing_writer = zip_writer
|
||||
.finish()
|
||||
.with_context(|| anyhow!("Failed to finalize output zip"))?;
|
||||
let buffered_writer = sign_writer
|
||||
.context("Failed to finalize output zip")?;
|
||||
let buffered_writer = signing_writer
|
||||
.finish(&key_ota, &cert_ota)
|
||||
.with_context(|| anyhow!("Failed to sign output zip"))?;
|
||||
.context("Failed to sign output zip")?;
|
||||
let hole_punching_writer = buffered_writer
|
||||
.into_inner()
|
||||
.with_context(|| anyhow!("Failed to flush output zip"))?;
|
||||
.context("Failed to flush output zip")?;
|
||||
let mut temp_writer = hole_punching_writer.into_inner();
|
||||
temp_writer
|
||||
.flush()
|
||||
.with_context(|| anyhow!("Failed to flush output zip"))?;
|
||||
temp_writer.flush().context("Failed to flush output zip")?;
|
||||
|
||||
// We do a lot of low-level hackery. Reopen and verify offsets.
|
||||
status!("Verifying metadata offsets");
|
||||
temp_writer.rewind()?;
|
||||
temp_writer.rewind().context("Failed to seek output zip")?;
|
||||
ota::verify_metadata(
|
||||
BufReader::new(&mut temp_writer),
|
||||
&metadata,
|
||||
payload_metadata_size,
|
||||
)
|
||||
.with_context(|| anyhow!("Failed to verify OTA metadata offsets"))?;
|
||||
.context("Failed to verify OTA metadata offsets")?;
|
||||
|
||||
status!("Completed after {:.1}s", start.elapsed().as_secs_f64());
|
||||
|
||||
@@ -996,37 +988,42 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &Arc<AtomicBool>) -> Resu
|
||||
temp_writer
|
||||
.as_file()
|
||||
.set_permissions(Permissions::from_mode(mode))
|
||||
.with_context(|| anyhow!("Failed to set permissions to {mode:o}: {temp_path:?}"))?;
|
||||
.with_context(|| format!("Failed to set permissions to {mode:o}: {temp_path:?}"))?;
|
||||
}
|
||||
|
||||
temp_writer.persist(output.as_ref()).with_context(|| {
|
||||
anyhow!("Failed to move temporary file to output path: {temp_path:?} -> {output:?}")
|
||||
format!("Failed to move temporary file to output path: {temp_path:?} -> {output:?}")
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let raw_reader = File::open(&cli.input)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| anyhow!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(raw_reader.clone()))
|
||||
.with_context(|| anyhow!("Failed to read zip: {:?}", cli.input))?;
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(raw_reader.reopen()))
|
||||
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
|
||||
let payload_entry = zip
|
||||
.by_name(ota::PATH_PAYLOAD)
|
||||
.with_context(|| anyhow!("Failed to open zip entry: {:?}", ota::PATH_PAYLOAD))?;
|
||||
.with_context(|| format!("Failed to open zip entry: {:?}", ota::PATH_PAYLOAD))?;
|
||||
let payload_offset = payload_entry.data_start();
|
||||
let payload_size = payload_entry.size();
|
||||
|
||||
// Open the payload data directly.
|
||||
let mut payload_reader = SectionReader::new(
|
||||
BufReader::new(raw_reader.clone()),
|
||||
BufReader::new(raw_reader.reopen()),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)?;
|
||||
)
|
||||
.context("Failed to directly open payload section")?;
|
||||
|
||||
let header = PayloadHeader::from_reader(&mut payload_reader)
|
||||
.with_context(|| anyhow!("Failed to load OTA payload header"))?;
|
||||
.context("Failed to load OTA payload header")?;
|
||||
if !header.is_full_ota() {
|
||||
bail!("Payload is a delta OTA, not a full OTA");
|
||||
}
|
||||
|
||||
let mut unique_images = BTreeSet::new();
|
||||
|
||||
if cli.all {
|
||||
@@ -1048,9 +1045,15 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &Arc<AtomicBool>) ->
|
||||
}
|
||||
}
|
||||
|
||||
let authority = ambient_authority();
|
||||
Dir::create_ambient_dir_all(&cli.directory, authority)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.directory))?;
|
||||
let directory = Dir::open_ambient_dir(&cli.directory, authority)
|
||||
.with_context(|| format!("Failed to open directory: {:?}", cli.directory))?;
|
||||
|
||||
extract_ota_zip(
|
||||
&raw_reader,
|
||||
&cli.directory,
|
||||
&directory,
|
||||
payload_offset,
|
||||
payload_size,
|
||||
&header,
|
||||
@@ -1061,10 +1064,10 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &Arc<AtomicBool>) ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let raw_reader = File::open(&cli.input)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| anyhow!("Failed to open for reading: {:?}", cli.input))?;
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut reader = BufReader::new(raw_reader);
|
||||
|
||||
status!("Verifying whole-file signature");
|
||||
@@ -1079,7 +1082,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &Arc<AtomicBool>) -> Re
|
||||
);
|
||||
} else if let Some(p) = &cli.cert_ota {
|
||||
let verify_cert = crypto::read_pem_cert_file(p)
|
||||
.with_context(|| anyhow!("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:?}");
|
||||
@@ -1089,7 +1092,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &Arc<AtomicBool>) -> Re
|
||||
}
|
||||
|
||||
ota::verify_metadata(&mut reader, &metadata, header.blob_offset)
|
||||
.with_context(|| anyhow!("Failed to verify OTA metadata offsets"))?;
|
||||
.context("Failed to verify OTA metadata offsets")?;
|
||||
|
||||
status!("Verifying payload");
|
||||
|
||||
@@ -1098,20 +1101,21 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &Arc<AtomicBool>) -> Re
|
||||
.get(ota::PF_NAME)
|
||||
.ok_or_else(|| anyhow!("Missing property files: {}", ota::PF_NAME))?;
|
||||
let pfs = ota::parse_property_files(pfs_raw)
|
||||
.with_context(|| anyhow!("Failed to parse property files: {}", ota::PF_NAME))?;
|
||||
.with_context(|| format!("Failed to parse property files: {}", ota::PF_NAME))?;
|
||||
let pf_payload = pfs
|
||||
.iter()
|
||||
.find(|pf| pf.name == ota::PATH_PAYLOAD)
|
||||
.ok_or_else(|| anyhow!("Missing property files entry: {}", ota::PATH_PAYLOAD))?;
|
||||
|
||||
let section_reader = SectionReader::new(&mut reader, pf_payload.offset, pf_payload.size)?;
|
||||
let section_reader = SectionReader::new(&mut reader, pf_payload.offset, pf_payload.size)
|
||||
.context("Failed to directly open payload section")?;
|
||||
|
||||
payload::verify_payload(section_reader, &ota_cert, &properties, cancel_signal)?;
|
||||
|
||||
status!("Extracting partition images to temporary directory");
|
||||
|
||||
let temp_dir =
|
||||
TempDir::new().with_context(|| anyhow!("Failed to create temporary directory"))?;
|
||||
let authority = ambient_authority();
|
||||
let temp_dir = TempDir::new(authority).context("Failed to create temporary directory")?;
|
||||
let raw_reader = reader.into_inner();
|
||||
let unique_images = header
|
||||
.manifest
|
||||
@@ -1123,7 +1127,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &Arc<AtomicBool>) -> Re
|
||||
|
||||
extract_ota_zip(
|
||||
&raw_reader,
|
||||
temp_dir.path(),
|
||||
&temp_dir,
|
||||
pf_payload.offset,
|
||||
pf_payload.size,
|
||||
&header,
|
||||
@@ -1135,17 +1139,16 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &Arc<AtomicBool>) -> Re
|
||||
|
||||
let boot_image = {
|
||||
let partitions_by_type = get_partitions_by_type(&header.manifest)?;
|
||||
let path = temp_dir
|
||||
.path()
|
||||
.join(format!("{}.img", partitions_by_type["@otacerts"]));
|
||||
let file =
|
||||
File::open(&path).with_context(|| anyhow!("Failed to open for reading: {path:?}"))?;
|
||||
let path = format!("{}.img", partitions_by_type["@otacerts"]);
|
||||
let file = temp_dir
|
||||
.open(&path)
|
||||
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
BootImage::from_reader(BufReader::new(file))
|
||||
.with_context(|| anyhow!("Failed to read boot image: {path:?}"))?
|
||||
.with_context(|| format!("Failed to read boot image: {path:?}"))?
|
||||
};
|
||||
|
||||
let ramdisk_certs = OtaCertPatcher::get_certificates(&boot_image)
|
||||
.with_context(|| anyhow!("Failed to read ramdisk's otacerts.zip"))?;
|
||||
let ramdisk_certs = OtaCertPatcher::get_certificates(&boot_image, cancel_signal)
|
||||
.context("Failed to read ramdisk's otacerts.zip")?;
|
||||
if !ramdisk_certs.contains(&ota_cert) {
|
||||
bail!("Ramdisk's otacerts.zip does not contain OTA certificate");
|
||||
}
|
||||
@@ -1153,9 +1156,9 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &Arc<AtomicBool>) -> Re
|
||||
status!("Verifying AVB signatures");
|
||||
|
||||
let public_key = if let Some(p) = &cli.public_key_avb {
|
||||
let data = fs::read(p).with_context(|| anyhow!("Failed to read file: {p:?}"))?;
|
||||
let data = fs::read(p).with_context(|| format!("Failed to read file: {p:?}"))?;
|
||||
let key = avb::decode_public_key(&data)
|
||||
.with_context(|| anyhow!("Failed to decode public key: {p:?}"))?;
|
||||
.with_context(|| format!("Failed to decode public key: {p:?}"))?;
|
||||
|
||||
Some(key)
|
||||
} else {
|
||||
@@ -1166,20 +1169,20 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &Arc<AtomicBool>) -> Re
|
||||
let mut descriptors = HashMap::<String, Descriptor>::new();
|
||||
|
||||
cli::avb::verify_headers(
|
||||
temp_dir.path(),
|
||||
&temp_dir,
|
||||
"vbmeta",
|
||||
public_key.as_ref(),
|
||||
&mut seen,
|
||||
&mut descriptors,
|
||||
)?;
|
||||
cli::avb::verify_descriptors(temp_dir.path(), &descriptors, cancel_signal)?;
|
||||
cli::avb::verify_descriptors(&temp_dir, &descriptors, false, cancel_signal)?;
|
||||
|
||||
status!("Signatures are all valid!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ota_main(cli: &OtaCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
pub fn ota_main(cli: &OtaCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
match &cli.command {
|
||||
OtaCommand::Patch(c) => patch_subcommand(c, cancel_signal),
|
||||
OtaCommand::Extract(c) => extract_subcommand(c, cancel_signal),
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
compression::{CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntryNew},
|
||||
},
|
||||
util::EscapedString,
|
||||
};
|
||||
|
||||
static CONTENT_BEGIN: &str = "----- BEGIN UTF-8 CONTENT -----";
|
||||
static CONTENT_END: &str = "----- END UTF-8 CONTENT -----";
|
||||
static CONTENT_END_NO_NEWLINE: &str = "----- END UTF-8 CONTENT (NO NEWLINE) -----";
|
||||
|
||||
static BINARY_BEGIN: &str = "----- BEGIN BINARY CONTENT -----";
|
||||
static BINARY_END: &str = "----- END BINARY CONTENT -----";
|
||||
static BINARY_END_TRUNCATED: &str = "----- END BINARY CONTENT (TRUNCATED) -----";
|
||||
|
||||
static NO_DATA: &str = "----- NO DATA -----";
|
||||
|
||||
fn print_content(data: &[u8], truncate: bool) {
|
||||
if data.is_empty() {
|
||||
println!("{NO_DATA}");
|
||||
return;
|
||||
}
|
||||
|
||||
if !data.contains(&b'\0') {
|
||||
if let Ok(s) = str::from_utf8(data) {
|
||||
if !s.contains(CONTENT_BEGIN)
|
||||
&& !s.contains(CONTENT_END)
|
||||
&& !s.contains(CONTENT_END_NO_NEWLINE)
|
||||
{
|
||||
println!("{CONTENT_BEGIN}");
|
||||
print!("{s}");
|
||||
if data.last() == Some(&b'\n') {
|
||||
println!("{CONTENT_END}");
|
||||
} else {
|
||||
println!();
|
||||
println!("{CONTENT_END_NO_NEWLINE}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{BINARY_BEGIN}");
|
||||
|
||||
if data.len() > 512 && truncate {
|
||||
println!("{}", EscapedString::new_unquoted(&data[..512]));
|
||||
println!("{BINARY_END_TRUNCATED}");
|
||||
} else {
|
||||
println!("{}", EscapedString::new_unquoted(&data));
|
||||
println!("{BINARY_END}");
|
||||
}
|
||||
}
|
||||
|
||||
fn load_archive(
|
||||
path: &Path,
|
||||
include_trailer: bool,
|
||||
) -> Result<(Vec<CpioEntryNew>, CompressedFormat)> {
|
||||
let file = File::open(path)?;
|
||||
let reader = CompressedReader::new(file, true)?;
|
||||
let format = reader.format();
|
||||
let entries = cpio::load(reader, include_trailer)?;
|
||||
|
||||
Ok((entries, format))
|
||||
}
|
||||
|
||||
fn save_archive(path: &Path, entries: &[CpioEntryNew], format: CompressedFormat) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let mut writer = CompressedWriter::new(file, format)?;
|
||||
cpio::save(&mut writer, entries, false)?;
|
||||
writer.finish()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ramdisk_main(cli: &RamdiskCli) -> Result<()> {
|
||||
match &cli.command {
|
||||
RamdiskCommand::Dump(c) => {
|
||||
let (entries, format) = load_archive(&c.input, true)
|
||||
.with_context(|| format!("Failed to read cpio: {:?}", c.input))?;
|
||||
|
||||
println!("Compression format: {format:?}");
|
||||
println!();
|
||||
|
||||
for entry in entries {
|
||||
println!("{entry}");
|
||||
print_content(&entry.content, !c.no_truncate);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
RamdiskCommand::Repack(c) => {
|
||||
let (entries, format) = load_archive(&c.input, false)
|
||||
.with_context(|| format!("Failed to read cpio: {:?}", c.input))?;
|
||||
|
||||
save_archive(&c.output, &entries, format)
|
||||
.with_context(|| format!("Failed to write cpio: {:?}", c.output))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dump cpio headers and data.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DumpCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Do not truncate binary file contents.
|
||||
#[arg(long)]
|
||||
no_truncate: bool,
|
||||
}
|
||||
|
||||
/// Repack cpio archive.
|
||||
#[derive(Debug, Parser)]
|
||||
struct RepackCli {
|
||||
/// Path to input cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
|
||||
/// Path to output cpio file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum RamdiskCommand {
|
||||
Dump(DumpCli),
|
||||
Repack(RepackCli),
|
||||
}
|
||||
|
||||
/// Show information about ramdisk cpio archives.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct RamdiskCli {
|
||||
#[command(subcommand)]
|
||||
command: RamdiskCommand,
|
||||
}
|
||||
+12
-2
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::{
|
||||
env::{self, VarError},
|
||||
ffi::OsString,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, BufReader, BufWriter, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
@@ -61,7 +61,7 @@ pub enum Error {
|
||||
#[error("DER error")]
|
||||
Der(#[from] x509_cert::der::Error),
|
||||
#[error("RSA error")]
|
||||
RsaSign(#[from] rsa::Error),
|
||||
Rsa(#[from] rsa::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
@@ -75,6 +75,16 @@ pub enum PassphraseSource {
|
||||
}
|
||||
|
||||
impl PassphraseSource {
|
||||
pub fn new(key_file: &Path, pass_file: Option<&Path>, env_var: Option<&OsStr>) -> Self {
|
||||
if let Some(v) = env_var {
|
||||
Self::EnvVar(v.to_owned())
|
||||
} else if let Some(p) = pass_file {
|
||||
Self::File(p.to_owned())
|
||||
} else {
|
||||
Self::Prompt(format!("Enter passphrase for {key_file:?}: "))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acquire(&self, confirm: bool) -> Result<String> {
|
||||
let passphrase = match self {
|
||||
Self::Prompt(p) => {
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{fmt, marker::PhantomData};
|
||||
|
||||
use bstr::{ByteSlice, ByteVec};
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Decoded string size ({actual}) does not match expected size ({expected})")]
|
||||
BadLength { expected: usize, actual: usize },
|
||||
}
|
||||
|
||||
pub trait FromEscaped: Sized {
|
||||
type Error;
|
||||
|
||||
fn from_escaped(data: &str) -> Result<Self, Self::Error>;
|
||||
}
|
||||
|
||||
impl FromEscaped for Vec<u8> {
|
||||
type Error = Error;
|
||||
|
||||
fn from_escaped(data: &str) -> Result<Self, Self::Error> {
|
||||
Ok(Self::unescape_bytes(data))
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> FromEscaped for [u8; N] {
|
||||
type Error = Error;
|
||||
|
||||
fn from_escaped(data: &str) -> Result<Self, Self::Error> {
|
||||
// Wasteful allocation, but bstr doesn't expose its decoder iterator in
|
||||
// its public API.
|
||||
let decoded = Vec::<u8>::from_escaped(data)?;
|
||||
let mut buf = [0u8; N];
|
||||
|
||||
if decoded.len() != buf.len() {
|
||||
return Err(Error::BadLength {
|
||||
expected: buf.len(),
|
||||
actual: decoded.len(),
|
||||
});
|
||||
}
|
||||
|
||||
buf.copy_from_slice(&decoded);
|
||||
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize<S, T>(data: T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let s = data.as_ref().escape_bytes().to_string();
|
||||
serializer.serialize_str(&s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: FromEscaped,
|
||||
<T as FromEscaped>::Error: fmt::Display,
|
||||
{
|
||||
struct EscapedStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for EscapedStrVisitor<T>
|
||||
where
|
||||
T: FromEscaped,
|
||||
<T as FromEscaped>::Error: fmt::Display,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "an escaped string")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, data: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
FromEscaped::from_escaped(data).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(EscapedStrVisitor(PhantomData))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use assert_matches::assert_matches;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn decode_vec() {
|
||||
for s in ["", "abc", "你好", "💩", "\x00", "\t\r\n"] {
|
||||
let escaped_s = s.as_bytes().escape_bytes().to_string();
|
||||
let decoded = Vec::<u8>::from_escaped(&escaped_s).unwrap();
|
||||
|
||||
assert_eq!(decoded, s.as_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_array() {
|
||||
assert_matches!(
|
||||
<[u8; 4]>::from_escaped(r"\t\r\n"),
|
||||
Err(Error::BadLength {
|
||||
expected: 4,
|
||||
actual: 3,
|
||||
})
|
||||
);
|
||||
assert_matches!(
|
||||
<[u8; 4]>::from_escaped(r"\t\r\n\x00\x00"),
|
||||
Err(Error::BadLength {
|
||||
expected: 4,
|
||||
actual: 5,
|
||||
})
|
||||
);
|
||||
assert_matches!(
|
||||
<[u8; 4]>::from_escaped(r"\t\r\n\x00"),
|
||||
Ok(data) if data == *b"\t\r\n\x00"
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
<[u8; 0]>::from_escaped(r"\t"),
|
||||
Err(Error::BadLength {
|
||||
expected: 0,
|
||||
actual: 1,
|
||||
})
|
||||
);
|
||||
assert_matches!(
|
||||
<[u8; 0]>::from_escaped(r""),
|
||||
Ok(data) if data.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_serde() {
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct TestData {
|
||||
#[serde(with = "super")]
|
||||
contents: Vec<u8>,
|
||||
}
|
||||
|
||||
let mut contents = b"foo\xffbar".to_vec();
|
||||
contents.extend("💩".as_bytes());
|
||||
|
||||
let data = TestData { contents };
|
||||
let serialized = toml_edit::ser::to_string(&data).unwrap();
|
||||
|
||||
assert_eq!(serialized, "contents = 'foo\\xFFbar💩'\n");
|
||||
|
||||
let new_data: TestData = toml_edit::de::from_str(&serialized).unwrap();
|
||||
assert_eq!(data.contents, new_data.contents);
|
||||
}
|
||||
}
|
||||
+683
-363
File diff suppressed because it is too large
Load Diff
+195
-108
@@ -57,6 +57,15 @@ const VENDOR_HDR_V3_SIZE: u32 = 2112;
|
||||
const VENDOR_HDR_V4_EXTRA_SIZE: u32 = 16;
|
||||
const VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE: u32 = 108;
|
||||
|
||||
/// Maximum size of any individual boot image component, like the kernel. This
|
||||
/// limit is currently 64 MiB, which should be sufficient since there is no
|
||||
/// known device where the entire boot image exceeds this size.
|
||||
const COMPONENT_MAX_SIZE: u32 = 64 * 1024 * 1024;
|
||||
/// Maximum size of the bootconfig component in vendor v4 images. This limit is
|
||||
/// currently 1 KiB, which is ~25x the size of the Pixel 7 Pro stock image's
|
||||
/// bootconfig.
|
||||
const BOOTCONFIG_MAX_SIZE: u32 = 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown boot image format")]
|
||||
@@ -71,8 +80,8 @@ pub enum Error {
|
||||
WriteFieldError(&'static str, #[source] io::Error),
|
||||
#[error("{0:?} field: invalid value: {1}")]
|
||||
InvalidFieldValue(&'static str, u32),
|
||||
#[error("{0:?} field exceeds integer bounds")]
|
||||
IntegerTooLarge(&'static str),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("Invalid data: {0}")]
|
||||
InvalidData(&'static str),
|
||||
#[error("VTS signature is missing hash descriptor")]
|
||||
@@ -257,6 +266,16 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
return Err(Error::UnknownHeaderVersion(header_version));
|
||||
}
|
||||
|
||||
if kernel_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("kernel_size"));
|
||||
} else if ramdisk_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
} else if second_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("second_size"));
|
||||
} else if page_size == 0 {
|
||||
return Err(Error::InvalidFieldValue("page_size", 0));
|
||||
}
|
||||
|
||||
let os_version = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let name = reader
|
||||
@@ -281,6 +300,10 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
|
||||
let mut v1_data = if header_version >= 1 {
|
||||
let recovery_dtbo_size = reader.read_u32::<LittleEndian>()?;
|
||||
if recovery_dtbo_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("recovery_dtbo_size"));
|
||||
}
|
||||
|
||||
let recovery_dtbo_offset = reader.read_u64::<LittleEndian>()?;
|
||||
let header_size = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
@@ -305,6 +328,10 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
|
||||
let mut v2_data = if header_version == 2 {
|
||||
let dtb_size = reader.read_u32::<LittleEndian>()?;
|
||||
if dtb_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("dtb_size"));
|
||||
}
|
||||
|
||||
let dtb_addr = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
let v2_extra = V2Extra {
|
||||
@@ -329,29 +356,39 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
|
||||
let mut ramdisk = vec![];
|
||||
let mut second = vec![];
|
||||
|
||||
kernel.resize(kernel_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut kernel)?;
|
||||
kernel.resize(kernel_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut kernel)
|
||||
.map_err(|e| Error::ReadFieldError("kernel", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
ramdisk.resize(ramdisk_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut ramdisk)?;
|
||||
ramdisk.resize(ramdisk_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut ramdisk)
|
||||
.map_err(|e| Error::ReadFieldError("ramdisk", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
second.resize(second_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut second)?;
|
||||
second.resize(second_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut second)
|
||||
.map_err(|e| Error::ReadFieldError("second", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
if let Some(v1) = &mut v1_data {
|
||||
v1.v1_extra
|
||||
.recovery_dtbo
|
||||
.resize(v1.recovery_dtbo_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut v1.v1_extra.recovery_dtbo)?;
|
||||
.resize(v1.recovery_dtbo_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut v1.v1_extra.recovery_dtbo)
|
||||
.map_err(|e| Error::ReadFieldError("recovery_dtbo", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
}
|
||||
|
||||
if let Some(v2) = &mut v2_data {
|
||||
v2.v2_extra.dtb.resize(v2.dtb_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut v2.v2_extra.dtb)?;
|
||||
v2.v2_extra.dtb.resize(v2.dtb_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut v2.v2_extra.dtb)
|
||||
.map_err(|e| Error::ReadFieldError("dtb", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
}
|
||||
|
||||
@@ -381,30 +418,35 @@ impl<W: Write> ToWriter<W> for BootImageV0Through2 {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, writer: W) -> Result<()> {
|
||||
if self.kernel.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("kernel_size"));
|
||||
} else if self.ramdisk.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
} else if self.second.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("second_size"));
|
||||
} else if self.page_size == 0 {
|
||||
return Err(Error::InvalidFieldValue("page_size", 0));
|
||||
}
|
||||
|
||||
if let Some(v1) = &self.v1_extra {
|
||||
if v1.recovery_dtbo.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("recovery_dtbo_size"));
|
||||
}
|
||||
}
|
||||
if let Some(v2) = &self.v2_extra {
|
||||
if v2.dtb.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("dtb_size"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
let kernel_size = self
|
||||
.kernel
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("kernel_size"))?;
|
||||
let ramdisk_size = self
|
||||
.ramdisk
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("ramdisk_size"))?;
|
||||
let second_size = self
|
||||
.second
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("second_size"))?;
|
||||
|
||||
writer.write_all(&BOOT_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(kernel_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.kernel.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.kernel_addr)?;
|
||||
writer.write_u32::<LittleEndian>(ramdisk_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.ramdisk.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.ramdisk_addr)?;
|
||||
writer.write_u32::<LittleEndian>(second_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.second.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.second_addr)?;
|
||||
writer.write_u32::<LittleEndian>(self.tags_addr)?;
|
||||
writer.write_u32::<LittleEndian>(self.page_size)?;
|
||||
@@ -427,46 +469,44 @@ impl<W: Write> ToWriter<W> for BootImageV0Through2 {
|
||||
.map_err(|e| Error::WriteFieldError("extra_cmdline", e))?;
|
||||
|
||||
if let Some(v1) = &self.v1_extra {
|
||||
let recovery_dtbo_size = v1
|
||||
.recovery_dtbo
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("recovery_dtbo_size"))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(recovery_dtbo_size)?;
|
||||
writer.write_u32::<LittleEndian>(v1.recovery_dtbo.len() as u32)?;
|
||||
writer.write_u64::<LittleEndian>(v1.recovery_dtbo_offset)?;
|
||||
writer.write_u32::<LittleEndian>(self.header_size())?;
|
||||
}
|
||||
|
||||
if let Some(v2) = &self.v2_extra {
|
||||
let dtb_size = v2
|
||||
.dtb
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("dtb_size"))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(dtb_size)?;
|
||||
writer.write_u32::<LittleEndian>(v2.dtb.len() as u32)?;
|
||||
writer.write_u64::<LittleEndian>(v2.dtb_addr)?;
|
||||
}
|
||||
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(&self.kernel)?;
|
||||
writer
|
||||
.write_all(&self.kernel)
|
||||
.map_err(|e| Error::WriteFieldError("kernel", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(&self.ramdisk)?;
|
||||
writer
|
||||
.write_all(&self.ramdisk)
|
||||
.map_err(|e| Error::WriteFieldError("ramdisk", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(&self.second)?;
|
||||
writer
|
||||
.write_all(&self.second)
|
||||
.map_err(|e| Error::WriteFieldError("second", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
if let Some(v1) = &self.v1_extra {
|
||||
writer.write_all(&v1.recovery_dtbo)?;
|
||||
writer
|
||||
.write_all(&v1.recovery_dtbo)
|
||||
.map_err(|e| Error::WriteFieldError("recovery_dtbo", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
}
|
||||
|
||||
if let Some(v2) = &self.v2_extra {
|
||||
writer.write_all(&v2.dtb)?;
|
||||
writer
|
||||
.write_all(&v2.dtb)
|
||||
.map_err(|e| Error::WriteFieldError("dtb", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
}
|
||||
|
||||
@@ -572,12 +612,23 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
|
||||
return Err(Error::UnknownHeaderVersion(header_version));
|
||||
}
|
||||
|
||||
if kernel_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("kernel_size"));
|
||||
} else if ramdisk_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
}
|
||||
|
||||
let cmdline = reader
|
||||
.read_string_padded(BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE)
|
||||
.map_err(|e| Error::ReadFieldError("cmdline", e))?;
|
||||
|
||||
let signature_size = if header_version == 4 {
|
||||
Some(reader.read_u32::<LittleEndian>()?)
|
||||
let signature_size = reader.read_u32::<LittleEndian>()?;
|
||||
if signature_size > HDR_V4_SIGNATURE_SIZE as u32 {
|
||||
return Err(Error::FieldOutOfBounds("signature_size"));
|
||||
}
|
||||
|
||||
Some(signature_size)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -591,20 +642,26 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
|
||||
let mut kernel = vec![];
|
||||
let mut ramdisk = vec![];
|
||||
|
||||
kernel.resize(kernel_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut kernel)?;
|
||||
kernel.resize(kernel_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut kernel)
|
||||
.map_err(|e| Error::ReadFieldError("kernel", e))?;
|
||||
padding::read_discard(&mut reader, PAGE_SIZE.into())?;
|
||||
|
||||
ramdisk.resize(ramdisk_size.to_usize().unwrap(), 0);
|
||||
reader.read_exact(&mut ramdisk)?;
|
||||
ramdisk.resize(ramdisk_size as usize, 0);
|
||||
reader
|
||||
.read_exact(&mut ramdisk)
|
||||
.map_err(|e| Error::ReadFieldError("ramdisk", e))?;
|
||||
padding::read_discard(&mut reader, PAGE_SIZE.into())?;
|
||||
|
||||
// Don't preserve the signature. It is only used for VTS tests and is
|
||||
// not relevant for booting.
|
||||
let v4_extra = if let Some(s) = signature_size {
|
||||
// OnePlus images have an invalid signature consisting of all zeros.
|
||||
let mut data = vec![0u8; s.to_usize().unwrap()];
|
||||
reader.read_exact(&mut data)?;
|
||||
let mut data = vec![0u8; s as usize];
|
||||
reader
|
||||
.read_exact(&mut data)
|
||||
.map_err(|e| Error::ReadFieldError("signature", e))?;
|
||||
|
||||
let signature = if s > 0 && !util::is_zero(&data) {
|
||||
Some(Header::from_reader(Cursor::new(data))?)
|
||||
@@ -634,22 +691,17 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
|
||||
|
||||
impl BootImageV3Through4 {
|
||||
fn to_writer_internal(&self, writer: impl Write, skip_v4_sig: bool) -> Result<()> {
|
||||
if self.kernel.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("kernel_size"));
|
||||
} else if self.ramdisk.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
}
|
||||
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
let kernel_size = self
|
||||
.kernel
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("kernel_size"))?;
|
||||
let ramdisk_size = self
|
||||
.ramdisk
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("ramdisk_size"))?;
|
||||
|
||||
writer.write_all(&BOOT_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(kernel_size)?;
|
||||
writer.write_u32::<LittleEndian>(ramdisk_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.kernel.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.ramdisk.len() as u32)?;
|
||||
writer.write_u32::<LittleEndian>(self.os_version)?;
|
||||
writer.write_u32::<LittleEndian>(self.header_size())?;
|
||||
|
||||
@@ -673,7 +725,7 @@ impl BootImageV3Through4 {
|
||||
|
||||
// The VTS signature is always a fixed size.
|
||||
if size > HDR_V4_SIGNATURE_SIZE {
|
||||
return Err(Error::IntegerTooLarge("signature_size"));
|
||||
return Err(Error::FieldOutOfBounds("signature_size"));
|
||||
}
|
||||
|
||||
padding::write_zeros(&mut sig_writer, HDR_V4_SIGNATURE_SIZE)?;
|
||||
@@ -688,15 +740,21 @@ impl BootImageV3Through4 {
|
||||
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
|
||||
|
||||
writer.write_all(&self.kernel)?;
|
||||
writer
|
||||
.write_all(&self.kernel)
|
||||
.map_err(|e| Error::WriteFieldError("kernel", e))?;
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
|
||||
|
||||
writer.write_all(&self.ramdisk)?;
|
||||
writer
|
||||
.write_all(&self.ramdisk)
|
||||
.map_err(|e| Error::WriteFieldError("ramdisk", e))?;
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
|
||||
|
||||
if !skip_v4_sig {
|
||||
if let Some(sig) = v4_signature {
|
||||
writer.write_all(&sig)?;
|
||||
writer
|
||||
.write_all(&sig)
|
||||
.map_err(|e| Error::WriteFieldError("signature", e))?;
|
||||
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
|
||||
}
|
||||
}
|
||||
@@ -910,9 +968,17 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
}
|
||||
|
||||
let page_size = reader.read_u32::<LittleEndian>()?;
|
||||
if page_size == 0 {
|
||||
return Err(Error::InvalidFieldValue("page_size", 0));
|
||||
}
|
||||
|
||||
let kernel_addr = reader.read_u32::<LittleEndian>()?;
|
||||
let ramdisk_addr = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let vendor_ramdisk_size = reader.read_u32::<LittleEndian>()?;
|
||||
if vendor_ramdisk_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("vendor_ramdisk_size"));
|
||||
}
|
||||
|
||||
let cmdline = reader
|
||||
.read_string_padded(VENDOR_BOOT_ARGS_SIZE)
|
||||
@@ -925,7 +991,12 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
.map_err(|e| Error::ReadFieldError("name", e))?;
|
||||
|
||||
let header_size = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let dtb_size = reader.read_u32::<LittleEndian>()?;
|
||||
if dtb_size > COMPONENT_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("dtb_size"));
|
||||
}
|
||||
|
||||
let dtb_addr = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
struct V4Data {
|
||||
@@ -938,14 +1009,18 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
let table_size = reader.read_u32::<LittleEndian>()?;
|
||||
let table_entry_num = reader.read_u32::<LittleEndian>()?;
|
||||
let table_entry_size = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let bootconfig_size = reader.read_u32::<LittleEndian>()?;
|
||||
if bootconfig_size > BOOTCONFIG_MAX_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("bootconfig_size"));
|
||||
}
|
||||
|
||||
if table_entry_size != VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE {
|
||||
return Err(Error::InvalidFieldValue(
|
||||
"vendor_ramdisk_table_entry_size",
|
||||
table_entry_size,
|
||||
));
|
||||
} else if table_size != table_entry_num * table_entry_size {
|
||||
} else if table_entry_num.checked_mul(table_entry_size) != Some(table_size) {
|
||||
return Err(Error::InvalidFieldValue(
|
||||
"vendor_ramdisk_table_size",
|
||||
table_size,
|
||||
@@ -972,8 +1047,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
|
||||
let mut ramdisks = vec![];
|
||||
|
||||
let mut vendor_ramdisk_data = vec![0u8; vendor_ramdisk_size.to_usize().unwrap()];
|
||||
reader.read_exact(&mut vendor_ramdisk_data)?;
|
||||
let mut vendor_ramdisk_data = vec![0u8; vendor_ramdisk_size as usize];
|
||||
reader
|
||||
.read_exact(&mut vendor_ramdisk_data)
|
||||
.map_err(|e| Error::ReadFieldError("vendor_ramdisk_data", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
// For v3, this is just one big ramdisk. For v4, we have to wait until
|
||||
@@ -984,8 +1061,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
vendor_ramdisk_data = vec![];
|
||||
}
|
||||
|
||||
let mut dtb = vec![0u8; dtb_size.to_usize().unwrap()];
|
||||
reader.read_exact(&mut dtb)?;
|
||||
let mut dtb = vec![0u8; dtb_size as usize];
|
||||
reader
|
||||
.read_exact(&mut dtb)
|
||||
.map_err(|e| Error::ReadFieldError("dtb", e))?;
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
if let Some(v4) = &mut v4_data {
|
||||
@@ -994,6 +1073,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
|
||||
for _ in 0..v4.vendor_ramdisk_table_entry_num {
|
||||
let ramdisk_size = reader.read_u32::<LittleEndian>()?;
|
||||
if ramdisk_size > vendor_ramdisk_size {
|
||||
return Err(Error::FieldOutOfBounds("ramdisk_size"));
|
||||
}
|
||||
|
||||
let ramdisk_offset = reader.read_u32::<LittleEndian>()?;
|
||||
let ramdisk_type = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
@@ -1010,7 +1093,7 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
return Err(Error::InvalidFieldValue("ramdisk_offset", ramdisk_offset));
|
||||
}
|
||||
|
||||
let mut ramdisk = vec![0u8; ramdisk_size.to_usize().unwrap()];
|
||||
let mut ramdisk = vec![0u8; ramdisk_size as usize];
|
||||
ramdisk_reader.read_exact(&mut ramdisk)?;
|
||||
ramdisks.push(ramdisk);
|
||||
|
||||
@@ -1033,7 +1116,7 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
|
||||
padding::read_discard(&mut reader, page_size.into())?;
|
||||
|
||||
v4.v4_extra.bootconfig = reader
|
||||
.read_string_padded(v4.bootconfig_size.to_usize().unwrap())
|
||||
.read_string_padded(v4.bootconfig_size as usize)
|
||||
.map_err(|e| Error::ReadFieldError("bootconfig", e))?;
|
||||
}
|
||||
|
||||
@@ -1070,27 +1153,29 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
return Err(Error::InvalidData("v3 only supports one ramdisk"));
|
||||
}
|
||||
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
let vendor_ramdisk_size = self.ramdisks.iter().map(|r| r.len()).sum::<usize>();
|
||||
if vendor_ramdisk_size > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("vendor_ramdisk_size"));
|
||||
} else if self.dtb.len() > COMPONENT_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("dtb_size"));
|
||||
} else if self.page_size == 0 {
|
||||
return Err(Error::InvalidFieldValue("page_size", 0));
|
||||
}
|
||||
|
||||
let vendor_ramdisk_size = self
|
||||
.ramdisks
|
||||
.iter()
|
||||
.map(|r| r.len())
|
||||
.sum::<usize>()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_size"))?;
|
||||
let dtb_size = self
|
||||
.dtb
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("dtb_size"))?;
|
||||
if let Some(v4) = &self.v4_extra {
|
||||
if v4.bootconfig.len() > BOOTCONFIG_MAX_SIZE as usize {
|
||||
return Err(Error::FieldOutOfBounds("bootconfig_size"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
writer.write_all(&VENDOR_BOOT_MAGIC)?;
|
||||
writer.write_u32::<LittleEndian>(self.header_version())?;
|
||||
writer.write_u32::<LittleEndian>(self.page_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.kernel_addr)?;
|
||||
writer.write_u32::<LittleEndian>(self.ramdisk_addr)?;
|
||||
writer.write_u32::<LittleEndian>(vendor_ramdisk_size)?;
|
||||
writer.write_u32::<LittleEndian>(vendor_ramdisk_size as u32)?;
|
||||
|
||||
writer
|
||||
.write_string_padded(&self.cmdline, VENDOR_BOOT_ARGS_SIZE)
|
||||
@@ -1103,7 +1188,7 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
.map_err(|e| Error::WriteFieldError("name", e))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(self.header_size())?;
|
||||
writer.write_u32::<LittleEndian>(dtb_size)?;
|
||||
writer.write_u32::<LittleEndian>(self.dtb.len() as u32)?;
|
||||
writer.write_u64::<LittleEndian>(self.dtb_addr)?;
|
||||
|
||||
if let Some(v4) = &self.v4_extra {
|
||||
@@ -1111,38 +1196,37 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
.ramdisks
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_table_entry_num"))?;
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("vendor_ramdisk_table_entry_num"))?;
|
||||
let vendor_ramdisk_table_size = vendor_ramdisk_table_entry_num
|
||||
.checked_mul(VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE)
|
||||
.and_then(|v| v.to_u32())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_table_size"))?;
|
||||
let bootconfig_size = v4
|
||||
.bootconfig
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("bootconfig"))?;
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("vendor_ramdisk_table_size"))?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(vendor_ramdisk_table_size)?;
|
||||
writer.write_u32::<LittleEndian>(vendor_ramdisk_table_entry_num)?;
|
||||
writer.write_u32::<LittleEndian>(VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE)?;
|
||||
writer.write_u32::<LittleEndian>(bootconfig_size)?;
|
||||
writer.write_u32::<LittleEndian>(v4.bootconfig.len() as u32)?;
|
||||
}
|
||||
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
for ramdisk in &self.ramdisks {
|
||||
writer.write_all(ramdisk)?;
|
||||
writer
|
||||
.write_all(ramdisk)
|
||||
.map_err(|e| Error::WriteFieldError("vendor_ramdisk_data", e))?;
|
||||
}
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(&self.dtb)?;
|
||||
writer
|
||||
.write_all(&self.dtb)
|
||||
.map_err(|e| Error::WriteFieldError("dtb", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
if let Some(v4) = &self.v4_extra {
|
||||
let mut ramdisk_offset = 0;
|
||||
|
||||
for (ramdisk, meta) in self.ramdisks.iter().zip(&v4.ramdisk_metas) {
|
||||
let ramdisk_size = ramdisk.len().to_u32().unwrap();
|
||||
let ramdisk_size = ramdisk.len() as u32;
|
||||
|
||||
writer.write_u32::<LittleEndian>(ramdisk_size)?;
|
||||
writer.write_u32::<LittleEndian>(ramdisk_offset)?;
|
||||
@@ -1160,7 +1244,9 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
}
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
|
||||
writer.write_all(v4.bootconfig.as_bytes())?;
|
||||
writer
|
||||
.write_all(v4.bootconfig.as_bytes())
|
||||
.map_err(|e| Error::WriteFieldError("bootconfig", e))?;
|
||||
padding::write_zeros(&mut writer, self.page_size.into())?;
|
||||
}
|
||||
|
||||
@@ -1169,6 +1255,7 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum BootImage {
|
||||
V0Through2(BootImageV0Through2),
|
||||
V3Through4(BootImageV3Through4),
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::io::{self, Read, Seek, Write};
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use lz4_flex::frame::FrameDecoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||
@@ -18,7 +19,7 @@ pub enum Error {
|
||||
#[error("Unknown compression format")]
|
||||
UnknownFormat,
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] io::Error),
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -96,7 +97,7 @@ impl<W: Write> Write for Lz4LegacyEncoder<W> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum CompressedFormat {
|
||||
None,
|
||||
Gzip,
|
||||
|
||||
+557
-138
@@ -4,18 +4,26 @@
|
||||
*/
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
io::{self, Read, Write},
|
||||
io::{self, Cursor, Read, Write},
|
||||
ops::Range,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use num_traits::ToPrimitive;
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::{ToPrimitive, Zero};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
escape,
|
||||
format::padding,
|
||||
stream::{CountingReader, CountingWriter, FromReader, ToWriter, WriteZerosExt},
|
||||
util::{EscapedString, NumBytes},
|
||||
octal,
|
||||
stream::{
|
||||
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt,
|
||||
},
|
||||
util::NumBytes,
|
||||
};
|
||||
|
||||
const MAGIC_NEW: &[u8; 6] = b"070701";
|
||||
@@ -34,16 +42,24 @@ const C_ISCTG: u32 = 0o110000;
|
||||
|
||||
const IO_BLOCK_SIZE: u64 = 512;
|
||||
|
||||
/// The threshold when reading data where memory allocation switches from
|
||||
/// allocating the exact size to resizing as necessary.
|
||||
const VEC_CAP_THRESHOLD: usize = 16384;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown magic: {0:?}")]
|
||||
UnknownMagic([u8; 6]),
|
||||
#[error("Hard links are not supported: {0}")]
|
||||
HardLinksNotSupported(EscapedString<Vec<u8>>),
|
||||
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
|
||||
HardLinksNotSupported(Vec<u8>),
|
||||
#[error("Entry of type {0} should not have data: {:?}", .1.as_bstr())]
|
||||
EntryHasData(CpioEntryType, Vec<u8>),
|
||||
#[error("No inodes available for device {0:x},{1:x}")]
|
||||
DeviceFull(u32, u32),
|
||||
#[error("{0:?} field exceeds integer bounds")]
|
||||
IntegerTooLarge(&'static str),
|
||||
#[error("I/O error")]
|
||||
IoError(#[from] io::Error),
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -60,7 +76,7 @@ fn read_int(mut reader: impl Read) -> io::Result<u32> {
|
||||
let digit = c.to_digit(16).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("{0}: Invalid hex char: {1}", EscapedString::new(&buf), c),
|
||||
format!("{:?}: Invalid hex char: {c}", buf.as_bstr()),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -85,32 +101,200 @@ fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
|
||||
writer.write_all(&buf)
|
||||
}
|
||||
|
||||
fn file_type(mode: u32) -> u32 {
|
||||
mode & 0o170000
|
||||
/// Read a chunk of bytes from the reader. If `size` is less than
|
||||
/// [`VEC_CAP_THRESHOLD`], then the buffer is allocated with the exact size.
|
||||
/// Otherwise, the buffer starts with a capacity of [`VEC_CAP_THRESHOLD`] and
|
||||
/// grows as necessary. This avoids allocating excessive memory when the entry
|
||||
/// specifies an excessively large value that's not backed by actual data.
|
||||
fn read_data(reader: impl Read, size: usize, cancel_signal: &AtomicBool) -> io::Result<Vec<u8>> {
|
||||
let buf = Vec::with_capacity(size.min(VEC_CAP_THRESHOLD));
|
||||
let mut cursor = Cursor::new(buf);
|
||||
|
||||
stream::copy_n(reader, &mut cursor, size as u64, cancel_signal)?;
|
||||
|
||||
Ok(cursor.into_inner())
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq)]
|
||||
pub struct CpioEntryNew {
|
||||
pub ino: u32,
|
||||
pub mode: u32,
|
||||
pub uid: u32,
|
||||
pub gid: u32,
|
||||
pub nlink: u32,
|
||||
pub mtime: u32,
|
||||
pub dev_maj: u32,
|
||||
pub dev_min: u32,
|
||||
pub rdev_maj: u32,
|
||||
pub rdev_min: u32,
|
||||
pub chksum: u32,
|
||||
pub name: Vec<u8>,
|
||||
pub content: Vec<u8>,
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum CpioEntryType {
|
||||
Pipe,
|
||||
Char,
|
||||
Directory,
|
||||
Block,
|
||||
Regular,
|
||||
Symlink,
|
||||
Socket,
|
||||
Reserved,
|
||||
Unknown(u16),
|
||||
}
|
||||
|
||||
impl fmt::Debug for CpioEntryNew {
|
||||
impl CpioEntryType {
|
||||
pub fn from_mode(mode: u32) -> Self {
|
||||
match mode & 0o170000 {
|
||||
S_IFIFO => Self::Pipe,
|
||||
S_IFCHR => Self::Char,
|
||||
S_IFDIR => Self::Directory,
|
||||
S_IFBLK => Self::Block,
|
||||
S_IFREG => Self::Regular,
|
||||
S_IFLNK => Self::Symlink,
|
||||
S_IFSOCK => Self::Socket,
|
||||
C_ISCTG => Self::Reserved,
|
||||
m => Self::Unknown(m as u16),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_mode(self) -> u32 {
|
||||
match self {
|
||||
Self::Pipe => S_IFIFO,
|
||||
Self::Char => S_IFCHR,
|
||||
Self::Directory => S_IFDIR,
|
||||
Self::Block => S_IFBLK,
|
||||
Self::Regular => S_IFREG,
|
||||
Self::Symlink => S_IFLNK,
|
||||
Self::Socket => S_IFSOCK,
|
||||
Self::Reserved => C_ISCTG,
|
||||
Self::Unknown(m) => m.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CpioEntryType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("CpioEntryNew")
|
||||
.field("ino", &self.ino)
|
||||
.field("mode", &self.mode)
|
||||
match self {
|
||||
Self::Pipe => write!(f, "pipe"),
|
||||
Self::Char => write!(f, "character device"),
|
||||
Self::Directory => write!(f, "directory"),
|
||||
Self::Block => write!(f, "block device"),
|
||||
Self::Regular => write!(f, "regular file"),
|
||||
Self::Symlink => write!(f, "symbolic link"),
|
||||
Self::Socket => write!(f, "socket"),
|
||||
Self::Reserved => write!(f, "reserved"),
|
||||
Self::Unknown(m) => write!(f, "unknown ({m:o})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CpioEntryType {
|
||||
fn default() -> Self {
|
||||
Self::Unknown(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum CpioEntryData {
|
||||
/// Size of entry's data. [`CpioReader`] and [`CpioWriter`] use this for
|
||||
/// [`CpioEntryType::Regular`] entries to allow for lazy reads and writes.
|
||||
Size(u32),
|
||||
/// Entry's data. For [`CpioEntryType::Symlink`] entries, this is the
|
||||
/// link target. For [`CpioEntryType::Regular`] entries, this is the file
|
||||
/// content. [`CpioReader`] will never use this when reading regular files.
|
||||
/// [`CpioWriter`] will write this immediately and lazy writes will not be
|
||||
/// allowed.
|
||||
Data(#[serde(with = "escape")] Vec<u8>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for CpioEntryData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Size(s) => f.debug_tuple("Size").field(s).finish(),
|
||||
Self::Data(d) => f.debug_tuple("Data").field(&NumBytes(d.len())).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CpioEntryData {
|
||||
fn default() -> Self {
|
||||
Self::Size(0)
|
||||
}
|
||||
}
|
||||
|
||||
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"))?,
|
||||
};
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn is_size(&self) -> bool {
|
||||
matches!(self, CpioEntryData::Size(_))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct CpioEntry {
|
||||
/// File path.
|
||||
#[serde(with = "escape")]
|
||||
pub path: Vec<u8>,
|
||||
|
||||
/// File data.
|
||||
#[serde(default, skip_serializing_if = "CpioEntryData::is_size")]
|
||||
pub data: CpioEntryData,
|
||||
|
||||
/// Inode number.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub inode: u32,
|
||||
|
||||
/// File type portion of the `st_mode`-style mode.
|
||||
pub file_type: CpioEntryType,
|
||||
|
||||
/// Permissions portion of the `st_mode`-style mode.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero", with = "octal")]
|
||||
pub file_mode: u16,
|
||||
|
||||
/// Owner user ID.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub uid: u32,
|
||||
|
||||
/// Owner group ID.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub gid: u32,
|
||||
|
||||
/// Number of paths referencing the inode.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub nlink: u32,
|
||||
|
||||
/// Modification timestamp in Unix time.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub mtime: u32,
|
||||
|
||||
/// Major ID (class of device) for the device containing the inode.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub dev_maj: u32,
|
||||
|
||||
/// Minor ID (specific device instance) for the device containing the inode.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub dev_min: u32,
|
||||
|
||||
/// Major ID (class of device) represented by this entry. This is only
|
||||
/// relevant for [`CpioEntryType::Char`] and [`CpioEntryType::Block`].
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub rdev_maj: u32,
|
||||
|
||||
/// Minor ID (specific device instance) represented by this entry. This is
|
||||
/// only relevant for [`CpioEntryType::Char`] and [`CpioEntryType::Block`].
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub rdev_min: u32,
|
||||
|
||||
/// CRC32 checksum.
|
||||
#[serde(default, skip_serializing_if = "Zero::is_zero")]
|
||||
pub crc32: u32,
|
||||
}
|
||||
|
||||
impl fmt::Debug for CpioEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("CpioEntry")
|
||||
.field("path", &self.path.as_bstr())
|
||||
.field("data", &self.data)
|
||||
.field("inode", &self.inode)
|
||||
.field("file_type", &self.file_type)
|
||||
.field("file_mode", &self.file_mode)
|
||||
.field("uid", &self.uid)
|
||||
.field("gid", &self.gid)
|
||||
.field("nlink", &self.nlink)
|
||||
@@ -119,84 +303,89 @@ impl fmt::Debug for CpioEntryNew {
|
||||
.field("dev_min", &self.dev_min)
|
||||
.field("rdev_maj", &self.rdev_maj)
|
||||
.field("rdev_min", &self.rdev_min)
|
||||
.field("chksum", &self.chksum)
|
||||
.field("name", &EscapedString::new(&self.name))
|
||||
.field("content", &NumBytes(self.content.len()))
|
||||
.field("crc32", &self.crc32)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CpioEntryNew {
|
||||
impl fmt::Display for CpioEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let file_type_str = match file_type(self.mode) {
|
||||
S_IFIFO => Cow::Borrowed("pipe"),
|
||||
S_IFCHR => Cow::Borrowed("character device"),
|
||||
S_IFDIR => Cow::Borrowed("directory"),
|
||||
S_IFBLK => Cow::Borrowed("block device"),
|
||||
S_IFREG => Cow::Borrowed("regular file"),
|
||||
S_IFLNK => Cow::Borrowed("symbolic link"),
|
||||
S_IFSOCK => Cow::Borrowed("socket"),
|
||||
C_ISCTG => Cow::Borrowed("reserved"),
|
||||
m => Cow::Owned(format!("unknown ({m:o})")),
|
||||
};
|
||||
|
||||
writeln!(f, "Filename: {}", EscapedString::new(&self.name))?;
|
||||
writeln!(f, "Filetype: {file_type_str}")?;
|
||||
writeln!(f, "Inode: {}", self.ino)?;
|
||||
writeln!(f, "Mode: {:o}", self.mode)?;
|
||||
writeln!(f, "UID: {}", self.uid)?;
|
||||
writeln!(f, "GID: {}", self.gid)?;
|
||||
writeln!(f, "Links: {}", self.nlink)?;
|
||||
writeln!(f, "Modified: {}", self.mtime)?;
|
||||
writeln!(f, "Device: {:x},{:x}", self.dev_maj, self.dev_min)?;
|
||||
writeln!(f, "Device ID: {:x},{:x}", self.rdev_maj, self.rdev_min)?;
|
||||
writeln!(f, "Checksum: {:x}", self.chksum)?;
|
||||
writeln!(f, "Content: {:?}", NumBytes(self.content.len()))?;
|
||||
writeln!(f, "Path: {:?}", self.path.as_bstr())?;
|
||||
match &self.data {
|
||||
CpioEntryData::Size(s) => {
|
||||
writeln!(f, "Data: {:?}", &NumBytes(*s))?;
|
||||
}
|
||||
CpioEntryData::Data(d) => {
|
||||
if self.file_type == CpioEntryType::Symlink {
|
||||
writeln!(f, "Data: {:?}", d.as_bstr())?;
|
||||
} else {
|
||||
writeln!(f, "Data: {:?}", &NumBytes(d.len()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
writeln!(f, "Inode: {}", self.inode)?;
|
||||
writeln!(f, "Type: {}", self.file_type)?;
|
||||
writeln!(f, "Mode: {:o}", self.file_mode)?;
|
||||
writeln!(f, "UID: {}", self.uid)?;
|
||||
writeln!(f, "GID: {}", self.gid)?;
|
||||
writeln!(f, "Links: {}", self.nlink)?;
|
||||
writeln!(f, "Modtime: {}", self.mtime)?;
|
||||
writeln!(f, "Idevice: {:x},{:x}", self.dev_maj, self.dev_min)?;
|
||||
writeln!(f, "Rdevice: {:x},{:x}", self.rdev_maj, self.rdev_min)?;
|
||||
write!(f, "CRC32: {:x}", self.crc32)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CpioEntryNew {
|
||||
impl CpioEntry {
|
||||
pub fn new_trailer() -> Self {
|
||||
Self {
|
||||
path: CPIO_TRAILER.to_vec(),
|
||||
// Must be 1 for CRC format.
|
||||
nlink: 1,
|
||||
name: CPIO_TRAILER.to_vec(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_symlink(link_target: &[u8], name: &[u8]) -> Self {
|
||||
pub fn new_symlink(path: &[u8], link_target: &[u8]) -> Self {
|
||||
Self {
|
||||
mode: S_IFLNK | 0o777,
|
||||
path: path.to_owned(),
|
||||
data: CpioEntryData::Data(link_target.to_owned()),
|
||||
file_type: CpioEntryType::Symlink,
|
||||
file_mode: 0o777,
|
||||
nlink: 1,
|
||||
name: name.to_owned(),
|
||||
content: link_target.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_directory(name: &[u8]) -> Self {
|
||||
pub fn new_directory(path: &[u8], mode: u16) -> Self {
|
||||
Self {
|
||||
mode: S_IFDIR,
|
||||
path: path.to_owned(),
|
||||
file_type: CpioEntryType::Directory,
|
||||
file_mode: mode,
|
||||
nlink: 1,
|
||||
name: name.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_file(name: &[u8]) -> Self {
|
||||
pub fn new_file(path: &[u8], mode: u16, data: CpioEntryData) -> Self {
|
||||
Self {
|
||||
mode: S_IFREG,
|
||||
path: path.to_owned(),
|
||||
data,
|
||||
file_type: CpioEntryType::Regular,
|
||||
file_mode: mode,
|
||||
nlink: 1,
|
||||
name: name.to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_trailer(&self) -> bool {
|
||||
self.path == CPIO_TRAILER
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> FromReader<R> for CpioEntryNew {
|
||||
impl<R: Read> FromReader<R> for CpioEntry {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(reader: R) -> Result<Self> {
|
||||
@@ -209,39 +398,61 @@ impl<R: Read> FromReader<R> for CpioEntryNew {
|
||||
return Err(Error::UnknownMagic(magic));
|
||||
}
|
||||
|
||||
let ino = read_int(&mut reader)?;
|
||||
let inode = read_int(&mut reader)?;
|
||||
let mode = read_int(&mut reader)?;
|
||||
let uid = read_int(&mut reader)?;
|
||||
let gid = read_int(&mut reader)?;
|
||||
let nlink = read_int(&mut reader)?;
|
||||
let mtime = read_int(&mut reader)?;
|
||||
let filesize = read_int(&mut reader)?;
|
||||
let file_size = read_int(&mut reader)?;
|
||||
let dev_maj = read_int(&mut reader)?;
|
||||
let dev_min = read_int(&mut reader)?;
|
||||
let rdev_maj = read_int(&mut reader)?;
|
||||
let rdev_min = read_int(&mut reader)?;
|
||||
let namesize = read_int(&mut reader)?;
|
||||
let chksum = read_int(&mut reader)?;
|
||||
let path_size = read_int(&mut reader)?;
|
||||
let crc32 = read_int(&mut reader)?;
|
||||
|
||||
let mut name = vec![0u8; namesize.to_usize().unwrap()];
|
||||
reader.read_exact(&mut name)?;
|
||||
if name.last() != Some(&b'\0') {
|
||||
let mut path = read_data(
|
||||
&mut reader,
|
||||
path_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
if path.last() != Some(&b'\0') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Filename is not NULL-terminated",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
name.pop();
|
||||
path.pop();
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
|
||||
let mut content = vec![0u8; filesize.to_usize().unwrap()];
|
||||
reader.read_exact(&mut content)?;
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
let file_type = CpioEntryType::from_mode(mode);
|
||||
let data = match file_type {
|
||||
// Handled by CpioReader for streaming reads.
|
||||
CpioEntryType::Regular => CpioEntryData::Size(file_size),
|
||||
// Symlinks store the target in the file data.
|
||||
CpioEntryType::Symlink => {
|
||||
let content = read_data(
|
||||
&mut reader,
|
||||
file_size.to_usize().unwrap(),
|
||||
&AtomicBool::new(false),
|
||||
)?;
|
||||
padding::read_discard(&mut reader, 4)?;
|
||||
|
||||
CpioEntryData::Data(content)
|
||||
}
|
||||
// No other entry type should have data.
|
||||
t if file_size != 0 => return Err(Error::EntryHasData(t, path)),
|
||||
_ => CpioEntryData::Size(0),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
ino,
|
||||
mode,
|
||||
path,
|
||||
data,
|
||||
inode,
|
||||
file_type,
|
||||
file_mode: (mode & 0o7777) as u16,
|
||||
uid,
|
||||
gid,
|
||||
nlink,
|
||||
@@ -250,76 +461,236 @@ impl<R: Read> FromReader<R> for CpioEntryNew {
|
||||
dev_min,
|
||||
rdev_maj,
|
||||
rdev_min,
|
||||
chksum,
|
||||
name,
|
||||
content,
|
||||
crc32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ToWriter<W> for CpioEntryNew {
|
||||
impl<W: Write> ToWriter<W> for CpioEntry {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, writer: W) -> Result<()> {
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
let filesize = self
|
||||
.content
|
||||
.len()
|
||||
.to_u32()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("filesize"))?;
|
||||
let namesize = self
|
||||
.name
|
||||
let path_size = self
|
||||
.path
|
||||
.len()
|
||||
.checked_add(1)
|
||||
.and_then(|s| s.to_u32())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("filesize"))?;
|
||||
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
|
||||
|
||||
if self.chksum == 0 {
|
||||
let file_size = self.data.size()?;
|
||||
if file_size != 0
|
||||
&& self.file_type != CpioEntryType::Regular
|
||||
&& self.file_type != CpioEntryType::Symlink
|
||||
{
|
||||
return Err(Error::EntryHasData(self.file_type, self.path.clone()));
|
||||
}
|
||||
|
||||
if self.crc32 == 0 {
|
||||
writer.write_all(MAGIC_NEW)?;
|
||||
} else {
|
||||
writer.write_all(MAGIC_NEW_CRC)?;
|
||||
}
|
||||
|
||||
write_int(&mut writer, self.ino)?;
|
||||
write_int(&mut writer, self.mode)?;
|
||||
let mode = self.file_type.to_mode() | u32::from(self.file_mode & 0o7777);
|
||||
|
||||
write_int(&mut writer, self.inode)?;
|
||||
write_int(&mut writer, mode)?;
|
||||
write_int(&mut writer, self.uid)?;
|
||||
write_int(&mut writer, self.gid)?;
|
||||
write_int(&mut writer, self.nlink)?;
|
||||
write_int(&mut writer, self.mtime)?;
|
||||
write_int(&mut writer, filesize)?;
|
||||
write_int(&mut writer, file_size)?;
|
||||
write_int(&mut writer, self.dev_maj)?;
|
||||
write_int(&mut writer, self.dev_min)?;
|
||||
write_int(&mut writer, self.rdev_maj)?;
|
||||
write_int(&mut writer, self.rdev_min)?;
|
||||
write_int(&mut writer, namesize)?;
|
||||
write_int(&mut writer, self.chksum)?;
|
||||
write_int(&mut writer, path_size)?;
|
||||
write_int(&mut writer, self.crc32)?;
|
||||
|
||||
writer.write_all(&self.name)?;
|
||||
writer.write_all(&self.path)?;
|
||||
writer.write_zeros_exact(1)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
|
||||
writer.write_all(&self.content)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
if let CpioEntryData::Data(d) = &self.data {
|
||||
writer.write_all(d)?;
|
||||
padding::write_zeros(&mut writer, 4)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(mut reader: impl Read, include_trailer: bool) -> Result<Vec<CpioEntryNew>> {
|
||||
let mut entries = vec![];
|
||||
pub struct CpioReader<R: Read> {
|
||||
reader: R,
|
||||
include_trailer: bool,
|
||||
range: Option<Range<u64>>,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
loop {
|
||||
let entry = CpioEntryNew::from_reader(&mut reader)?;
|
||||
if file_type(entry.mode) != S_IFDIR && entry.nlink > 1 {
|
||||
return Err(Error::HardLinksNotSupported(EscapedString::new(entry.name)));
|
||||
impl<R: Read> CpioReader<R> {
|
||||
pub fn new(reader: R, include_trailer: bool) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
include_trailer,
|
||||
range: None,
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> R {
|
||||
self.reader
|
||||
}
|
||||
|
||||
fn skip_data(&mut self) -> io::Result<()> {
|
||||
if let Some(range) = &mut self.range {
|
||||
// This cannot overflow because cpio file sizes are 32 bit.
|
||||
let n = range.end - range.start + padding::calc(range.end, 4);
|
||||
self.reader.read_discard_exact(n)?;
|
||||
self.range = None;
|
||||
}
|
||||
|
||||
if entry.name == CPIO_TRAILER {
|
||||
if include_trailer {
|
||||
entries.push(entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_entry(&mut self) -> Result<Option<CpioEntry>> {
|
||||
if self.done {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.skip_data()?;
|
||||
|
||||
let entry = CpioEntry::from_reader(&mut self.reader)?;
|
||||
|
||||
if entry.is_trailer() {
|
||||
self.done = true;
|
||||
|
||||
if !self.include_trailer {
|
||||
return Ok(None);
|
||||
}
|
||||
break;
|
||||
} else if let CpioEntryData::Size(s) = entry.data {
|
||||
self.range = Some(0..u64::from(s));
|
||||
}
|
||||
|
||||
Ok(Some(entry))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for CpioReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
let Some(range) = &mut self.range else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"No entry opened",
|
||||
));
|
||||
};
|
||||
|
||||
let to_read = (range.end - range.start).min(buf.len() as u64) as usize;
|
||||
let n = self.reader.read(&mut buf[..to_read])?;
|
||||
|
||||
range.start += n as u64;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpioWriter<W: Write> {
|
||||
writer: CountingWriter<W>,
|
||||
pad_to_block_size: bool,
|
||||
range: Option<Range<u64>>,
|
||||
max_inode: u32,
|
||||
}
|
||||
|
||||
impl<W: Write> CpioWriter<W> {
|
||||
pub fn new(writer: W, pad_to_block_size: bool) -> Self {
|
||||
Self {
|
||||
writer: CountingWriter::new(writer),
|
||||
pad_to_block_size,
|
||||
range: None,
|
||||
max_inode: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_entry(&mut self) -> io::Result<()> {
|
||||
if let Some(range) = &mut self.range {
|
||||
// This cannot overflow because cpio file sizes are 32 bit.
|
||||
let n = range.end - range.start + padding::calc(range.end, 4);
|
||||
self.writer.write_zeros_exact(n)?;
|
||||
self.range = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_entry(&mut self, entry: &CpioEntry) -> Result<()> {
|
||||
self.finish_entry()?;
|
||||
|
||||
entry.to_writer(&mut self.writer)?;
|
||||
|
||||
if let CpioEntryData::Size(s) = entry.data {
|
||||
self.range = Some(0..u64::from(s));
|
||||
}
|
||||
|
||||
self.max_inode = self.max_inode.max(entry.inode);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Result<W> {
|
||||
self.finish_entry()?;
|
||||
|
||||
self.start_entry(&CpioEntry::new_trailer())?;
|
||||
|
||||
// Pad until the end of the block.
|
||||
if self.pad_to_block_size {
|
||||
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)?;
|
||||
}
|
||||
|
||||
Ok(self.writer.finish().0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for CpioWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let Some(range) = &mut self.range else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"No entry started",
|
||||
));
|
||||
};
|
||||
|
||||
let to_write = (range.end - range.start).min(buf.len() as u64) as usize;
|
||||
let n = self.writer.write(&buf[..to_write])?;
|
||||
|
||||
range.start += n as u64;
|
||||
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
reader: impl Read,
|
||||
include_trailer: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<CpioEntry>> {
|
||||
let mut cpio_reader = CpioReader::new(reader, include_trailer);
|
||||
let mut entries = vec![];
|
||||
|
||||
while let Some(mut entry) = cpio_reader.next_entry()? {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
|
||||
return Err(Error::HardLinksNotSupported(entry.path.clone()));
|
||||
}
|
||||
|
||||
if let CpioEntryData::Size(s) = entry.data {
|
||||
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)?;
|
||||
entry.data = CpioEntryData::Data(data);
|
||||
}
|
||||
|
||||
entries.push(entry);
|
||||
@@ -328,35 +699,83 @@ pub fn load(mut reader: impl Read, include_trailer: bool) -> Result<Vec<CpioEntr
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
pub fn sort(entries: &mut [CpioEntryNew]) {
|
||||
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
pub fn sort(entries: &mut [CpioEntry]) {
|
||||
entries.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
}
|
||||
|
||||
pub fn reassign_inodes(entries: &mut [CpioEntryNew]) {
|
||||
let mut inode = 300000;
|
||||
|
||||
for entry in entries {
|
||||
entry.ino = inode;
|
||||
inode += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save(writer: impl Write, entries: &[CpioEntryNew], pad_to_block_size: bool) -> Result<()> {
|
||||
let mut writer = CountingWriter::new(writer);
|
||||
|
||||
for entry in entries {
|
||||
entry.to_writer(&mut writer)?;
|
||||
/// Assign inodes to entries. If `missing_only` is true, then inodes are only
|
||||
/// assigned if the inode field in an entry is set to 0.
|
||||
///
|
||||
/// New inodes are assigned starting immediately after the highest inode number
|
||||
/// for a given ([`CpioEntry::dev_maj`], [`CpioEntry::dev_min`]) pair. If there
|
||||
/// are no existing inodes assigned for a device, then the numbers begin at
|
||||
/// 300000.
|
||||
pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()> {
|
||||
fn next_non_zero(i: u32) -> u32 {
|
||||
if i == u32::MAX {
|
||||
1
|
||||
} else {
|
||||
i.wrapping_add(1)
|
||||
}
|
||||
}
|
||||
|
||||
let mut trailer = CpioEntryNew::new_trailer();
|
||||
// 1 higher than the highest inode if possible.
|
||||
trailer.ino = entries.iter().map(|e| e.ino).max().map_or(0, |i| i + 1);
|
||||
trailer.to_writer(&mut writer)?;
|
||||
// (dev maj, dev min) -> (inode set, last assigned inode)
|
||||
let mut inodes: HashMap<(u32, u32), (HashSet<u32>, u32)> = HashMap::new();
|
||||
|
||||
// Pad until the end of the block.
|
||||
if pad_to_block_size {
|
||||
padding::write_zeros(&mut writer, IO_BLOCK_SIZE)?;
|
||||
if missing_only {
|
||||
for entry in &mut *entries {
|
||||
if entry.inode != 0 {
|
||||
let key = (entry.dev_maj, entry.dev_min);
|
||||
let (set, last) = inodes.entry(key).or_default();
|
||||
|
||||
set.insert(entry.inode);
|
||||
*last = (*last).max(entry.inode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
if entry.inode == 0 {
|
||||
let key = (entry.dev_maj, entry.dev_min);
|
||||
let (set, last) = inodes
|
||||
.entry(key)
|
||||
.or_insert_with(|| (HashSet::new(), 299999));
|
||||
|
||||
let mut unused = next_non_zero(*last);
|
||||
|
||||
while set.contains(&unused) {
|
||||
if unused == *last {
|
||||
return Err(Error::DeviceFull(entry.dev_maj, entry.dev_min));
|
||||
}
|
||||
|
||||
unused = next_non_zero(unused);
|
||||
}
|
||||
|
||||
entry.inode = unused;
|
||||
set.insert(unused);
|
||||
*last = unused;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save(
|
||||
writer: impl Write,
|
||||
entries: &[CpioEntry],
|
||||
pad_to_block_size: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
|
||||
|
||||
for entry in entries {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
cpio_writer.start_entry(entry)?;
|
||||
// CpioEntryData::Data will have already been written.
|
||||
}
|
||||
|
||||
cpio_writer.finish()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,839 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use num_traits::ToPrimitive;
|
||||
use rayon::{
|
||||
prelude::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::{ParallelSlice, ParallelSliceMut},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
format::verityrs,
|
||||
stream::{self, FromReader, ReadSeek, ToWriter, WriteSeek, WriteZerosExt},
|
||||
util::NumBytes,
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
const FEC_MAX_BLOCK_SIZE: u32 = 16384;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("FEC with parity byte count of {0} is not supported")]
|
||||
UnsupportedParity(u8),
|
||||
#[error("Cannot calculate FEC for empty data")]
|
||||
InputEmpty,
|
||||
#[error("Input size ({input}) is not a multiple of FEC block size ({block})")]
|
||||
NotBlockAligned { input: u64, block: u32 },
|
||||
#[error("FEC should have size {expected} for input size {input}, but has size {actual}")]
|
||||
InvalidFecSize {
|
||||
input: u64,
|
||||
expected: usize,
|
||||
actual: usize,
|
||||
},
|
||||
#[error("Cannot repair data due to too many errors")]
|
||||
TooManyErrors,
|
||||
#[error("Input data contains errors")]
|
||||
HasErrors,
|
||||
#[error("Data is too small to contain FEC headers")]
|
||||
DataTooSmall,
|
||||
#[error("The two FEC headers are different")]
|
||||
HeadersDifferent,
|
||||
#[error("Invalid FEC header magic: {0:#x}")]
|
||||
InvalidHeaderMagic(u32),
|
||||
#[error("Unsupported FEC header version: {0}")]
|
||||
UnsupportedHeaderVersion(u32),
|
||||
#[error("Invalid FEC header size: {0}")]
|
||||
InvalidHeaderSize(u32),
|
||||
#[error("FEC size in header {value} does not match available data ({available})")]
|
||||
InvalidHeaderFecSize { value: usize, available: usize },
|
||||
#[error("Expected FEC digest {expected}, but have {actual}")]
|
||||
InvalidFecDigest { expected: String, actual: String },
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A small wrapper around a byte array to represent a single Reed-Solomon
|
||||
/// codeword for any `RS(255, K)`.
|
||||
struct Codeword {
|
||||
data: [u8; 255],
|
||||
rs_k: u8,
|
||||
}
|
||||
|
||||
impl Codeword {
|
||||
fn new(rs_k: u8) -> Self {
|
||||
Self {
|
||||
data: [0u8; 255],
|
||||
rs_k,
|
||||
}
|
||||
}
|
||||
|
||||
fn data(&self) -> &[u8] {
|
||||
&self.data[..usize::from(self.rs_k)]
|
||||
}
|
||||
|
||||
fn data_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.data[..usize::from(self.rs_k)]
|
||||
}
|
||||
|
||||
fn parity(&mut self) -> &[u8] {
|
||||
&self.data[usize::from(self.rs_k)..]
|
||||
}
|
||||
|
||||
fn parity_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.data[usize::from(self.rs_k)..]
|
||||
}
|
||||
|
||||
fn all(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
fn all_mut(&mut self) -> &mut [u8] {
|
||||
&mut self.data
|
||||
}
|
||||
}
|
||||
|
||||
/// Since Rust's built-in .div_ceil() is still nightly-only.
|
||||
fn div_ceil(dividend: u64, divisor: u64) -> u64 {
|
||||
dividend / divisor + u64::from(dividend % divisor != 0)
|
||||
}
|
||||
|
||||
/// A type for performing FEC generation, verification, and error correction for
|
||||
/// a specific file size and Reed Solomon configuration. The implementation uses
|
||||
/// dm-verity's interleaving access pattern.
|
||||
///
|
||||
/// The interleaving access pattern can be visualized by placing the file
|
||||
/// offsets in a two-dimensional grid. For example, when reading a 2072576-byte
|
||||
/// file for calculating RS(255, 253):
|
||||
///
|
||||
/// ```text
|
||||
/// | <-------- Round 0 --------> | <-------- Round 1 --------> |
|
||||
/// |-----------------------------|-----------------------------|
|
||||
/// ^ | 0 1 ... 4095 | 4096 4097 ... 8191 |
|
||||
/// | | 8192 8192 ... 12287 | 12288 12289 ... 16383 |
|
||||
/// rs_k | 16384 16385 ... 20479 | 20480 20481 ... 24575 |
|
||||
/// | | ....... ....... ... ....... | ....... ....... ... ....... |
|
||||
/// v | 2064384 2064385 ... 2068479 | 2068480 2068481 ... 2072575 |
|
||||
/// ```
|
||||
///
|
||||
/// A regular sequential read of the file is traversing the grid row-by-row,
|
||||
/// while an interleaving read is traversing the grid column-by-column. Each
|
||||
/// column is always `rs_k` items tall, so each column forms the data portion of
|
||||
/// an RS codeword. The number of columns is always a multiple of the FEC block
|
||||
/// size. Since RS operates on fixed-size codewords and a file size might not
|
||||
/// always fill the grid completely, out-of-bounds offsets are treated as if
|
||||
/// they contain a `\0` byte.
|
||||
///
|
||||
/// All operations are multithreaded with I/O operations parallelized at the
|
||||
/// "round" level and RS operations parallelized at the column level.
|
||||
pub struct Fec {
|
||||
file_size: u64,
|
||||
block_size: u32,
|
||||
rs_k: u8,
|
||||
rounds: u64,
|
||||
}
|
||||
|
||||
impl Fec {
|
||||
pub fn new(file_size: u64, block_size: u32, parity: u8) -> Result<Self> {
|
||||
if file_size == 0 {
|
||||
return Err(Error::InputEmpty);
|
||||
} else if file_size % u64::from(block_size) != 0 {
|
||||
return Err(Error::NotBlockAligned {
|
||||
input: file_size,
|
||||
block: block_size,
|
||||
});
|
||||
} else if block_size > FEC_MAX_BLOCK_SIZE {
|
||||
return Err(Error::FieldOutOfBounds("block_size"));
|
||||
}
|
||||
|
||||
let rs_k = 255 - parity;
|
||||
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
|
||||
return Err(Error::UnsupportedParity(parity));
|
||||
}
|
||||
|
||||
let blocks = div_ceil(file_size, u64::from(block_size));
|
||||
let rounds = div_ceil(blocks, u64::from(rs_k));
|
||||
|
||||
// Check upfront so we don't need to do checked multiplication later.
|
||||
rounds
|
||||
.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"))?;
|
||||
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(Self {
|
||||
file_size,
|
||||
block_size,
|
||||
rs_k,
|
||||
rounds,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the number of parity bytes per codeword.
|
||||
#[inline]
|
||||
fn parity(&self) -> u8 {
|
||||
255 - self.rs_k
|
||||
}
|
||||
|
||||
/// Get the size of the FEC data needed to cover the entire file.
|
||||
#[inline]
|
||||
fn fec_size(&self) -> usize {
|
||||
usize::from(self.parity()) * self.rounds as usize * self.block_size as usize
|
||||
}
|
||||
|
||||
/// Get the backing file offset for the specified `offset` in the
|
||||
/// interleaved view.
|
||||
fn backing_offset(&self, offset: u64) -> u64 {
|
||||
let rs_k = u64::from(self.rs_k);
|
||||
|
||||
offset / rs_k + offset % rs_k * self.rounds * u64::from(self.block_size)
|
||||
}
|
||||
|
||||
/// Read a raw sequential block from the backing file, starting at offset
|
||||
/// `offset` in the interleaved view. This reads a horizontal block-aligned
|
||||
/// slice in the file offset grid.
|
||||
fn read_seq_block(
|
||||
&self,
|
||||
mut reader: impl Read + Seek,
|
||||
offset: u64,
|
||||
buf: &mut [u8],
|
||||
) -> io::Result<()> {
|
||||
assert_eq!(
|
||||
buf.len(),
|
||||
self.block_size as usize,
|
||||
"Buffer does not match block size",
|
||||
);
|
||||
|
||||
let backing_offset = self.backing_offset(offset);
|
||||
|
||||
// Out of bounds offsets are treated as if they contain zeros.
|
||||
if backing_offset >= self.file_size {
|
||||
buf.fill(0);
|
||||
} else {
|
||||
reader.seek(SeekFrom::Start(backing_offset))?;
|
||||
reader.read_exact(buf)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a raw sequential block to the backing file, starting at offset
|
||||
/// `offset` in the interleaved view. This writes a horizontal block-aligned
|
||||
/// slice in the file offset grid.
|
||||
fn write_seq_block(
|
||||
&self,
|
||||
mut writer: impl Write + Seek,
|
||||
offset: u64,
|
||||
buf: &[u8],
|
||||
) -> io::Result<()> {
|
||||
assert_eq!(
|
||||
buf.len(),
|
||||
self.block_size as usize,
|
||||
"Buffer does not match block size",
|
||||
);
|
||||
|
||||
let backing_offset = self.backing_offset(offset);
|
||||
|
||||
// Out of bounds offsets are ignored.
|
||||
if backing_offset < self.file_size {
|
||||
writer.seek(SeekFrom::Start(backing_offset))?;
|
||||
writer.write_all(buf)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the nth round from the file. The data is laid out sequentially
|
||||
/// (row-by-row).
|
||||
fn read_round(&self, mut reader: impl Read + Seek, round: u64) -> io::Result<Vec<u8>> {
|
||||
let mut grid = vec![0u8; usize::from(self.rs_k) * self.block_size as usize];
|
||||
|
||||
for row in 0..self.rs_k {
|
||||
let interleaved_offset =
|
||||
round * u64::from(self.rs_k) * u64::from(self.block_size) + u64::from(row);
|
||||
let row_start = usize::from(row) * self.block_size as usize;
|
||||
let row_end = row_start + self.block_size as usize;
|
||||
let row_slice = &mut grid[row_start..row_end];
|
||||
|
||||
self.read_seq_block(&mut reader, interleaved_offset, row_slice)?;
|
||||
}
|
||||
|
||||
Ok(grid)
|
||||
}
|
||||
|
||||
/// Write the nth round to the file. The data is expected to be laid out
|
||||
/// sequentially (row-by-row).
|
||||
fn write_round(
|
||||
&self,
|
||||
mut writer: impl Write + Seek,
|
||||
round: u64,
|
||||
grid: &[u8],
|
||||
) -> io::Result<()> {
|
||||
for row in 0..self.rs_k {
|
||||
let interleaved_offset =
|
||||
round * u64::from(self.rs_k) * u64::from(self.block_size) + u64::from(row);
|
||||
let row_start = usize::from(row) * self.block_size as usize;
|
||||
let row_end = row_start + self.block_size as usize;
|
||||
let row_slice = &grid[row_start..row_end];
|
||||
|
||||
self.write_seq_block(&mut writer, interleaved_offset, row_slice)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the nth RS codeword from a round's grid.
|
||||
fn get_codeword(&self, grid: &[u8], column: usize) -> Codeword {
|
||||
let mut codeword = Codeword::new(self.rs_k);
|
||||
let data = codeword.data_mut();
|
||||
|
||||
for row in 0..usize::from(self.rs_k) {
|
||||
data[row] = grid[row * self.block_size as usize + column];
|
||||
}
|
||||
|
||||
codeword
|
||||
}
|
||||
|
||||
/// Put the nth RS codeword into a round's grid.
|
||||
fn put_codeword(&self, grid: &mut [u8], column: usize, codeword: &Codeword) {
|
||||
let data = codeword.data();
|
||||
|
||||
for row in 0..usize::from(self.rs_k) {
|
||||
grid[row * self.block_size as usize + column] = data[row];
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate FEC data for a single round.
|
||||
fn generate_one_round(
|
||||
&self,
|
||||
reader: impl Read + Seek,
|
||||
round: u64,
|
||||
fec: &mut [u8],
|
||||
) -> Result<()> {
|
||||
assert_eq!(
|
||||
fec.len(),
|
||||
usize::from(self.parity()) * self.block_size as usize,
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let encode = verityrs::FN_ENCODE[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
for (column, buf) in fec.chunks_exact_mut(parity).enumerate() {
|
||||
let mut codeword = self.get_codeword(&grid, column);
|
||||
encode(codeword.all_mut());
|
||||
buf.copy_from_slice(codeword.parity());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify file data for a single round.
|
||||
fn verify_one_round(&self, reader: impl Read + Seek, round: u64, fec: &[u8]) -> Result<()> {
|
||||
assert_eq!(
|
||||
fec.len(),
|
||||
usize::from(self.parity()) * self.block_size as usize,
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let grid = self.read_round(reader, round)?;
|
||||
let is_correct = verityrs::FN_IS_CORRECT[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
|
||||
for (column, buf) in fec.chunks_exact(parity).enumerate() {
|
||||
let mut codeword = self.get_codeword(&grid, column);
|
||||
codeword.parity_mut().copy_from_slice(buf);
|
||||
|
||||
if !is_correct(codeword.all()) {
|
||||
return Err(Error::HasErrors);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repair file data for a single round.
|
||||
fn repair_one_round(
|
||||
&self,
|
||||
reader: impl Read + Seek,
|
||||
writer: impl Write + Seek,
|
||||
round: u64,
|
||||
fec: &[u8],
|
||||
) -> Result<u64> {
|
||||
assert_eq!(
|
||||
fec.len(),
|
||||
usize::from(self.parity()) * self.block_size as usize,
|
||||
"FEC buffer length does not match block size",
|
||||
);
|
||||
|
||||
let mut grid = self.read_round(reader, round)?;
|
||||
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
|
||||
let parity = usize::from(self.parity());
|
||||
let mut num_corrected = 0;
|
||||
|
||||
for (column, buf) in fec.chunks_exact(parity).enumerate() {
|
||||
let mut codeword = self.get_codeword(&grid, column);
|
||||
codeword.parity_mut().copy_from_slice(buf);
|
||||
|
||||
let n = correct_errors(codeword.all_mut()).ok_or(Error::TooManyErrors)?;
|
||||
if n > 0 {
|
||||
self.put_codeword(&mut grid, column, &codeword);
|
||||
}
|
||||
|
||||
num_corrected += n as u64;
|
||||
}
|
||||
|
||||
if num_corrected > 0 {
|
||||
self.write_round(writer, round, &grid)?;
|
||||
}
|
||||
|
||||
Ok(num_corrected)
|
||||
}
|
||||
|
||||
/// Generate FEC data for the file. The file size must match the file size
|
||||
/// given to [`Self::new()`].
|
||||
///
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn generate(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<u8>> {
|
||||
let fec_size = self.fec_size();
|
||||
let mut fec = vec![0u8; fec_size];
|
||||
|
||||
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = open_input()?;
|
||||
self.generate_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(fec)
|
||||
}
|
||||
|
||||
/// Verify that the file contains no errors. This is significantly faster
|
||||
/// than [`Self::repair()`] if only error detection, not correction, is
|
||||
/// needed.
|
||||
///
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn verify(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
fec: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec_size = self.fec_size();
|
||||
if fec.len() != fec_size {
|
||||
return Err(Error::InvalidFecSize {
|
||||
input: self.file_size,
|
||||
expected: fec_size,
|
||||
actual: fec.len(),
|
||||
});
|
||||
}
|
||||
|
||||
fec.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<()> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = open_input()?;
|
||||
self.verify_one_round(reader, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<()>>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repair the file. Up to `parity / 2` bytes per codeword can be repaired.
|
||||
/// If the file is successfully repaired, the number of repaired bytes is
|
||||
/// returned. If the file is corrupt beyond repair, [`Error::TooManyErrors`]
|
||||
/// is returned. It's not safe to assume that as much data as possible has
|
||||
/// been repaired when [`Error::TooManyErrors`] is returned due to fail-fast
|
||||
/// behavior.
|
||||
///
|
||||
/// This function corrects errors at unknown locations only. Correcting
|
||||
/// erasures at known locations is not supported.
|
||||
///
|
||||
/// This function is multithreaded and uses rayon's global thread pool.
|
||||
pub fn repair(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
fec: &[u8],
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<u64> {
|
||||
let fec_size = self.fec_size();
|
||||
if fec.len() != fec_size {
|
||||
return Err(Error::InvalidFecSize {
|
||||
input: self.file_size,
|
||||
expected: fec_size,
|
||||
actual: fec.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let num_corrected = fec
|
||||
.par_chunks_exact(fec_size / self.rounds as usize)
|
||||
.enumerate()
|
||||
.map(|(round, buf)| -> Result<u64> {
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
|
||||
let reader = open_input()?;
|
||||
let writer = open_output()?;
|
||||
self.repair_one_round(reader, writer, round as u64, buf)
|
||||
})
|
||||
.collect::<Result<Vec<u64>>>()?
|
||||
.into_iter()
|
||||
.sum();
|
||||
|
||||
Ok(num_corrected)
|
||||
}
|
||||
}
|
||||
|
||||
/// A type for reading and writing AOSP's standalone FEC image format.
|
||||
///
|
||||
/// The FEC data parser in this implementation is strict. All header fields,
|
||||
/// like the version, header size, and digest, must be valid and both copies of
|
||||
/// the header must match.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct FecImage {
|
||||
pub fec: Vec<u8>,
|
||||
pub data_size: u64,
|
||||
pub parity: u8,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FecImage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Fec")
|
||||
.field("fec", &NumBytes(self.fec.len()))
|
||||
.field("data_size", &self.data_size)
|
||||
.field("parity", &self.parity)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl FecImage {
|
||||
/// Generate FEC data for a file. `parity` is the number of parity bytes per
|
||||
/// 255-byte Reed-Solomon codeword.
|
||||
pub fn generate(
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
parity: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Self> {
|
||||
let data_size = {
|
||||
let mut file = open_input()?;
|
||||
file.seek(SeekFrom::End(0))?
|
||||
};
|
||||
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
|
||||
let fec_data = fec.generate(open_input, cancel_signal)?;
|
||||
|
||||
Ok(Self {
|
||||
fec: fec_data,
|
||||
data_size,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check that a file contains no errors. This is significantly faster than
|
||||
/// [`Self::repair()`] if performing a repair is not necessary.
|
||||
pub fn verify(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.verify(open_input, &self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// Repair a file using this instance's FEC data. The maximum correctable
|
||||
/// errors per 255-byte codeword is half of [`Self::parity`]. Returns the
|
||||
/// number of bytes corrected if the file is successfully repaired or
|
||||
/// [`Error::TooManyErrors`] if the file cannot be repaired. This function
|
||||
/// fails fast. If an RS codeword cannot be repaired, other potentially
|
||||
/// repairable codewords may not be repaired.
|
||||
///
|
||||
/// Note that if there are too many errors inside a certain codeword, it's
|
||||
/// possible for there to be a false positive where the corrupted codeword
|
||||
/// is "corrected" into an incorrect value. FEC error detection is not a
|
||||
/// replacement for cryptographically secure digests.
|
||||
///
|
||||
/// The inputs and outputs should point to the same underlying file because
|
||||
/// only regions where errors are corrected are written. It is guaranteed
|
||||
/// that multiple threads will always read and write disjoint file offsets.
|
||||
pub fn repair(
|
||||
&self,
|
||||
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<u64> {
|
||||
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
|
||||
fec.repair(open_input, open_output, &self.fec, cancel_signal)
|
||||
}
|
||||
|
||||
/// 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]);
|
||||
|
||||
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())?;
|
||||
|
||||
Ok(writer.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> FromReader<R> for FecImage {
|
||||
type Error = Error;
|
||||
|
||||
fn from_reader(mut reader: R) -> Result<Self> {
|
||||
// Avoid requiring seekable readers since we need to read everything
|
||||
// into memory anyway.
|
||||
let mut fec = Vec::new();
|
||||
reader.read_to_end(&mut fec)?;
|
||||
|
||||
if fec.len() < FEC_BLOCK_SIZE {
|
||||
return Err(Error::DataTooSmall);
|
||||
}
|
||||
|
||||
// Make sure both headers match.
|
||||
let header1_offset = fec.len() - FEC_BLOCK_SIZE;
|
||||
let header2_offset = fec.len() - FEC_HEADER_SIZE;
|
||||
let header1_raw = &fec[header1_offset..header1_offset + FEC_HEADER_SIZE];
|
||||
let header2_raw = &fec[header2_offset..header2_offset + FEC_HEADER_SIZE];
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
let version = header_reader.read_u32::<LittleEndian>()?;
|
||||
if version != FEC_VERSION {
|
||||
return Err(Error::UnsupportedHeaderVersion(version));
|
||||
}
|
||||
|
||||
let header_size = header_reader.read_u32::<LittleEndian>()?;
|
||||
if header_size != FEC_HEADER_SIZE as u32 {
|
||||
return Err(Error::InvalidHeaderSize(header_size));
|
||||
}
|
||||
|
||||
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 actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
|
||||
if fec_size != actual_fec_size {
|
||||
return Err(Error::InvalidHeaderFecSize {
|
||||
value: fec_size,
|
||||
available: actual_fec_size,
|
||||
});
|
||||
}
|
||||
|
||||
let input_size = header_reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
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() {
|
||||
return Err(Error::InvalidFecDigest {
|
||||
expected: hex::encode(digest),
|
||||
actual: hex::encode(actual_digest),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
fec,
|
||||
data_size: input_size,
|
||||
parity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ToWriter<W> for FecImage {
|
||||
type Error = Error;
|
||||
|
||||
fn to_writer(&self, mut writer: W) -> Result<()> {
|
||||
let header = self.build_header()?;
|
||||
|
||||
writer.write_all(&self.fec)?;
|
||||
writer.write_all(&header)?;
|
||||
writer.write_zeros_exact((FEC_BLOCK_SIZE - 2 * FEC_HEADER_SIZE) as u64)?;
|
||||
writer.write_all(&header)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::Seek,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use rand::RngCore;
|
||||
|
||||
use crate::stream::SharedCursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn corrupt_byte(file: &mut SharedCursor, offset: u64) {
|
||||
let mut buf = [0u8; 1];
|
||||
|
||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||
file.read_exact(&mut buf).unwrap();
|
||||
|
||||
buf[0] = buf[0].wrapping_add(1);
|
||||
|
||||
file.seek(SeekFrom::Start(offset)).unwrap();
|
||||
file.write_all(&buf).unwrap();
|
||||
}
|
||||
|
||||
fn run_test(block_size: u32, rs_k: u8) {
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
let parity = 255 - rs_k;
|
||||
|
||||
// Generate data big enough to span multiple rounds, but don't fill the
|
||||
// offset grid to ensure that the out-of-bounds-is-0 behavior works.
|
||||
let size = usize::from(rs_k) * block_size as usize * 3 - block_size as usize;
|
||||
let mut file = SharedCursor::default();
|
||||
let orig_digest = {
|
||||
let mut buf = vec![0u8; size];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
file.write_all(&buf).unwrap();
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
};
|
||||
|
||||
let fec = Fec::new(size as u64, block_size, parity).unwrap();
|
||||
assert_eq!(fec.rounds, 3);
|
||||
|
||||
let num_codewords = fec.rounds as usize * block_size as usize;
|
||||
|
||||
// Generate FEC data.
|
||||
let fec_data = fec
|
||||
.generate(|| Ok(Box::new(file.reopen())), &cancel_signal)
|
||||
.unwrap();
|
||||
|
||||
// Verify that there are no errors.
|
||||
fec.verify(|| Ok(Box::new(file.reopen())), &fec_data, &cancel_signal)
|
||||
.unwrap();
|
||||
|
||||
// Verify that errors are detected.
|
||||
corrupt_byte(&mut file, 0);
|
||||
assert_matches!(
|
||||
fec.verify(|| Ok(Box::new(file.reopen())), &fec_data, &cancel_signal,),
|
||||
Err(Error::HasErrors)
|
||||
);
|
||||
|
||||
// Corrupt one byte in every single codeword.
|
||||
for offset in 1..num_codewords {
|
||||
corrupt_byte(&mut file, offset as u64);
|
||||
}
|
||||
|
||||
// Verify that all the single-byte errors can be fixed.
|
||||
fec.repair(
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
|| Ok(Box::new(file.reopen())),
|
||||
&fec_data,
|
||||
&cancel_signal,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let repaired_digest = {
|
||||
let mut buf = Vec::new();
|
||||
file.rewind().unwrap();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
};
|
||||
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
|
||||
|
||||
// We don't test for Error::TooManyErrors because of the chance of false
|
||||
// positives due to the nature of RS.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_verify_repair() {
|
||||
for block_size in [1, 2, 4, 8, 16, 32, 64] {
|
||||
for rs_k in verityrs::FN_ENCODE.keys() {
|
||||
println!("Testing block_size={block_size}, rs_k={rs_k}");
|
||||
run_test(block_size, *rs_k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_image() {
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let mut file = SharedCursor::default();
|
||||
{
|
||||
let mut buf = [0u8; FEC_BLOCK_SIZE];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
file.write_all(&buf).unwrap();
|
||||
}
|
||||
|
||||
let image = FecImage::generate(|| Ok(Box::new(file.reopen())), 2, &cancel_signal).unwrap();
|
||||
|
||||
let mut fec_file = Cursor::new(Vec::new());
|
||||
image.to_writer(&mut fec_file).unwrap();
|
||||
|
||||
fec_file.rewind().unwrap();
|
||||
let new_image = FecImage::from_reader(&mut fec_file).unwrap();
|
||||
|
||||
assert_eq!(image, new_image);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ pub mod avb;
|
||||
pub mod bootimage;
|
||||
pub mod compression;
|
||||
pub mod cpio;
|
||||
pub mod fec;
|
||||
pub mod ota;
|
||||
pub mod padding;
|
||||
pub mod payload;
|
||||
pub mod verityrs;
|
||||
|
||||
+20
-20
@@ -7,12 +7,13 @@ use std::{
|
||||
collections::BTreeMap,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
iter,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use cms::signed_data::SignedData;
|
||||
use const_oid::{db::rfc5912, ObjectIdentifier};
|
||||
use memchr::memmem;
|
||||
use prost::Message;
|
||||
use ring::digest::Context;
|
||||
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
|
||||
use sha1::Sha1;
|
||||
@@ -24,9 +25,8 @@ use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, Z
|
||||
use crate::{
|
||||
crypto,
|
||||
format::payload::{self, PayloadHeader},
|
||||
protobuf::build::tools::releasetools::{mod_OtaMetadata::OtaType, OtaMetadata},
|
||||
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter},
|
||||
util,
|
||||
};
|
||||
|
||||
pub const PATH_METADATA: &str = "META-INF/com/android/metadata";
|
||||
@@ -64,8 +64,8 @@ pub enum Error {
|
||||
UnsupportedDigestAlgorithm(ObjectIdentifier),
|
||||
#[error("Unsupported signature algorithm: {0}")]
|
||||
UnsupportedSignatureAlgorithm(ObjectIdentifier),
|
||||
#[error("Expected entry offsets {0:?}, but have {1:?}")]
|
||||
MismatchedPropertyFiles(String, String),
|
||||
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
|
||||
MismatchedPropertyFiles { expected: String, actual: String },
|
||||
#[error("Property files {0:?} exceed {1} byte reserved space")]
|
||||
InsufficientReservedSpace(String, usize),
|
||||
#[error("Invalid property file entry: {0:?}")]
|
||||
@@ -76,8 +76,8 @@ pub enum Error {
|
||||
CmsSign(#[from] crypto::Error),
|
||||
#[error("Payload error")]
|
||||
Payload(#[from] payload::Error),
|
||||
#[error("Protobuf error")]
|
||||
Protobuf(#[from] quick_protobuf::Error),
|
||||
#[error("Failed to decode protobuf message")]
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("SPKI error")]
|
||||
Spki(#[from] pkcs8::spki::Error),
|
||||
#[error("x509 DER error")]
|
||||
@@ -99,11 +99,11 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
|
||||
let mut pairs = BTreeMap::<String, String>::new();
|
||||
|
||||
match metadata.type_pb {
|
||||
OtaType::AB => {
|
||||
match metadata.r#type() {
|
||||
OtaType::Ab => {
|
||||
pairs.insert("ota-type".to_owned(), "AB".to_owned());
|
||||
}
|
||||
OtaType::BLOCK => {
|
||||
OtaType::Block => {
|
||||
pairs.insert("ota-type".to_owned(), "BLOCK".to_owned());
|
||||
}
|
||||
_ => {}
|
||||
@@ -161,7 +161,7 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
|
||||
.into_iter()
|
||||
.map(|(k, v)| format!("{k}={v}\n"))
|
||||
.collect::<String>();
|
||||
let modern_metadata = util::write_protobuf(metadata)?;
|
||||
let modern_metadata = metadata.encode_to_vec();
|
||||
|
||||
Ok((legacy_metadata, modern_metadata))
|
||||
}
|
||||
@@ -299,7 +299,7 @@ pub fn add_metadata(
|
||||
metadata_pb_raw: &[u8],
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<OtaMetadata> {
|
||||
let mut metadata: OtaMetadata = util::read_protobuf(metadata_pb_raw)?;
|
||||
let mut metadata = OtaMetadata::decode(metadata_pb_raw)?;
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
|
||||
let mut zip_entries = zip_entries.to_owned();
|
||||
@@ -389,7 +389,10 @@ pub fn verify_metadata(
|
||||
for (key, value) in &metadata.property_files {
|
||||
let new_value = compute_property_files(key, &zip_entries, Some(value.len()))?;
|
||||
if *value != new_value {
|
||||
return Err(Error::MismatchedPropertyFiles(value.clone(), new_value));
|
||||
return Err(Error::MismatchedPropertyFiles {
|
||||
expected: value.clone(),
|
||||
actual: new_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,10 +455,7 @@ fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
|
||||
/// CMS signed attributes are intentionally not supported because AOSP recovery
|
||||
/// does not support them either. It expects the CMS [`SignedData`] structure to
|
||||
/// be used for nothing more than a raw signature transport mechanism.
|
||||
pub fn verify_ota(
|
||||
mut reader: impl Read + Seek,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
) -> Result<Certificate> {
|
||||
pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> Result<Certificate> {
|
||||
let (sd, hashed_size) = parse_ota_sig(&mut reader)?;
|
||||
|
||||
// Make sure the certificate in the CMS structure matches the otacert zip
|
||||
@@ -521,9 +521,9 @@ pub fn parse_zip_ota_info(
|
||||
|
||||
let metadata = {
|
||||
let mut entry = zip.by_name(PATH_METADATA_PB)?;
|
||||
let mut buf = vec![0u8; entry.size() as usize];
|
||||
entry.read_exact(&mut buf)?;
|
||||
util::read_protobuf::<OtaMetadata>(&buf)?
|
||||
let mut buf = Vec::new();
|
||||
entry.read_to_end(&mut buf)?;
|
||||
OtaMetadata::decode(buf.as_slice())?
|
||||
};
|
||||
|
||||
let certificate = {
|
||||
|
||||
+102
-87
@@ -6,7 +6,7 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
@@ -14,7 +14,7 @@ use base64::Engine;
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use bzip2::write::BzDecoder;
|
||||
use num_traits::ToPrimitive;
|
||||
use quick_protobuf::MessageWrite;
|
||||
use prost::Message;
|
||||
use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
|
||||
use ring::digest::{Context, Digest};
|
||||
use rsa::{traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey};
|
||||
@@ -30,57 +30,67 @@ use xz2::{
|
||||
use crate::{
|
||||
crypto,
|
||||
protobuf::chromeos_update_engine::{
|
||||
mod_InstallOperation, mod_Signatures::Signature, DeltaArchiveManifest, Extent,
|
||||
install_operation::Type, signatures::Signature, DeltaArchiveManifest, Extent,
|
||||
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, CountingWriter, FromReader, HashingWriter, ReadDiscardExt, ReadSeek,
|
||||
SharedCursor, WriteSeek,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
||||
const OTA_MAGIC: &[u8; 4] = b"CrAU";
|
||||
const OTA_HEADER_SIZE: usize = OTA_MAGIC.len() + 8 + 8 + 4;
|
||||
|
||||
const MANIFEST_MAX_SIZE: usize = 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown magic: {0:?}")]
|
||||
UnknownMagic([u8; 4]),
|
||||
#[error("Unsupported payload version: {0}")]
|
||||
UnsupportedVersion(u64),
|
||||
#[error("File is a delta OTA, not a full OTA")]
|
||||
NotFullOta,
|
||||
#[error("Payload contains no signatures")]
|
||||
NoSignatures,
|
||||
#[error("Blob offset should be {0}, but is {1}")]
|
||||
InvalidBlobOffset(u64, u64),
|
||||
#[error("Payload signatures offset should be {0}, but is {1}")]
|
||||
InvalidPayloadSignaturesOffset(u64, u64),
|
||||
#[error("Blob offset should be {expected}, but is {actual}")]
|
||||
InvalidBlobOffset { expected: u64, actual: u64 },
|
||||
#[error("Payload signatures offset should be {expected}, but is {actual}")]
|
||||
InvalidPayloadSignaturesOffset { expected: u64, actual: u64 },
|
||||
#[error("Invalid payload properties line: {0:?}")]
|
||||
InvalidPropertiesLine(String),
|
||||
#[error("Duplicate payload property: {0:?}")]
|
||||
DuplicateProperty(String),
|
||||
#[error("Payload property {0:?} ({1:?}) does not match expected value {2:?}")]
|
||||
InvalidProperty(String, String, Option<String>),
|
||||
#[error("Payload property {key:?} ({actual:?}) does not match expected value {expected:?}")]
|
||||
InvalidProperty {
|
||||
key: String,
|
||||
expected: Option<String>,
|
||||
actual: String,
|
||||
},
|
||||
#[error("Unsupported partition operation: {0:?}")]
|
||||
UnsupportedOperation(mod_InstallOperation::Type),
|
||||
#[error("Expected sha256 {0:?}, but have {1:?}")]
|
||||
MismatchedDigest(Option<String>, String),
|
||||
#[error("Size of {0} ({1}) is not aligned to the block size ({2})")]
|
||||
InvalidPartitionSize(String, u64, u32),
|
||||
UnsupportedOperation(Type),
|
||||
#[error("Expected sha256 {expected:?}, but have {actual:?}")]
|
||||
MismatchedDigest {
|
||||
expected: Option<String>,
|
||||
actual: String,
|
||||
},
|
||||
#[error("Size of {name} ({size}) is not aligned to the block size ({block_size})")]
|
||||
InvalidPartitionSize {
|
||||
name: String,
|
||||
size: u64,
|
||||
block_size: u32,
|
||||
},
|
||||
#[error("Partition not found in payload: {0}")]
|
||||
MissingPartition(String),
|
||||
#[error("Partitions not found in payload: {0:?}")]
|
||||
MissingPartitions(HashSet<String>),
|
||||
#[error("{0:?} field is missing")]
|
||||
MissingField(&'static str),
|
||||
#[error("{0:?} field exceeds integer bounds")]
|
||||
IntegerTooLarge(&'static str),
|
||||
#[error("{0:?} field is out of bounds")]
|
||||
FieldOutOfBounds(&'static str),
|
||||
#[error("Crypto error")]
|
||||
Crypto(#[from] crypto::Error),
|
||||
#[error("Protobuf error")]
|
||||
Protobuf(#[from] quick_protobuf::Error),
|
||||
#[error("Failed to decode protobuf message")]
|
||||
ProtobufDecode(#[from] prost::DecodeError),
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] xz2::stream::Error),
|
||||
#[error("RSA error")]
|
||||
@@ -99,6 +109,15 @@ pub struct PayloadHeader {
|
||||
pub blob_offset: u64,
|
||||
}
|
||||
|
||||
impl PayloadHeader {
|
||||
pub fn is_full_ota(&self) -> bool {
|
||||
self.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.all(|p| p.old_partition_info.is_none())
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> FromReader<R> for PayloadHeader {
|
||||
type Error = Error;
|
||||
|
||||
@@ -121,21 +140,19 @@ impl<R: Read> FromReader<R> for PayloadHeader {
|
||||
let manifest_size = reader
|
||||
.read_u64::<BigEndian>()?
|
||||
.to_usize()
|
||||
.ok_or_else(|| Error::IntegerTooLarge("manifest_size"))?;
|
||||
.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 mut manifest_raw = vec![0u8; manifest_size];
|
||||
reader.read_exact(&mut manifest_raw)?;
|
||||
let manifest: DeltaArchiveManifest = util::read_protobuf(&manifest_raw)?;
|
||||
|
||||
// Fail as soon as possible since it's impossible to support delta OTAs.
|
||||
if manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.any(|p| p.old_partition_info.is_some())
|
||||
{
|
||||
return Err(Error::NotFullOta);
|
||||
}
|
||||
let manifest = DeltaArchiveManifest::decode(manifest_raw.as_slice())?;
|
||||
|
||||
// Skip manifest signatures.
|
||||
reader.read_discard_exact(metadata_signature_size.into())?;
|
||||
@@ -166,6 +183,7 @@ fn sign_digest(digest: &[u8], key: &RsaPrivateKey) -> Result<Signatures> {
|
||||
data: Some(digest_signed),
|
||||
// Always fits in even a u16.
|
||||
unpadded_signature_size: Some(unpadded_size as u32),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let signatures = Signatures {
|
||||
@@ -296,9 +314,10 @@ impl<W: Write> PayloadWriter<W> {
|
||||
// The blob must contain all data in sequential order with no gaps.
|
||||
for p in &mut header.manifest.partitions {
|
||||
for op in &mut p.operations {
|
||||
op.data_offset = Some(blob_size);
|
||||
|
||||
if let Some(length) = op.data_length {
|
||||
// The field must be left unset when the blob contains no
|
||||
// data for the operation.
|
||||
op.data_offset = Some(blob_size);
|
||||
blob_size += length;
|
||||
}
|
||||
}
|
||||
@@ -310,14 +329,14 @@ impl<W: Write> PayloadWriter<W> {
|
||||
ring::digest::digest(&ring::digest::SHA256, b"").as_ref(),
|
||||
&key,
|
||||
)?;
|
||||
let dummy_sig_size = dummy_sig.get_size();
|
||||
let dummy_sig_size = dummy_sig.encoded_len();
|
||||
|
||||
// Fill out the new payload signature information.
|
||||
header.manifest.signatures_offset = Some(blob_size);
|
||||
header.manifest.signatures_size = Some(dummy_sig_size as u64);
|
||||
|
||||
// Build new manifest.
|
||||
let manifest_raw_new = util::write_protobuf(&header.manifest)?;
|
||||
let manifest_raw_new = header.manifest.encode_to_vec();
|
||||
|
||||
// Excludes signatures (hashes are for signing).
|
||||
let mut h_partial = Context::new(&ring::digest::SHA256);
|
||||
@@ -345,7 +364,7 @@ impl<W: Write> PayloadWriter<W> {
|
||||
// in the payload hash.
|
||||
let metadata_hash = h_partial.clone().finish();
|
||||
let metadata_sig = sign_digest(metadata_hash.as_ref(), &key)?;
|
||||
let metadata_sig_raw = util::write_protobuf(&metadata_sig)?;
|
||||
let metadata_sig_raw = metadata_sig.encode_to_vec();
|
||||
write_hash!(inner, [h_full], &metadata_sig_raw)?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -373,7 +392,7 @@ impl<W: Write> PayloadWriter<W> {
|
||||
// Append payload signature.
|
||||
let payload_partial_hash = self.h_partial.clone().finish();
|
||||
let payload_sig = sign_digest(payload_partial_hash.as_ref(), &self.key)?;
|
||||
let payload_sig_raw = util::write_protobuf(&payload_sig)?;
|
||||
let payload_sig_raw = payload_sig.encode_to_vec();
|
||||
write_hash!(self.inner, [self.h_full], &payload_sig_raw)?;
|
||||
|
||||
// Everything before the blob.
|
||||
@@ -404,7 +423,8 @@ impl<W: Write> PayloadWriter<W> {
|
||||
/// [`InstallOperation::data_length`].
|
||||
pub fn begin_next_operation(&mut self) -> Result<bool> {
|
||||
if let Some(operation) = self.operation() {
|
||||
// ZERO/DISCARD operations will not have a length.
|
||||
// Only operations that reference data in the blob will have a
|
||||
// length set.
|
||||
if self.written < operation.data_length.unwrap_or(0) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
@@ -553,11 +573,11 @@ impl<W: Write> CompressedPartitionWriter<W> {
|
||||
/// size, hash, and install operation metadata.
|
||||
pub fn finish(mut self, partition: &mut PartitionUpdate) -> Result<W> {
|
||||
if self.written % u64::from(self.block_size) != 0 {
|
||||
return Err(Error::InvalidPartitionSize(
|
||||
partition.partition_name.clone(),
|
||||
self.written,
|
||||
self.block_size,
|
||||
));
|
||||
return Err(Error::InvalidPartitionSize {
|
||||
name: partition.partition_name.clone(),
|
||||
size: self.written,
|
||||
block_size: self.block_size,
|
||||
});
|
||||
}
|
||||
|
||||
self.inner.flush()?;
|
||||
@@ -580,18 +600,12 @@ impl<W: Write> CompressedPartitionWriter<W> {
|
||||
num_blocks: Some(self.written / u64::from(self.block_size)),
|
||||
};
|
||||
|
||||
let operation = InstallOperation {
|
||||
type_pb: mod_InstallOperation::Type::REPLACE_XZ,
|
||||
// Must be manually updated by the caller.
|
||||
data_offset: None,
|
||||
data_length: Some(size_compressed),
|
||||
src_extents: vec![],
|
||||
src_length: None,
|
||||
dst_extents: vec![extent],
|
||||
dst_length: None,
|
||||
data_sha256_hash: Some(digest_compressed.as_ref().to_vec()),
|
||||
src_sha256_hash: None,
|
||||
};
|
||||
// data_offset must be manually updated by the caller.
|
||||
let mut operation = InstallOperation::default();
|
||||
operation.set_type(Type::ReplaceXz);
|
||||
operation.data_length = Some(size_compressed);
|
||||
operation.dst_extents.push(extent);
|
||||
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
|
||||
|
||||
partition.operations.clear();
|
||||
partition.operations.push(operation);
|
||||
@@ -619,7 +633,7 @@ pub fn verify_payload(
|
||||
mut reader: impl Read + Seek,
|
||||
cert: &Certificate,
|
||||
properties_raw: &str,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let header = PayloadHeader::from_reader(&mut reader)?;
|
||||
reader.rewind()?;
|
||||
@@ -665,7 +679,7 @@ pub fn verify_payload(
|
||||
)?;
|
||||
|
||||
let buf = writer.into_inner();
|
||||
util::read_protobuf::<Signatures>(&buf)?
|
||||
Signatures::decode(buf.as_slice())?
|
||||
};
|
||||
|
||||
// Check the metadata signatures.
|
||||
@@ -675,7 +689,10 @@ pub fn verify_payload(
|
||||
{
|
||||
let actual = reader.stream_position()?;
|
||||
if header.blob_offset != actual {
|
||||
return Err(Error::InvalidBlobOffset(header.blob_offset, actual));
|
||||
return Err(Error::InvalidBlobOffset {
|
||||
expected: header.blob_offset,
|
||||
actual,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,7 +714,7 @@ pub fn verify_payload(
|
||||
let expected = header.blob_offset + payload_signatures_offset;
|
||||
let actual = reader.stream_position()?;
|
||||
if expected != actual {
|
||||
return Err(Error::InvalidPayloadSignaturesOffset(expected, actual));
|
||||
return Err(Error::InvalidPayloadSignaturesOffset { expected, actual });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,7 +731,7 @@ pub fn verify_payload(
|
||||
)?;
|
||||
|
||||
let buf = writer.into_inner();
|
||||
util::read_protobuf::<Signatures>(&buf)?
|
||||
Signatures::decode(buf.as_slice())?
|
||||
};
|
||||
|
||||
// Check the payload signatures.
|
||||
@@ -735,11 +752,11 @@ pub fn verify_payload(
|
||||
let expected_value = expected_properties.get(&key);
|
||||
|
||||
if expected_value != Some(&actual_value) {
|
||||
return Err(Error::InvalidProperty(
|
||||
return Err(Error::InvalidProperty {
|
||||
key,
|
||||
actual_value,
|
||||
expected_value.cloned(),
|
||||
));
|
||||
expected: expected_value.cloned(),
|
||||
actual: actual_value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -753,7 +770,7 @@ pub fn apply_operation(
|
||||
block_size: u32,
|
||||
blob_offset: u64,
|
||||
op: &InstallOperation,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
for extent in &op.dst_extents {
|
||||
let start_block = extent
|
||||
@@ -765,19 +782,19 @@ pub fn apply_operation(
|
||||
|
||||
let out_offset = start_block
|
||||
.checked_mul(block_size.into())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("out_offset"))?;
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("out_offset"))?;
|
||||
let out_data_length = num_blocks
|
||||
.checked_mul(block_size.into())
|
||||
.ok_or_else(|| Error::IntegerTooLarge("out_data_length"))?;
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("out_data_length"))?;
|
||||
|
||||
writer.seek(SeekFrom::Start(out_offset))?;
|
||||
|
||||
let mut hasher = Context::new(&ring::digest::SHA256);
|
||||
|
||||
match op.type_pb {
|
||||
match op.r#type() {
|
||||
// Handle ZERO/DISCARD specially since they don't require access to
|
||||
// the payload blob.
|
||||
mod_InstallOperation::Type::ZERO | mod_InstallOperation::Type::DISCARD => {
|
||||
Type::Zero | Type::Discard => {
|
||||
stream::copy_n_inspect(
|
||||
io::repeat(0),
|
||||
&mut writer,
|
||||
@@ -795,12 +812,12 @@ pub fn apply_operation(
|
||||
.ok_or_else(|| Error::MissingField("data_length"))?;
|
||||
let in_offset = blob_offset
|
||||
.checked_add(data_offset)
|
||||
.ok_or_else(|| Error::IntegerTooLarge("in_offset"))?;
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("in_offset"))?;
|
||||
|
||||
reader.seek(SeekFrom::Start(in_offset))?;
|
||||
|
||||
match other {
|
||||
mod_InstallOperation::Type::REPLACE => {
|
||||
Type::Replace => {
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
@@ -809,7 +826,7 @@ pub fn apply_operation(
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
mod_InstallOperation::Type::REPLACE_BZ => {
|
||||
Type::ReplaceBz => {
|
||||
let mut decoder = BzDecoder::new(&mut writer);
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
@@ -820,7 +837,7 @@ pub fn apply_operation(
|
||||
)?;
|
||||
decoder.finish()?;
|
||||
}
|
||||
mod_InstallOperation::Type::REPLACE_XZ => {
|
||||
Type::ReplaceXz => {
|
||||
let mut decoder = XzDecoder::new(&mut writer);
|
||||
stream::copy_n_inspect(
|
||||
&mut reader,
|
||||
@@ -831,7 +848,7 @@ pub fn apply_operation(
|
||||
)?;
|
||||
decoder.finish()?;
|
||||
}
|
||||
_ => return Err(Error::UnsupportedOperation(op.type_pb)),
|
||||
_ => return Err(Error::UnsupportedOperation(op.r#type())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,13 +856,11 @@ pub fn apply_operation(
|
||||
let expected_digest = op.data_sha256_hash.as_deref();
|
||||
let digest = hasher.finish();
|
||||
|
||||
if expected_digest != Some(digest.as_ref())
|
||||
&& op.type_pb != mod_InstallOperation::Type::ZERO
|
||||
{
|
||||
return Err(Error::MismatchedDigest(
|
||||
expected_digest.map(hex::encode),
|
||||
hex::encode(digest.as_ref()),
|
||||
));
|
||||
if expected_digest != Some(digest.as_ref()) && op.r#type() != Type::Zero {
|
||||
return Err(Error::MismatchedDigest {
|
||||
expected: expected_digest.map(hex::encode),
|
||||
actual: hex::encode(digest.as_ref()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,7 +874,7 @@ pub fn extract_image_to_memory(
|
||||
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
|
||||
header: &PayloadHeader,
|
||||
partition_name: &str,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<SharedCursor> {
|
||||
let partition = header
|
||||
.manifest
|
||||
@@ -874,12 +889,12 @@ pub fn extract_image_to_memory(
|
||||
.par_iter()
|
||||
.map(|op| -> Result<()> {
|
||||
let reader = open_payload()?;
|
||||
let writer = stream.clone();
|
||||
let writer = stream.reopen();
|
||||
|
||||
apply_operation(
|
||||
reader,
|
||||
writer,
|
||||
header.manifest.block_size,
|
||||
header.manifest.block_size(),
|
||||
header.blob_offset,
|
||||
op,
|
||||
cancel_signal,
|
||||
@@ -900,7 +915,7 @@ pub fn extract_images<'a>(
|
||||
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
|
||||
header: &PayloadHeader,
|
||||
partition_names: impl IntoIterator<Item = &'a str>,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let mut remaining = partition_names.into_iter().collect::<HashSet<_>>();
|
||||
// We parallelize at the operation level or else one thread might get stuck
|
||||
@@ -929,7 +944,7 @@ pub fn extract_images<'a>(
|
||||
apply_operation(
|
||||
reader,
|
||||
writer,
|
||||
header.manifest.block_size,
|
||||
header.manifest.block_size(),
|
||||
header.blob_offset,
|
||||
op,
|
||||
cancel_signal,
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
// The gf256 library uses compile-time proc macro code generation. Since
|
||||
// dm-verity supports RS(255, 231) through RS(255, 253), we'll generate RS
|
||||
// implementations for every supported configuration.
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use gf256::rs::rs;
|
||||
use phf::phf_map;
|
||||
|
||||
#[rs(block = 255, data = 231)]
|
||||
mod rs255w231 {}
|
||||
#[rs(block = 255, data = 232)]
|
||||
mod rs255w232 {}
|
||||
#[rs(block = 255, data = 233)]
|
||||
mod rs255w233 {}
|
||||
#[rs(block = 255, data = 234)]
|
||||
mod rs255w234 {}
|
||||
#[rs(block = 255, data = 235)]
|
||||
mod rs255w235 {}
|
||||
#[rs(block = 255, data = 236)]
|
||||
mod rs255w236 {}
|
||||
#[rs(block = 255, data = 237)]
|
||||
mod rs255w237 {}
|
||||
#[rs(block = 255, data = 238)]
|
||||
mod rs255w238 {}
|
||||
#[rs(block = 255, data = 239)]
|
||||
mod rs255w239 {}
|
||||
#[rs(block = 255, data = 240)]
|
||||
mod rs255w240 {}
|
||||
#[rs(block = 255, data = 241)]
|
||||
mod rs255w241 {}
|
||||
#[rs(block = 255, data = 242)]
|
||||
mod rs255w242 {}
|
||||
#[rs(block = 255, data = 243)]
|
||||
mod rs255w243 {}
|
||||
#[rs(block = 255, data = 244)]
|
||||
mod rs255w244 {}
|
||||
#[rs(block = 255, data = 245)]
|
||||
mod rs255w245 {}
|
||||
#[rs(block = 255, data = 246)]
|
||||
mod rs255w246 {}
|
||||
#[rs(block = 255, data = 247)]
|
||||
mod rs255w247 {}
|
||||
#[rs(block = 255, data = 248)]
|
||||
mod rs255w248 {}
|
||||
#[rs(block = 255, data = 249)]
|
||||
mod rs255w249 {}
|
||||
#[rs(block = 255, data = 250)]
|
||||
mod rs255w250 {}
|
||||
#[rs(block = 255, data = 251)]
|
||||
mod rs255w251 {}
|
||||
#[rs(block = 255, data = 252)]
|
||||
mod rs255w252 {}
|
||||
#[rs(block = 255, data = 253)]
|
||||
mod rs255w253 {}
|
||||
|
||||
pub static FN_ENCODE: phf::Map<u8, fn(&mut [u8])> = phf_map! {
|
||||
231u8 => rs255w231::encode,
|
||||
232u8 => rs255w232::encode,
|
||||
233u8 => rs255w233::encode,
|
||||
234u8 => rs255w234::encode,
|
||||
235u8 => rs255w235::encode,
|
||||
236u8 => rs255w236::encode,
|
||||
237u8 => rs255w237::encode,
|
||||
238u8 => rs255w238::encode,
|
||||
239u8 => rs255w239::encode,
|
||||
240u8 => rs255w240::encode,
|
||||
241u8 => rs255w241::encode,
|
||||
242u8 => rs255w242::encode,
|
||||
243u8 => rs255w243::encode,
|
||||
244u8 => rs255w244::encode,
|
||||
245u8 => rs255w245::encode,
|
||||
246u8 => rs255w246::encode,
|
||||
247u8 => rs255w247::encode,
|
||||
248u8 => rs255w248::encode,
|
||||
249u8 => rs255w249::encode,
|
||||
250u8 => rs255w250::encode,
|
||||
251u8 => rs255w251::encode,
|
||||
252u8 => rs255w252::encode,
|
||||
253u8 => rs255w253::encode,
|
||||
};
|
||||
|
||||
pub static FN_IS_CORRECT: phf::Map<u8, fn(&[u8]) -> bool> = phf_map! {
|
||||
231u8 => rs255w231::is_correct,
|
||||
232u8 => rs255w232::is_correct,
|
||||
233u8 => rs255w233::is_correct,
|
||||
234u8 => rs255w234::is_correct,
|
||||
235u8 => rs255w235::is_correct,
|
||||
236u8 => rs255w236::is_correct,
|
||||
237u8 => rs255w237::is_correct,
|
||||
238u8 => rs255w238::is_correct,
|
||||
239u8 => rs255w239::is_correct,
|
||||
240u8 => rs255w240::is_correct,
|
||||
241u8 => rs255w241::is_correct,
|
||||
242u8 => rs255w242::is_correct,
|
||||
243u8 => rs255w243::is_correct,
|
||||
244u8 => rs255w244::is_correct,
|
||||
245u8 => rs255w245::is_correct,
|
||||
246u8 => rs255w246::is_correct,
|
||||
247u8 => rs255w247::is_correct,
|
||||
248u8 => rs255w248::is_correct,
|
||||
249u8 => rs255w249::is_correct,
|
||||
250u8 => rs255w250::is_correct,
|
||||
251u8 => rs255w251::is_correct,
|
||||
252u8 => rs255w252::is_correct,
|
||||
253u8 => rs255w253::is_correct,
|
||||
};
|
||||
|
||||
// Each one of these has its own error type, but the functions can only fail one
|
||||
// way (too many corrupt bytes), so just throw away the error and return an
|
||||
// Option instead.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub static FN_CORRECT_ERRORS: phf::Map<u8, fn(&mut [u8]) -> Option<usize>> = phf_map! {
|
||||
231u8 => |data: &mut [u8]| rs255w231::correct_errors(data).ok(),
|
||||
232u8 => |data: &mut [u8]| rs255w232::correct_errors(data).ok(),
|
||||
233u8 => |data: &mut [u8]| rs255w233::correct_errors(data).ok(),
|
||||
234u8 => |data: &mut [u8]| rs255w234::correct_errors(data).ok(),
|
||||
235u8 => |data: &mut [u8]| rs255w235::correct_errors(data).ok(),
|
||||
236u8 => |data: &mut [u8]| rs255w236::correct_errors(data).ok(),
|
||||
237u8 => |data: &mut [u8]| rs255w237::correct_errors(data).ok(),
|
||||
238u8 => |data: &mut [u8]| rs255w238::correct_errors(data).ok(),
|
||||
239u8 => |data: &mut [u8]| rs255w239::correct_errors(data).ok(),
|
||||
240u8 => |data: &mut [u8]| rs255w240::correct_errors(data).ok(),
|
||||
241u8 => |data: &mut [u8]| rs255w241::correct_errors(data).ok(),
|
||||
242u8 => |data: &mut [u8]| rs255w242::correct_errors(data).ok(),
|
||||
243u8 => |data: &mut [u8]| rs255w243::correct_errors(data).ok(),
|
||||
244u8 => |data: &mut [u8]| rs255w244::correct_errors(data).ok(),
|
||||
245u8 => |data: &mut [u8]| rs255w245::correct_errors(data).ok(),
|
||||
246u8 => |data: &mut [u8]| rs255w246::correct_errors(data).ok(),
|
||||
247u8 => |data: &mut [u8]| rs255w247::correct_errors(data).ok(),
|
||||
248u8 => |data: &mut [u8]| rs255w248::correct_errors(data).ok(),
|
||||
249u8 => |data: &mut [u8]| rs255w249::correct_errors(data).ok(),
|
||||
250u8 => |data: &mut [u8]| rs255w250::correct_errors(data).ok(),
|
||||
251u8 => |data: &mut [u8]| rs255w251::correct_errors(data).ok(),
|
||||
252u8 => |data: &mut [u8]| rs255w252::correct_errors(data).ok(),
|
||||
253u8 => |data: &mut [u8]| rs255w253::correct_errors(data).ok(),
|
||||
};
|
||||
@@ -16,7 +16,9 @@ extern crate alloc;
|
||||
pub mod boot;
|
||||
pub mod cli;
|
||||
pub mod crypto;
|
||||
pub mod escape;
|
||||
pub mod format;
|
||||
pub mod octal;
|
||||
pub mod protobuf;
|
||||
pub mod stream;
|
||||
pub mod util;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
//! Hack to format an integer as an octal string because toml_edit can't output
|
||||
//! octal-formatted integers and many other toml parsers can't parse it either.
|
||||
|
||||
use std::{
|
||||
fmt::{self, Octal},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use num_traits::{Num, PrimInt};
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: PrimInt + Octal,
|
||||
{
|
||||
serializer.serialize_str(&format!("{data:o}"))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: PrimInt,
|
||||
<T as Num>::FromStrRadixErr: fmt::Display,
|
||||
{
|
||||
struct OctalStrVisitor<T>(PhantomData<T>);
|
||||
|
||||
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
|
||||
where
|
||||
T: PrimInt,
|
||||
<T as Num>::FromStrRadixErr: fmt::Display,
|
||||
{
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "a string containing an octal number")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, data: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
T::from_str_radix(data, 8).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(OctalStrVisitor(PhantomData))
|
||||
}
|
||||
+11
-1
@@ -1 +1,11 @@
|
||||
include!(concat!(env!("OUT_DIR"), "/protobuf/mod.rs"));
|
||||
pub mod build {
|
||||
pub mod tools {
|
||||
pub mod releasetools {
|
||||
include!(concat!(env!("OUT_DIR"), "/build.tools.releasetools.rs"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod chromeos_update_engine {
|
||||
include!(concat!(env!("OUT_DIR"), "/chromeos_update_engine.rs"));
|
||||
}
|
||||
|
||||
+53
-35
@@ -12,10 +12,11 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
|
||||
use crate::util::{self, EscapedString};
|
||||
use crate::util;
|
||||
|
||||
/// A trait for seekable readers. This is only needed because `dyn Read + Seek`
|
||||
/// is not a valid construct in Rust yet.
|
||||
@@ -118,7 +119,7 @@ impl<R: Read> ReadStringExt for R {
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {}: {e}", EscapedString::new(e.as_bytes())),
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -138,7 +139,7 @@ impl<R: Read> ReadStringExt for R {
|
||||
String::from_utf8(buf).map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Invalid UTF-8: {}: {e}", EscapedString::new(e.as_bytes())),
|
||||
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -398,9 +399,10 @@ impl<W: Write + Seek> Write for HolePunchingWriter<W> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A file wrapper that uses a userspace file offset. A cloned instances uses
|
||||
/// the same underlying kernel file descriptor, but a new userspace file offset.
|
||||
#[derive(Clone)]
|
||||
/// A file wrapper that uses a userspace file offset. A reopened instance uses
|
||||
/// the same underlying kernel file descriptor, but a new userspace file offset,
|
||||
/// initially set to 0.
|
||||
#[derive(Debug)]
|
||||
pub struct PSeekFile {
|
||||
// The lock is needed because flush() takes a `&mut self`.
|
||||
file: Arc<RwLock<File>>,
|
||||
@@ -415,6 +417,13 @@ impl PSeekFile {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reopen(&self) -> Self {
|
||||
Self {
|
||||
file: self.file.clone(),
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_len(&self, size: u64) -> io::Result<()> {
|
||||
let file_locked = self.file.read().unwrap();
|
||||
file_locked.set_len(size)
|
||||
@@ -509,17 +518,24 @@ impl Seek for PSeekFile {
|
||||
/// readers into different parts of the same [`SharedCursor`] writer and the
|
||||
/// read operation is significantly more expensive than the write operation (eg.
|
||||
/// due to decompression).
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Default)]
|
||||
pub struct SharedCursor {
|
||||
inner: Arc<Mutex<Cursor<Vec<u8>>>>,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl SharedCursor {
|
||||
pub fn clone_rewind(&self) -> Self {
|
||||
let mut new = self.clone();
|
||||
new.offset = 0;
|
||||
new
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reopen(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,6 +576,21 @@ impl Seek for SharedCursor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an I/O error with the [`io::ErrorKind::Interrupted`] type if
|
||||
/// `cancel_signal` is true. This should be called frequently in I/O loops for
|
||||
/// cancellation to be responsive.
|
||||
#[inline]
|
||||
pub fn check_cancel(cancel_signal: &AtomicBool) -> io::Result<()> {
|
||||
if cancel_signal.load(Ordering::SeqCst) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Received cancel signal",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy exactly `size` bytes from `reader` to `writer`, invoking `inspect`
|
||||
/// after every buffer read iteration. If either `reader` or `writer` reaches
|
||||
/// EOF before `size` bytes are copied, an error is returned. The operation is
|
||||
@@ -569,17 +600,12 @@ pub fn copy_n_inspect(
|
||||
mut writer: impl Write,
|
||||
mut size: u64,
|
||||
mut inspect: impl FnMut(&[u8]),
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
let mut buf = [0u8; 16384];
|
||||
|
||||
while size > 0 {
|
||||
if cancel_signal.load(Ordering::SeqCst) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Received cancel signal",
|
||||
));
|
||||
}
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
let to_read = size.min(buf.len() as u64) as usize;
|
||||
reader.read_exact(&mut buf[..to_read])?;
|
||||
@@ -599,7 +625,7 @@ pub fn copy_n(
|
||||
reader: impl Read,
|
||||
writer: impl Write,
|
||||
size: u64,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<()> {
|
||||
copy_n_inspect(reader, writer, size, |_| {}, cancel_signal)
|
||||
}
|
||||
@@ -610,18 +636,13 @@ pub fn copy_n(
|
||||
pub fn copy(
|
||||
mut reader: impl Read,
|
||||
mut writer: impl Write,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> io::Result<u64> {
|
||||
let mut buf = [0u8; 16384];
|
||||
let mut copied = 0;
|
||||
|
||||
loop {
|
||||
if cancel_signal.load(Ordering::SeqCst) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Received cancel signal",
|
||||
));
|
||||
}
|
||||
check_cancel(cancel_signal)?;
|
||||
|
||||
let n = reader.read(&mut buf)?;
|
||||
if n == 0 {
|
||||
@@ -640,10 +661,7 @@ pub fn copy(
|
||||
mod tests {
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use ring::digest::Context;
|
||||
@@ -830,8 +848,8 @@ mod tests {
|
||||
fn pseek_file() {
|
||||
let raw_file = tempfile::tempfile().unwrap();
|
||||
let mut a = PSeekFile::new(raw_file);
|
||||
let mut b = a.clone();
|
||||
let mut c = b.clone();
|
||||
let mut b = a.reopen();
|
||||
let mut c = b.reopen();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
@@ -850,8 +868,8 @@ mod tests {
|
||||
#[test]
|
||||
fn shared_cursor() {
|
||||
let mut a = SharedCursor::default();
|
||||
let mut b = a.clone();
|
||||
let mut c = b.clone();
|
||||
let mut b = a.reopen();
|
||||
let mut c = b.reopen();
|
||||
|
||||
b.write_all(b"foobar").unwrap();
|
||||
c.write_all(b"hello").unwrap();
|
||||
@@ -869,7 +887,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn copy() {
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let mut reader = Cursor::new(b"foobar");
|
||||
let mut writer = Cursor::new([0u8; 6]);
|
||||
|
||||
|
||||
+14
-82
@@ -3,19 +3,19 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::fmt;
|
||||
use std::{fmt, path::Path};
|
||||
|
||||
use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer};
|
||||
use num_traits::PrimInt;
|
||||
|
||||
pub const ZEROS: [u8; 16384] = [0u8; 16384];
|
||||
|
||||
/// A small wrapper to format a number as a size in bytes.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct NumBytes(pub usize);
|
||||
pub struct NumBytes<T: PrimInt>(pub T);
|
||||
|
||||
impl fmt::Debug for NumBytes {
|
||||
impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.0 == 1 {
|
||||
if self.0 == T::one() {
|
||||
write!(f, "<{:?} byte>", self.0)
|
||||
} else {
|
||||
write!(f, "<{:?} bytes>", self.0)
|
||||
@@ -23,72 +23,6 @@ impl fmt::Debug for NumBytes {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a byte slice to format it as ASCII with invalid bytes
|
||||
/// escaped as `\x##`.
|
||||
#[derive(Clone)]
|
||||
pub struct EscapedString<T: AsRef<[u8]>> {
|
||||
inner: T,
|
||||
quoted: bool,
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> EscapedString<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
quoted: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_unquoted(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
quoted: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
|
||||
pub fn get_ref(&self) -> &T {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self) -> &mut T {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> fmt::Debug for EscapedString<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let escaped: String = self
|
||||
.inner
|
||||
.as_ref()
|
||||
.iter()
|
||||
.flat_map(|b| b.escape_ascii())
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
if self.quoted {
|
||||
write!(f, "\"")?;
|
||||
}
|
||||
|
||||
write!(f, "{escaped}")?;
|
||||
|
||||
if self.quoted {
|
||||
write!(f, "\"")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> fmt::Display for EscapedString<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a byte slice is all zeros.
|
||||
pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
while !buf.is_empty() {
|
||||
@@ -103,16 +37,14 @@ pub fn is_zero(mut buf: &[u8]) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Read a protobuf message with no leading size field.
|
||||
pub fn read_protobuf<'a, M: MessageRead<'a>>(data: &'a [u8]) -> quick_protobuf::Result<M> {
|
||||
let mut reader = BytesReader::from_bytes(data);
|
||||
M::from_reader(&mut reader, data)
|
||||
}
|
||||
/// Get the non-empty parent of a path. If the path has no parent in the string,
|
||||
/// then `.` is returned. This does not perform any filesystem operations.
|
||||
pub fn parent_path(path: &Path) -> &Path {
|
||||
if let Some(parent) = path.parent() {
|
||||
if !parent.as_os_str().is_empty() {
|
||||
return parent;
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a protobuf message with no leading size field.
|
||||
pub fn write_protobuf<M: MessageWrite>(message: &M) -> quick_protobuf::Result<Vec<u8>> {
|
||||
let mut buf = Vec::with_capacity(message.get_size());
|
||||
let mut writer = Writer::new(&mut buf);
|
||||
message.write_message(&mut writer)?;
|
||||
Ok(buf)
|
||||
Path::new(".")
|
||||
}
|
||||
|
||||
+126
-16
@@ -3,13 +3,20 @@
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::io::{self, Cursor, Read, Seek, SeekFrom};
|
||||
use std::{
|
||||
io::{Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use pkcs8::DecodePrivateKey;
|
||||
use rsa::RsaPrivateKey;
|
||||
|
||||
use avbroot::{self, format::avb};
|
||||
use avbroot::{
|
||||
self,
|
||||
format::avb::{self, AppendedDescriptorMut, AppendedDescriptorRef},
|
||||
stream::{self, SharedCursor},
|
||||
};
|
||||
|
||||
fn get_test_key() -> RsaPrivateKey {
|
||||
let data = include_str!(concat!(
|
||||
@@ -35,11 +42,14 @@ fn round_trip_root_image() {
|
||||
let (mut header, footer, _) = avb::load_image(reader).unwrap();
|
||||
assert_matches!(footer, None);
|
||||
|
||||
let key = get_test_key();
|
||||
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Clear out the signature-related fields and re-sign.
|
||||
header.hash.clear();
|
||||
header.signature.clear();
|
||||
header.public_key.clear();
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
@@ -50,33 +60,133 @@ fn round_trip_root_image() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_image() {
|
||||
fn round_trip_appended_hash_image() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/vbmeta_appended.img",
|
||||
"/tests/data/vbmeta_appended_hash.img",
|
||||
));
|
||||
let mut reader = Cursor::new(data);
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
let (mut header, footer, _) = avb::load_image(&mut reader).unwrap();
|
||||
let footer = footer.unwrap();
|
||||
let (mut header, footer, image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let mut footer = footer.unwrap();
|
||||
|
||||
let key = get_test_key();
|
||||
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Verify the digest.
|
||||
match header.appended_descriptor().unwrap() {
|
||||
AppendedDescriptorRef::HashTree(_) => panic!("Expected hash descriptor"),
|
||||
AppendedDescriptorRef::Hash(d) => {
|
||||
reader.rewind().unwrap();
|
||||
d.verify(&mut reader, &cancel_signal).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
|
||||
// Copy the partition data.
|
||||
reader.seek(SeekFrom::Start(0)).unwrap();
|
||||
stream::copy_n(
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
footer.original_image_size,
|
||||
&cancel_signal,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Regenerate the digest.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(_) => panic!("Expected hash descriptor"),
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
d.root_digest.clear();
|
||||
writer.rewind().unwrap();
|
||||
d.update(&mut writer, &cancel_signal).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Clear out the signature-related fields and re-sign.
|
||||
header.hash.clear();
|
||||
header.signature.clear();
|
||||
header.public_key.clear();
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
|
||||
let mut writer = Cursor::new(Vec::new());
|
||||
|
||||
// Copy the partition data.
|
||||
let image_size = reader.seek(SeekFrom::End(0)).unwrap();
|
||||
reader.seek(SeekFrom::Start(0)).unwrap();
|
||||
io::copy(&mut reader.take(footer.original_image_size), &mut writer).unwrap();
|
||||
|
||||
// Write new vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &footer, image_size).unwrap();
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
|
||||
let new_data = writer.into_inner();
|
||||
|
||||
assert_eq!(data, new_data.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_hash_tree_image() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/vbmeta_appended_hash_tree.img",
|
||||
));
|
||||
let mut reader = SharedCursor::default();
|
||||
reader.write_all(data).unwrap();
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
let (mut header, footer, image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let mut footer = footer.unwrap();
|
||||
|
||||
let key = get_test_key();
|
||||
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Verify the hash tree and FEC data.
|
||||
match header.appended_descriptor().unwrap() {
|
||||
AppendedDescriptorRef::HashTree(d) => {
|
||||
d.verify(|| Ok(Box::new(reader.reopen())), &cancel_signal)
|
||||
.unwrap();
|
||||
}
|
||||
AppendedDescriptorRef::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
let mut writer = SharedCursor::default();
|
||||
|
||||
// Copy the partition data, excluding the hash tree and FEC data.
|
||||
reader.seek(SeekFrom::Start(0)).unwrap();
|
||||
stream::copy_n(
|
||||
&mut reader,
|
||||
&mut writer,
|
||||
footer.original_image_size,
|
||||
&cancel_signal,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Regenerate the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.root_digest.clear();
|
||||
d.tree_offset = 0;
|
||||
d.tree_size = 0;
|
||||
d.fec_offset = 0;
|
||||
d.fec_size = 0;
|
||||
|
||||
d.update(
|
||||
|| Ok(Box::new(writer.reopen())),
|
||||
|| Ok(Box::new(writer.reopen())),
|
||||
&cancel_signal,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
// Clear out the signature-related fields and re-sign.
|
||||
header.hash.clear();
|
||||
header.signature.clear();
|
||||
header.public_key.clear();
|
||||
header.sign(&key).unwrap();
|
||||
|
||||
// Write new vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
|
||||
let mut new_data = Vec::new();
|
||||
writer.rewind().unwrap();
|
||||
writer.read_to_end(&mut new_data).unwrap();
|
||||
|
||||
assert_eq!(data, new_data.as_slice());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::io::{self, Cursor};
|
||||
|
||||
use avbroot::{
|
||||
self,
|
||||
format::cpio::{CpioEntryType, CpioReader, CpioWriter},
|
||||
util,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn round_trip_archive() {
|
||||
let data = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/archive.cpio",
|
||||
));
|
||||
assert_ne!(data.len() % 512, 0);
|
||||
|
||||
for pad_to_block_size in [false, true] {
|
||||
println!("Pad to block size: {pad_to_block_size}");
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
let mut cpio_reader = CpioReader::new(reader, false);
|
||||
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
|
||||
|
||||
while let Some(entry) = cpio_reader.next_entry().unwrap() {
|
||||
cpio_writer.start_entry(&entry).unwrap();
|
||||
|
||||
if entry.file_type == CpioEntryType::Regular {
|
||||
io::copy(&mut cpio_reader, &mut cpio_writer).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let writer = cpio_writer.finish().unwrap();
|
||||
let new_data = writer.get_ref().as_slice();
|
||||
|
||||
if pad_to_block_size {
|
||||
assert!(new_data.starts_with(data));
|
||||
assert!(util::is_zero(&new_data[data.len()..]));
|
||||
assert_eq!(new_data.len() % 512, 0);
|
||||
} else {
|
||||
assert_eq!(new_data, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -5,9 +5,11 @@ yanked = "deny"
|
||||
notice = "deny"
|
||||
|
||||
[licenses]
|
||||
include-dev = true
|
||||
unlicensed = "deny"
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
@@ -26,15 +28,26 @@ license-files = [
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn"
|
||||
multiple-versions-include-dev = true
|
||||
deny = [
|
||||
# https://github.com/serde-rs/serde/issues/2538
|
||||
{ name = "serde_derive", version = ">=1.0.172,<1.0.184" },
|
||||
]
|
||||
|
||||
[bans.build]
|
||||
executables = "deny"
|
||||
include-dependencies = true
|
||||
include-workspace = true
|
||||
bypass = [
|
||||
# Copies of unmodified crashwrangler objects for old macOS versions.
|
||||
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/xz2-rs",
|
||||
"https://github.com/chenxiaolong/zip",
|
||||
"https://github.com/jongiddy/bzip2-rs",
|
||||
]
|
||||
|
||||
+3
-5
@@ -10,17 +10,15 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
attohttpc = "0.26.1"
|
||||
avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
reqwest = { version = "0.11.20", features = ["stream"] }
|
||||
ring = "0.16.20"
|
||||
ring = "0.17.0"
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
tokio = { version = "1.32.0", features = ["signal", "rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.14"
|
||||
toml_edit = { version = "0.19.14", features = ["serde"] }
|
||||
toml_edit = { version = "0.20.1", features = ["serde"] }
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
[dependencies.zip]
|
||||
|
||||
+62
-62
@@ -1,86 +1,86 @@
|
||||
[magisk]
|
||||
"url" = "https://github.com/topjohnwu/Magisk/releases/download/v26.0/Magisk-v26.0.apk"
|
||||
"hash" = "9e14d3d3ca1f1a2765f8ca215ebbf35ea5fd2896fb147eea581fcaa3b4e77d25"
|
||||
"url" = "https://github.com/topjohnwu/Magisk/releases/download/v26.3/Magisk.v26.3.apk"
|
||||
"hash" = "30ff6ec0709412adfcd0b735c0eb1f61cd9d589af4bdef4cf03c09b986b5acce"
|
||||
|
||||
# Google Pixel 7 Pro
|
||||
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
|
||||
[device.cheetah]
|
||||
url = "https://dl.google.com/dl/android/aosp/cheetah-ota-tq2a.230305.008.c1-6ac5ff2e.zip"
|
||||
url = "https://dl.google.com/dl/android/aosp/cheetah-ota-tq3a.230901.001-6b881553.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 151715 },
|
||||
{ start = 21683150, end = 23179365 },
|
||||
{ start = 2043499985, end = 2043508325 },
|
||||
{ start = 2315331822, end = 2333060126 },
|
||||
{ start = 2344084950, end = 2344090066 },
|
||||
{ start = 0, end = 152523 },
|
||||
{ start = 21750700, end = 23251455 },
|
||||
{ start = 2044485458, end = 2044493782 },
|
||||
{ start = 2316042420, end = 2333812318 },
|
||||
{ start = 2344911202, end = 2344916318 },
|
||||
]
|
||||
hash.original.full = "6ac5ff2e14dc16755ea4ea30e6dbe25103b889a36a465194ef943bd0d665b91c"
|
||||
hash.original.stripped = "549522015f0369a3b89385f532ab62235b47c2c39540bd0adaaf6acc81fdda94"
|
||||
hash.patched.full = "b380720852e2a1e994bcf38064f577bac68b18e799cf8166cb6a7edb8a661cb4"
|
||||
hash.patched.stripped = "560cdf4d7b25fb5bc650f96ae5a3c7593ac229a364bb39887287cfb734f6b377"
|
||||
hash.avb_images."init_boot.img" = "fac9305ce22b897fbfb193968d5346f4c70f6c18c3060bb106226f134ce5f433"
|
||||
hash.avb_images."vbmeta.img" = "0b3d719b751dd43bbec95d02b1bf57b5dae62e42a52502895892a207b773e77d"
|
||||
hash.avb_images."vbmeta_system.img" = "cf8c77dcf0a4474d49b5bdc2a44bdb3646464d5212fbe12aa5d3c5f531742f4f"
|
||||
hash.avb_images."vbmeta_vendor.img" = "660d8f61acd95a4f8ad416b4cbe126e9c039706462b4236ad723953c72ac49a8"
|
||||
hash.avb_images."vendor_boot.img" = "c788d4d8eb7926ad1bfa2a9c000343c3c73cedc580449f5270a711feb620f033"
|
||||
hash.original.full = "6b881553f012d582080642d660e1cf5c9e6fe41e9f1c6ab12ae87fab7894e307"
|
||||
hash.original.stripped = "9befd7887a125ebd8e9ae0555469dababe6bc04b0aa41aa2562036782a6d87e0"
|
||||
hash.patched.full = "6a08ae5c08e42b29ffc476511b6c60de8b77ba3f06c469a0a640e84cb95b9db9"
|
||||
hash.patched.stripped = "479b4e203d432148baaf4eaa69205c2291ec10e8541c9a78e6d9b799e994f1e3"
|
||||
hash.avb_images."init_boot.img" = "3bedb41be98c46241f11219021dfbb799a6d5c89e6e00d45a66f9a5b42e7dfbc"
|
||||
hash.avb_images."vbmeta.img" = "e8e6e898ca73807edb43af0a0e86d4a94b14256def89581970287a9b1bf7a3ee"
|
||||
hash.avb_images."vbmeta_system.img" = "dbb63e08f26f46ccda501d99058d513ff71e3d6302c14d587442b666ff08862a"
|
||||
hash.avb_images."vbmeta_vendor.img" = "6ffa0a10e72c3371653be80de1380832b4d7f8bbf38a2bd861d44a4097a57117"
|
||||
hash.avb_images."vendor_boot.img" = "8f9407435e1ea532e55be418d58e1b3a4f8e0794e3cf4e562e0a0414058c6108"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
[device.bluejay]
|
||||
url = "https://dl.google.com/dl/android/aosp/bluejay-ota-tq2a.230305.008.e1-915f9087.zip"
|
||||
url = "https://dl.google.com/dl/android/aosp/bluejay-ota-tq3a.230901.001-1f1f0abe.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 140787 },
|
||||
{ start = 1060207, end = 21612852 },
|
||||
{ start = 1886150700, end = 1886158844 },
|
||||
{ start = 2069112987, end = 2092260102 },
|
||||
{ start = 2098778558, end = 2098783674 },
|
||||
{ start = 0, end = 142587 },
|
||||
{ start = 1062619, end = 21678441 },
|
||||
{ start = 1920893927, end = 1920902079 },
|
||||
{ start = 2103596124, end = 2126776858 },
|
||||
{ start = 2133324030, end = 2133329146 },
|
||||
]
|
||||
hash.original.full = "915f9087b627b6961be9bb447dc63a7a1083b536753a78715e98641eaeb9c9d1"
|
||||
hash.original.stripped = "a3ee5b6e39e687665c31790118ab9f47715b0b8285ae9847dbf81307f963db14"
|
||||
hash.patched.full = "bfa7b26d90bdc889a7a199439e1564b219e5e2dbfddc1657bc1c6b73229be67e"
|
||||
hash.patched.stripped = "4e56ee4a8554f2b08ffc2f1470ad60b9b63d2b6fb469ed1f7c4b1204bbf8ad7d"
|
||||
hash.avb_images."boot.img" = "a1a705092e7034d20b83c94d78291418e13c343b1573c1b11e0fc884fc00ae62"
|
||||
hash.avb_images."vbmeta.img" = "3c123705be57ab142d2b43beef9b123eaad129df17a523b59b1d63b9122d28b0"
|
||||
hash.avb_images."vbmeta_system.img" = "285b83e4290f3257dc3678f0c3191794830bb2d72fb0969b69fc8f09d7ddff12"
|
||||
hash.avb_images."vbmeta_vendor.img" = "981f736586b91a9f4c93c4208a0d191a35ff15118c6fa505d755ee7fda8b2477"
|
||||
hash.avb_images."vendor_boot.img" = "ceffcb4fdb33aa3bb2c70621060acd5af612206b21cc413d5c2d39fee25144ab"
|
||||
hash.original.full = "1f1f0abe67a6f6f47287be6dafec2c12628de6a715b82ca7beddaf67ad22aca5"
|
||||
hash.original.stripped = "38b15f5efdc7e056bc799859ba72ef9a73e93c61292c59f85fb4b9c31acc5f82"
|
||||
hash.patched.full = "20f3eb522c45f8185c8c121ef2f6a18214345b85f3021d05841e1b002d348b3e"
|
||||
hash.patched.stripped = "105f4305f886a79c03fcabd296a44ef498e2b9e602970c0ef6d8599b74a704b0"
|
||||
hash.avb_images."boot.img" = "a19cb4d4fcc7f3e7d3046c3d19e2f243fb02513ca848ff92ad70d1ada55c4e65"
|
||||
hash.avb_images."vbmeta.img" = "2d817e35f7b6cdc2edce58ef249a966fc677085b70a4aced4c829320aeef0be2"
|
||||
hash.avb_images."vbmeta_system.img" = "98a050f0d53a016fbb78147b1b4a9bca3fde615aa4da34bf62c2e07a395104b5"
|
||||
hash.avb_images."vbmeta_vendor.img" = "fac530f47f237e76f3c7c3cdfe96308170dd8e8f0b227d81114a489c69ba763c"
|
||||
hash.avb_images."vendor_boot.img" = "6b66d8ea772dafc85e8745e88c9e05ff0401d062010c4d1ae0914b93cf3510aa"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
[device.bramble]
|
||||
url = "https://dl.google.com/dl/android/aosp/bramble-ota-tq2a.230305.008.c1-a925dd09.zip"
|
||||
url = "https://dl.google.com/dl/android/aosp/bramble-ota-tq3a.230901.001-6d107ffa.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 140531 },
|
||||
{ start = 496187, end = 11655082 },
|
||||
{ start = 1650283561, end = 1650287993 },
|
||||
{ start = 1884739919, end = 1908081011 },
|
||||
{ start = 1910894027, end = 1910899144 },
|
||||
{ start = 0, end = 142225 },
|
||||
{ start = 497945, end = 11647087 },
|
||||
{ start = 1683478307, end = 1683482723 },
|
||||
{ start = 1917281683, end = 1940628772 },
|
||||
{ start = 1943442548, end = 1943447665 },
|
||||
]
|
||||
hash.original.full = "a925dd09c8d613d46cf72677c16f4fadee18bc21734d57047c6ccf31f672507b"
|
||||
hash.original.stripped = "79322b0b417359e8f072032de676d7e5bd2715a3b3554c48ed5cc9e9a25c6866"
|
||||
hash.patched.full = "cf79cd60acd3635f5085d8bb411d4a8dc4d7e62440f62a14e7ea12f5a9b7cd8a"
|
||||
hash.patched.stripped = "b684a78fe08014f1633e74d1f380e137823ba77a47ddf428bc649364f5548b75"
|
||||
hash.avb_images."boot.img" = "2bbf2c6d2f82d454426b26ac3b4887b26ca0591b458e6cf137207ecbb8f7649d"
|
||||
hash.avb_images."vbmeta.img" = "ab3b2487671b3fc28e163898621d6c068164d335b6ddd9b94e4fde9471c95d66"
|
||||
hash.avb_images."vbmeta_system.img" = "2fcd52d7462916a8510bbb07f2f5a14200afe2de97568396fe75e04c5c283152"
|
||||
hash.avb_images."vendor_boot.img" = "e27f157c4ebf4e958165997a4b87d4de1cfc34f4dea423a153327a27b053cac7"
|
||||
hash.original.full = "6d107ffac1cd3da2c972112acc75957ed725e5c13d57ca724d9bcca5404fcebd"
|
||||
hash.original.stripped = "5b889bdab3bb12ddcd3c243a56e1c58bedada8831069f49d56fe5098fb141e35"
|
||||
hash.patched.full = "c2866d8959fea55884de6281403e7daf435cded62477181354eb76332446af28"
|
||||
hash.patched.stripped = "a9ccefe44818560144f073bef8ae1c2c840cf8fff9dafb0e589bd5fd40cf0fea"
|
||||
hash.avb_images."boot.img" = "8e7278a2e8ae44ffc5475717eb0e1aa56bfb7650aef34375b0fe92f790835f95"
|
||||
hash.avb_images."vbmeta.img" = "b036132b867f52a86eef79716261f35b1eb50e843dc4fe42f72cce67b24ae2db"
|
||||
hash.avb_images."vbmeta_system.img" = "9a7c6fd654e7a92aeffbdbd55ea0d87eee36f4c235e1b505423ad8a13a751a00"
|
||||
hash.avb_images."vendor_boot.img" = "e774aa770fd9c0d19a509f212309f20b08da411f8830a7383646a65571437933"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
[device.sunfish]
|
||||
url = "https://dl.google.com/dl/android/aosp/sunfish-ota-tq2a.230305.008.c1-174fd16b.zip"
|
||||
url = "https://dl.google.com/dl/android/aosp/sunfish-ota-tq3a.230805.001-01fd34b2.zip"
|
||||
sections = [
|
||||
{ start = 0, end = 129700 },
|
||||
{ start = 476996, end = 34035261 },
|
||||
{ start = 1624751947, end = 1624756371 },
|
||||
{ start = 1823132563, end = 1823137581 },
|
||||
{ start = 0, end = 131325 },
|
||||
{ start = 478553, end = 34014760 },
|
||||
{ start = 1658403115, end = 1658407531 },
|
||||
{ start = 1855778567, end = 1855783585 },
|
||||
]
|
||||
hash.original.full = "174fd16b47ef994ea8f3cb0f3fb456df2654b0aa1f9ea6fb8e54e5c6319f2601"
|
||||
hash.original.stripped = "943ce3ae2aac8a0ccd4a7e9d4e38a9c495c39639f2621c6853be4c7a3fa0fc26"
|
||||
hash.patched.full = "cdfffa731f0aca0ab9eff1e7d7bfee8c4716054d64cf0a47e50f50da6cbdb849"
|
||||
hash.patched.stripped = "35b720058c460dd28b00f40bed4bcd1d133522d42d00ba048172b3092a7444ee"
|
||||
hash.avb_images."boot.img" = "22182f2efc7043f35d79e71abb400ce919c6b3b8419405e16469644932367ee6"
|
||||
hash.avb_images."vbmeta.img" = "4cbe171bb37515f59cd4082cc2d982ca1edfeba9db09dd62c8959dd318423888"
|
||||
hash.avb_images."vbmeta_system.img" = "7cdb590bfc1056a5a8c7606ff05e99eb344efe108296682698b5cfe83905e0cd"
|
||||
hash.original.full = "01fd34b206152a3559039161c9874ab03df37da4268b86a9e0be899de5fc0af7"
|
||||
hash.original.stripped = "cc311b5bd46e06cfdefbade794d33aa9bc3ceda4ad4f38bfe9f0dfc17033d207"
|
||||
hash.patched.full = "b788d2d86008a3923734a40fb89ca98533116e51e10f35c0447beb956b236bf5"
|
||||
hash.patched.stripped = "b174f530d63bfe753ab9435a910b01a1abb2e08a8628b50101f876ed2313e536"
|
||||
hash.avb_images."boot.img" = "7cdb8e4a9b79d9c2b4d5a513c219082c0609cf6cab8cf07e122dcaaa2bd1ed63"
|
||||
hash.avb_images."vbmeta.img" = "3679c7224e3e3e0793b4d1a031e116098460a6b4c5f3f88d5a3a0a4c65b21582"
|
||||
hash.avb_images."vbmeta_system.img" = "1d3efa00fd1d44a594c7317072468fa95c23d83d2759d6d6e757783ceeabc594"
|
||||
|
||||
# OnePlus 10 Pro
|
||||
# Build NE2215_11_C.26
|
||||
@@ -107,10 +107,10 @@ sections = [
|
||||
]
|
||||
hash.original.full = "929f892fbd70699cf7f118a119aac1ae1b86351e1ada17715666fa4401e63472"
|
||||
hash.original.stripped = "4eabaf79b6c2b5df305e3ecdc2b9570c0dd27350b4e8d6434584000c4989ff3d"
|
||||
hash.patched.full = "8e9cf3159e57d706047325a7b774dc2dc84f0d56baae100ad54c0c723b621469"
|
||||
hash.patched.stripped = "0b5dcdfdfea742bf6662b286ba260f8572a7b0081bb1129b7274c29214cdfb49"
|
||||
hash.avb_images."boot.img" = "f5dc3b147c54589be8db00ca15257a3688424a9eb88bd9c2cec82ebf4f6bf859"
|
||||
hash.avb_images."recovery.img" = "a42c0bf4f023cd24394184a33ee113783a9c89a7cc4c0c582a5f72cc23b72309"
|
||||
hash.patched.full = "ec576b7430e5a2788e89f8bbe37ee81b116386d9b101865d3b75eb9c51a0db4c"
|
||||
hash.patched.stripped = "ddd0cf1b42564f8c2980320c14f8e0eb125412eb7a13ae8fb52d35424546c235"
|
||||
hash.avb_images."boot.img" = "480d7cf519326fcaa5106ecfbbeb907309068635f7f6a25e5e4571d525c4926a"
|
||||
hash.avb_images."recovery.img" = "eeb0f67e2084174fced510f4b59a82022559ffbe1c20d2c0ea4757fcd989a3af"
|
||||
hash.avb_images."vbmeta.img" = "c022cf79da301a8430af5c49704944c490707fa0306031fe3ea22c39ce4734f6"
|
||||
hash.avb_images."vbmeta_system.img" = "749616b7f04487c05e9e363ad2071a0ab3bae29d497daf1f1a7695f7c8cfa82a"
|
||||
hash.avb_images."vbmeta_vendor.img" = "a6037fce745384425fb12745b8568386b84fb57ca6f94f6e47bcf754de341ae4"
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@
|
||||
|
||||
use std::{collections::BTreeMap, fs, ops::Range, path::Path};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml_edit::{
|
||||
ser::ValueSerializer,
|
||||
@@ -131,9 +131,9 @@ pub fn add_device(document: &mut Document, name: &str, device: &Device) -> Resul
|
||||
|
||||
pub fn load_config(path: &Path) -> Result<(Config, Document)> {
|
||||
let contents =
|
||||
fs::read_to_string(path).with_context(|| anyhow!("Failed to read config: {path:?}"))?;
|
||||
fs::read_to_string(path).with_context(|| format!("Failed to read config: {path:?}"))?;
|
||||
let config: Config = toml_edit::de::from_str(&contents)
|
||||
.with_context(|| anyhow!("Failed to parse config: {path:?}"))?;
|
||||
.with_context(|| format!("Failed to parse config: {path:?}"))?;
|
||||
let document: Document = contents.parse().unwrap();
|
||||
|
||||
Ok((config, document))
|
||||
|
||||
+165
-160
@@ -4,29 +4,28 @@
|
||||
*/
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
collections::{HashMap, VecDeque},
|
||||
fs::{self, OpenOptions},
|
||||
io::{self, Seek, SeekFrom, Write},
|
||||
io::{self, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{self, Sender},
|
||||
},
|
||||
thread::{self, ThreadId},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use avbroot::stream::PSeekFile;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::{
|
||||
runtime::Runtime,
|
||||
signal::ctrl_c,
|
||||
sync::{mpsc, oneshot},
|
||||
task::{self, JoinSet},
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
/// Minimum download chunk size per task.
|
||||
const MIN_CHUNK_SIZE: u64 = 1024 * 1024;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
pub trait ProgressDisplay {
|
||||
fn progress(&mut self, current: u64, total: u64);
|
||||
|
||||
@@ -112,11 +111,21 @@ impl ProgressDisplay for BasicProgressDisplay {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProgressMessage {
|
||||
task_id: u64,
|
||||
bytes: u64,
|
||||
// Controller replies with new ending offset
|
||||
resp: oneshot::Sender<u64>,
|
||||
enum MessageData {
|
||||
Progress {
|
||||
bytes: u64,
|
||||
// Controller replies with a new ending offset.
|
||||
resp: Sender<u64>,
|
||||
},
|
||||
Completion {
|
||||
result: Result<()>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Message {
|
||||
id: ThreadId,
|
||||
data: MessageData,
|
||||
}
|
||||
|
||||
/// Download a contiguous byte range. The number of bytes downloaded per loop
|
||||
@@ -125,65 +134,70 @@ struct ProgressMessage {
|
||||
/// download via the oneshot channel in the `resp` field. An appropriate error
|
||||
/// will be returned if the full range (subject to modification) cannot be fully
|
||||
/// downloaded (eg. premature EOF is an error).
|
||||
async fn download_range(
|
||||
task_id: u64,
|
||||
fn download_range(
|
||||
url: &str,
|
||||
mut file: PSeekFile,
|
||||
initial_range: Range<u64>,
|
||||
channel: mpsc::Sender<ProgressMessage>,
|
||||
channel: Sender<Message>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
assert!(initial_range.start < initial_range.end);
|
||||
|
||||
let client = reqwest::ClientBuilder::new().build()?;
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
let mut response = attohttpc::get(url)
|
||||
.connect_timeout(TIMEOUT)
|
||||
.read_timeout(TIMEOUT)
|
||||
.header(
|
||||
reqwest::header::RANGE,
|
||||
format!("bytes={}-{}", initial_range.start, initial_range.end - 1),
|
||||
"Range",
|
||||
&format!("bytes={}-{}", initial_range.start, initial_range.end - 1),
|
||||
)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.with_context(|| anyhow!("Failed to start download for range: {initial_range:?}"))?;
|
||||
let mut stream = response.bytes_stream();
|
||||
.with_context(|| format!("Failed to start download for range: {initial_range:?}"))?;
|
||||
|
||||
let mut range = initial_range.clone();
|
||||
let mut buf = [0u8; 65536];
|
||||
|
||||
while range.start < range.end {
|
||||
let data = if let Some(x) = stream.next().await {
|
||||
x?
|
||||
} else {
|
||||
return Err(anyhow!("Unexpected EOF from server"));
|
||||
};
|
||||
if cancel_signal.load(Ordering::SeqCst) {
|
||||
bail!("Received cancel signal");
|
||||
}
|
||||
|
||||
let to_read = (range.end - range.start).min(buf.len() as u64) as usize;
|
||||
let n = response.read(&mut buf[..to_read]).with_context(|| {
|
||||
format!(
|
||||
"Failed to download {to_read} bytes at offset {}",
|
||||
range.start,
|
||||
)
|
||||
})?;
|
||||
if n == 0 {
|
||||
bail!("Unexpected EOF from server");
|
||||
}
|
||||
|
||||
// This may overlap with another task's write when a range split occurs,
|
||||
// but the same data will be written anyway, so it's not a huge deal.
|
||||
task::block_in_place(|| {
|
||||
file.seek(SeekFrom::Start(range.start))?;
|
||||
file.write_all(&data)
|
||||
})
|
||||
.with_context(|| {
|
||||
file.seek(SeekFrom::Start(range.start))?;
|
||||
file.write_all(&buf[..n]).with_context(|| {
|
||||
format!(
|
||||
"Failed to write {} bytes to output file at offset {}",
|
||||
data.len(),
|
||||
"Failed to write {n} bytes to output file at offset {}",
|
||||
range.start,
|
||||
)
|
||||
})?;
|
||||
|
||||
let consumed = cmp::min(range.end - range.start, data.len() as u64);
|
||||
range.start += consumed;
|
||||
range.start += n as u64;
|
||||
|
||||
// Report progress to the controller.
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let msg = ProgressMessage {
|
||||
task_id,
|
||||
bytes: consumed,
|
||||
resp: tx,
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let msg = Message {
|
||||
id: thread::current().id(),
|
||||
data: MessageData::Progress {
|
||||
bytes: n as u64,
|
||||
resp: tx,
|
||||
},
|
||||
};
|
||||
channel.send(msg).await?;
|
||||
channel.send(msg)?;
|
||||
|
||||
// Get new ending offset from controller.
|
||||
let new_end = rx.await?;
|
||||
let new_end = rx.recv()?;
|
||||
if new_end != range.end {
|
||||
debug_assert!(new_end <= range.end);
|
||||
range.end = new_end;
|
||||
@@ -193,33 +207,37 @@ async fn download_range(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create download task for a byte range. This just calls [`download_range()`]
|
||||
/// and returns a tuple containing the task ID and the result.
|
||||
async fn download_task(
|
||||
task_id: u64,
|
||||
url: String,
|
||||
/// This just calls [`download_range()`] and sends a completion message to the
|
||||
/// channel with the result.
|
||||
fn download_thread(
|
||||
url: &str,
|
||||
file: PSeekFile,
|
||||
initial_range: Range<u64>,
|
||||
channel: mpsc::Sender<ProgressMessage>,
|
||||
) -> (u64, Result<()>) {
|
||||
(
|
||||
task_id,
|
||||
download_range(task_id, &url, file, initial_range, channel).await,
|
||||
)
|
||||
channel: mpsc::Sender<Message>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) {
|
||||
let result = download_range(url, file, initial_range, channel.clone(), cancel_signal);
|
||||
|
||||
channel
|
||||
.send(Message {
|
||||
id: thread::current().id(),
|
||||
data: MessageData::Completion { result },
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Send a HEAD request to get the value of the Content-Length header.
|
||||
async fn get_content_length(url: &str) -> Result<u64> {
|
||||
let response = reqwest::Client::new()
|
||||
.head(url)
|
||||
fn get_content_length(url: &str) -> Result<u64> {
|
||||
let response = attohttpc::head(url)
|
||||
.connect_timeout(TIMEOUT)
|
||||
.read_timeout(TIMEOUT)
|
||||
.send()
|
||||
.await
|
||||
.and_then(|r| r.error_for_status())
|
||||
.context("Failed to send HEAD request to get Content-Length")?;
|
||||
|
||||
response
|
||||
.headers()
|
||||
.get("content-length")
|
||||
.get("Content-Length")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|h| h.parse().ok())
|
||||
.ok_or_else(|| anyhow!("HEAD request did not return a valid Content-Length"))
|
||||
@@ -229,28 +247,27 @@ async fn get_content_length(url: &str) -> Result<u64> {
|
||||
/// returned as an Err. Normal/expected errors and download progress info are
|
||||
/// reported via `display`. Returns the remaining ranges that need to be
|
||||
/// downloaded.
|
||||
async fn download_ranges(
|
||||
fn download_ranges(
|
||||
url: &str,
|
||||
output: &Path,
|
||||
initial_ranges: Option<&[Range<u64>]>,
|
||||
display: &mut dyn ProgressDisplay,
|
||||
max_tasks: usize,
|
||||
max_threads: usize,
|
||||
max_errors: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<Vec<Range<u64>>> {
|
||||
let file_size = get_content_length(url).await?;
|
||||
let file_size = get_content_length(url)?;
|
||||
|
||||
// Open for writing, but without truncation.
|
||||
let file = task::block_in_place(|| {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| anyhow!("Failed to open for writing: {output:?}"))
|
||||
})?;
|
||||
let file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(output)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
|
||||
task::block_in_place(|| file.set_len(file_size))
|
||||
.with_context(|| anyhow!("Failed to set file size: {output:?}"))?;
|
||||
file.set_len(file_size)
|
||||
.with_context(|| format!("Failed to set file size: {output:?}"))?;
|
||||
|
||||
// Queue of ranges that need to be downloaded.
|
||||
let mut remaining = VecDeque::from(match initial_ranges {
|
||||
@@ -260,114 +277,101 @@ async fn download_ranges(
|
||||
});
|
||||
// Ranges that have failed.
|
||||
let mut failed = Vec::<Range<u64>>::new();
|
||||
// Ranges for currently running tasks.
|
||||
let mut task_ranges = HashMap::<u64, Range<u64>>::new();
|
||||
// Ranges for currently running threads.
|
||||
let mut thread_ranges = HashMap::<ThreadId, Range<u64>>::new();
|
||||
|
||||
// Overall progress.
|
||||
let mut progress = file_size - remaining.iter().map(|r| r.end - r.start).sum::<u64>();
|
||||
display.progress(progress, file_size);
|
||||
|
||||
let mut tasks = JoinSet::new();
|
||||
let mut next_task_id = 0;
|
||||
let mut error_count = 0u8;
|
||||
// Progress messages from tasks.
|
||||
let (tx, mut rx) = mpsc::channel(max_tasks);
|
||||
thread::scope(|scope| {
|
||||
let mut threads = HashMap::new();
|
||||
let mut error_count = 0u8;
|
||||
// Progress messages from threads.
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
loop {
|
||||
// Spawn new tasks.
|
||||
while tasks.len() < max_tasks {
|
||||
if remaining.is_empty() && !tasks.is_empty() {
|
||||
// No more ranges to download. Split another task's range.
|
||||
let (_, old_range) = task_ranges
|
||||
.iter_mut()
|
||||
.max_by_key(|(_, r)| r.end - r.start)
|
||||
.unwrap();
|
||||
let size = old_range.end - old_range.start;
|
||||
loop {
|
||||
// Spawn new threads.
|
||||
while !cancel_signal.load(Ordering::SeqCst) && threads.len() < max_threads {
|
||||
if remaining.is_empty() && !threads.is_empty() {
|
||||
// No more ranges to download. Split another thread's range.
|
||||
let (_, old_range) = thread_ranges
|
||||
.iter_mut()
|
||||
.max_by_key(|(_, r)| r.end - r.start)
|
||||
.unwrap();
|
||||
let size = old_range.end - old_range.start;
|
||||
|
||||
if size >= MIN_CHUNK_SIZE {
|
||||
let new_range = old_range.start + size / 2..old_range.end;
|
||||
old_range.end = new_range.start;
|
||||
remaining.push_back(new_range);
|
||||
if size >= MIN_CHUNK_SIZE {
|
||||
let new_range = old_range.start + size / 2..old_range.end;
|
||||
old_range.end = new_range.start;
|
||||
remaining.push_back(new_range);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(thread_range) = remaining.pop_front() {
|
||||
let file_cloned = file.reopen();
|
||||
let thread_range_cloned = thread_range.clone();
|
||||
let tx_cloned = tx.clone();
|
||||
|
||||
let join_handle = scope.spawn(|| {
|
||||
download_thread(
|
||||
url,
|
||||
file_cloned,
|
||||
thread_range_cloned,
|
||||
tx_cloned,
|
||||
cancel_signal,
|
||||
)
|
||||
});
|
||||
|
||||
thread_ranges.insert(join_handle.thread().id(), thread_range);
|
||||
threads.insert(join_handle.thread().id(), join_handle);
|
||||
} else {
|
||||
// No pending ranges and no running threads can be split.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(task_range) = remaining.pop_front() {
|
||||
tasks.spawn(download_task(
|
||||
next_task_id,
|
||||
url.to_owned(),
|
||||
file.clone(),
|
||||
task_range.clone(),
|
||||
tx.clone(),
|
||||
));
|
||||
|
||||
task_ranges.insert(next_task_id, task_range);
|
||||
next_task_id += 1;
|
||||
} else {
|
||||
// No pending ranges and no running tasks can be split.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
// Interrupted by user.
|
||||
c = ctrl_c() => {
|
||||
c?;
|
||||
if threads.is_empty() {
|
||||
// Nothing left to do.
|
||||
break;
|
||||
}
|
||||
|
||||
// Received progress notification.
|
||||
msg = rx.recv() => {
|
||||
let msg = msg.unwrap();
|
||||
let Message { id, data } = rx.recv().unwrap();
|
||||
|
||||
progress += msg.bytes;
|
||||
display.progress(progress, file_size);
|
||||
match data {
|
||||
MessageData::Progress { bytes, resp } => {
|
||||
progress += bytes;
|
||||
display.progress(progress, file_size);
|
||||
|
||||
let task_range = task_ranges.get_mut(&msg.task_id).unwrap();
|
||||
task_range.start += msg.bytes;
|
||||
let thread_range = thread_ranges.get_mut(&id).unwrap();
|
||||
thread_range.start += bytes;
|
||||
|
||||
msg.resp.send(task_range.end).unwrap();
|
||||
}
|
||||
resp.send(thread_range.end).unwrap();
|
||||
}
|
||||
MessageData::Completion { result } => {
|
||||
threads.remove(&id).unwrap().join().unwrap();
|
||||
|
||||
// Received completion message.
|
||||
r = tasks.join_next() => {
|
||||
match r {
|
||||
// All tasks exited.
|
||||
None => {
|
||||
break;
|
||||
},
|
||||
let thread_range = thread_ranges.remove(&id).unwrap();
|
||||
|
||||
// Download task panicked.
|
||||
Some(Err(e)) => {
|
||||
return Err(e).context("Unexpected panic in download task");
|
||||
}
|
||||
|
||||
// Task completed successfully.
|
||||
Some(Ok((task_id, Ok(_)))) => {
|
||||
task_ranges.remove(&task_id).unwrap();
|
||||
}
|
||||
|
||||
// Task failed.
|
||||
Some(Ok((task_id, Err(e)))) => {
|
||||
display.error(&format!("[Task#{task_id}] {e}"));
|
||||
if let Err(e) = result {
|
||||
display.error(&format!("[{id:?}] {e:?}"));
|
||||
error_count += 1;
|
||||
|
||||
let range = task_ranges.remove(&task_id).unwrap();
|
||||
|
||||
if error_count < max_errors {
|
||||
remaining.push_back(range);
|
||||
remaining.push_back(thread_range);
|
||||
} else {
|
||||
failed.push(range);
|
||||
failed.push(thread_range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
display.finish();
|
||||
|
||||
failed.extend(remaining.into_iter());
|
||||
failed.extend(task_ranges.into_values());
|
||||
failed.extend(remaining);
|
||||
failed.extend(thread_ranges.into_values());
|
||||
|
||||
Ok(failed)
|
||||
}
|
||||
@@ -381,11 +385,11 @@ fn read_state(path: &Path) -> Result<Option<State>> {
|
||||
let data = match fs::read_to_string(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(e) => Err(e).with_context(|| anyhow!("Failed to read download state: {path:?}"))?,
|
||||
Err(e) => Err(e).with_context(|| format!("Failed to read download state: {path:?}"))?,
|
||||
};
|
||||
|
||||
let state = toml_edit::de::from_str(&data)
|
||||
.with_context(|| anyhow!("Failed to parse download state: {path:?}"))?;
|
||||
.with_context(|| format!("Failed to parse download state: {path:?}"))?;
|
||||
|
||||
Ok(Some(state))
|
||||
}
|
||||
@@ -393,7 +397,7 @@ fn read_state(path: &Path) -> Result<Option<State>> {
|
||||
fn write_state(path: &Path, state: &State) -> Result<()> {
|
||||
let data = toml_edit::ser::to_string(state).unwrap();
|
||||
|
||||
fs::write(path, data).with_context(|| anyhow!("Failed to write download state: {path:?}"))?;
|
||||
fs::write(path, data).with_context(|| format!("Failed to write download state: {path:?}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -428,6 +432,7 @@ pub fn download(
|
||||
display: &mut dyn ProgressDisplay,
|
||||
max_tasks: usize,
|
||||
max_errors: u8,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let state_path = state_path(output);
|
||||
let ranges = match read_state(&state_path)? {
|
||||
@@ -435,15 +440,15 @@ pub fn download(
|
||||
None => initial_ranges.map(|r| r.to_vec()),
|
||||
};
|
||||
|
||||
let runtime = Runtime::new()?;
|
||||
let remaining = runtime.block_on(download_ranges(
|
||||
let remaining = download_ranges(
|
||||
url,
|
||||
output,
|
||||
ranges.as_deref(),
|
||||
display,
|
||||
max_tasks,
|
||||
max_errors,
|
||||
))?;
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
if remaining.is_empty() {
|
||||
delete_if_exists(&state_path)?;
|
||||
|
||||
+36
-40
@@ -26,6 +26,7 @@ use anyhow::{anyhow, bail, Context, Result};
|
||||
use avbroot::{
|
||||
cli::ota::{ExtractCli, PatchCli, VerifyCli},
|
||||
format::{ota, payload::PayloadHeader},
|
||||
protobuf::chromeos_update_engine::install_operation::Type,
|
||||
stream::{self, FromReader, HashingReader, PSeekFile, SectionReader},
|
||||
};
|
||||
use clap::Parser;
|
||||
@@ -90,30 +91,30 @@ fn exclusion_to_inclusion(holes: &[Range<u64>], file_range: Range<u64>) -> Resul
|
||||
fn strip_image(
|
||||
input: &Path,
|
||||
output: &Path,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(Vec<Range<u64>>, [u8; 32])> {
|
||||
println!("Stripping {input:?} to {output:?}");
|
||||
|
||||
let mut raw_reader = File::open(input)
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| anyhow!("Failed to open for reading: {input:?}"))?;
|
||||
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.clone()))
|
||||
.with_context(|| anyhow!("Failed to read zip: {input:?}"))?;
|
||||
.with_context(|| format!("Failed to open for reading: {input:?}"))?;
|
||||
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()))
|
||||
.with_context(|| format!("Failed to read zip: {input:?}"))?;
|
||||
let payload_entry = zip_reader
|
||||
.by_name(ota::PATH_PAYLOAD)
|
||||
.with_context(|| anyhow!("Failed to open zip entry: {:?}", ota::PATH_PAYLOAD))?;
|
||||
.with_context(|| format!("Failed to open zip entry: {:?}", ota::PATH_PAYLOAD))?;
|
||||
let payload_offset = payload_entry.data_start();
|
||||
let payload_size = payload_entry.size();
|
||||
|
||||
// Open the payload data directly.
|
||||
let mut payload_reader = SectionReader::new(
|
||||
BufReader::new(raw_reader.clone()),
|
||||
BufReader::new(raw_reader.reopen()),
|
||||
payload_offset,
|
||||
payload_size,
|
||||
)?;
|
||||
|
||||
let header = PayloadHeader::from_reader(&mut payload_reader)
|
||||
.with_context(|| anyhow!("Failed to load OTA payload header"))?;
|
||||
.context("Failed to load OTA payload header")?;
|
||||
|
||||
let required_images =
|
||||
avbroot::cli::ota::get_required_images(&header.manifest, "@gki_ramdisk", true)?
|
||||
@@ -121,13 +122,11 @@ fn strip_image(
|
||||
.collect::<HashSet<_>>();
|
||||
let mut data_holes = vec![];
|
||||
|
||||
use avbroot::protobuf::chromeos_update_engine::mod_InstallOperation::Type;
|
||||
|
||||
for p in &header.manifest.partitions {
|
||||
if !required_images.contains(&p.partition_name) {
|
||||
for op in &p.operations {
|
||||
match op.type_pb {
|
||||
Type::ZERO | Type::DISCARD => continue,
|
||||
match op.r#type() {
|
||||
Type::Zero | Type::Discard => continue,
|
||||
_ => {
|
||||
let start = payload_offset
|
||||
+ header.blob_offset
|
||||
@@ -147,10 +146,10 @@ fn strip_image(
|
||||
|
||||
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
let raw_writer =
|
||||
File::create(output).with_context(|| anyhow!("Failed to open for writing: {output:?}"))?;
|
||||
File::create(output).with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
raw_writer
|
||||
.set_len(file_size)
|
||||
.with_context(|| anyhow!("Failed to set file size: {output:?}"))?;
|
||||
.with_context(|| format!("Failed to set file size: {output:?}"))?;
|
||||
let mut buf_writer = BufWriter::new(raw_writer);
|
||||
let mut buf_reader = BufReader::new(raw_reader);
|
||||
|
||||
@@ -194,11 +193,11 @@ fn url_filename(url: &str) -> Result<&str> {
|
||||
.ok_or_else(|| anyhow!("Failed to determine filename from URL: {url}"))
|
||||
}
|
||||
|
||||
fn hash_file(path: &Path, cancel_signal: &Arc<AtomicBool>) -> Result<[u8; 32]> {
|
||||
fn hash_file(path: &Path, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
|
||||
println!("Calculating hash of {path:?}");
|
||||
|
||||
let raw_reader =
|
||||
File::open(path).with_context(|| anyhow!("Failed to open for reading: {path:?}"))?;
|
||||
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let buf_reader = BufReader::new(raw_reader);
|
||||
let context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
let mut hashing_reader = HashingReader::new(buf_reader, context);
|
||||
@@ -211,7 +210,7 @@ fn hash_file(path: &Path, cancel_signal: &Arc<AtomicBool>) -> Result<[u8; 32]> {
|
||||
Ok(digest.as_ref().try_into().unwrap())
|
||||
}
|
||||
|
||||
fn verify_hash(path: &Path, sha256: &[u8; 32], cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
fn verify_hash(path: &Path, sha256: &[u8; 32], cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let digest = hash_file(path, cancel_signal)?;
|
||||
|
||||
if sha256 != digest.as_ref() {
|
||||
@@ -239,7 +238,7 @@ fn download_file(
|
||||
sections: Option<&[Range<u64>]>,
|
||||
path_is_dir: bool,
|
||||
validate: Validate,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PathBuf> {
|
||||
let path = if path_is_dir {
|
||||
path.join(url_filename(url)?)
|
||||
@@ -249,7 +248,7 @@ fn download_file(
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| anyhow!("Failed to create directory: {parent:?}"))?;
|
||||
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
|
||||
}
|
||||
|
||||
let mut do_validate = validate != Validate::Never;
|
||||
@@ -270,6 +269,7 @@ fn download_file(
|
||||
&mut display,
|
||||
DOWNLOAD_TASKS,
|
||||
DOWNLOAD_RETRIES,
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ fn download_magisk(
|
||||
config: &Config,
|
||||
work_dir: &Path,
|
||||
revalidate: bool,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PathBuf> {
|
||||
download_file(
|
||||
&work_dir.join("magisk"),
|
||||
@@ -307,7 +307,7 @@ fn download_image(
|
||||
work_dir: &Path,
|
||||
stripped: bool,
|
||||
revalidate: bool,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<PathBuf> {
|
||||
let info = &config.device[device];
|
||||
let mut path = work_dir.join(device);
|
||||
@@ -376,7 +376,7 @@ fn test_keys() -> Result<(TempDir, Vec<OsString>, Vec<OsString>)> {
|
||||
("ota.crt", &ota_cert[..], Some("--cert-ota"), Some("--cert-ota")),
|
||||
] {
|
||||
let path = temp_dir.path().join(name);
|
||||
fs::write(&path, data).with_context(|| anyhow!("Failed to write test key: {path:?}"))?;
|
||||
fs::write(&path, data).with_context(|| format!("Failed to write test key: {path:?}"))?;
|
||||
|
||||
if let Some(arg) = patch_arg {
|
||||
patch_args.push(arg.into());
|
||||
@@ -395,7 +395,7 @@ fn patch_image(
|
||||
input_file: &Path,
|
||||
output_file: &Path,
|
||||
extra_args: &[OsString],
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
println!("Patching {input_file:?}");
|
||||
|
||||
@@ -425,11 +425,7 @@ fn patch_image(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_image(
|
||||
input_file: &Path,
|
||||
output_dir: &Path,
|
||||
cancel_signal: &Arc<AtomicBool>,
|
||||
) -> Result<()> {
|
||||
fn extract_image(input_file: &Path, output_dir: &Path, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
println!("Extracting AVB partitions from {input_file:?}");
|
||||
|
||||
let cli = ExtractCli::try_parse_from([
|
||||
@@ -444,7 +440,7 @@ fn extract_image(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_image(input_file: &Path, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
fn verify_image(input_file: &Path, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
println!("Verifying signatures in {input_file:?}");
|
||||
|
||||
let (_temp_key_dir, _, key_args) = test_keys()?;
|
||||
@@ -464,12 +460,12 @@ fn verify_image(input_file: &Path, cancel_signal: &Arc<AtomicBool>) -> Result<()
|
||||
|
||||
fn get_magisk_partition(path: &Path) -> Result<String> {
|
||||
let raw_reader =
|
||||
File::open(path).with_context(|| anyhow!("Failed to open for reading: {path:?}"))?;
|
||||
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(raw_reader))
|
||||
.with_context(|| anyhow!("Failed to read zip: {path:?}"))?;
|
||||
.with_context(|| format!("Failed to read zip: {path:?}"))?;
|
||||
let payload_entry = zip
|
||||
.by_name(ota::PATH_PAYLOAD)
|
||||
.with_context(|| anyhow!("Failed to open zip entry: {:?}", ota::PATH_PAYLOAD))?;
|
||||
.with_context(|| format!("Failed to open zip entry: {:?}", ota::PATH_PAYLOAD))?;
|
||||
let payload_offset = payload_entry.data_start();
|
||||
let payload_size = payload_entry.size();
|
||||
|
||||
@@ -480,7 +476,7 @@ fn get_magisk_partition(path: &Path) -> Result<String> {
|
||||
let mut payload_reader = SectionReader::new(buf_reader, payload_offset, payload_size)?;
|
||||
|
||||
let header = PayloadHeader::from_reader(&mut payload_reader)
|
||||
.with_context(|| anyhow!("Failed to load OTA payload header"))?;
|
||||
.context("Failed to load OTA payload header")?;
|
||||
let images = avbroot::cli::ota::get_partitions_by_type(&header.manifest)?;
|
||||
|
||||
Ok(images["@gki_ramdisk"].clone())
|
||||
@@ -509,7 +505,7 @@ fn filter_devices<'a>(config: &'a Config, cli: &'a DeviceGroup) -> Result<BTreeS
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
fn strip_subcommand(cli: &StripCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
fn strip_subcommand(cli: &StripCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (sections, sha256) = strip_image(&cli.input, &cli.output, cancel_signal)?;
|
||||
|
||||
println!("Preserved sections:");
|
||||
@@ -522,7 +518,7 @@ fn strip_subcommand(cli: &StripCli, cancel_signal: &Arc<AtomicBool>) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_subcommand(cli: &AddCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
fn add_subcommand(cli: &AddCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (config, mut document) = config::load_config(&cli.config.config)?;
|
||||
|
||||
let image_dir = cli.config.work_dir.join(&cli.device);
|
||||
@@ -625,18 +621,18 @@ fn add_subcommand(cli: &AddCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
|
||||
let config_serialized = document.to_string();
|
||||
fs::write(&cli.config.config, config_serialized)
|
||||
.with_context(|| anyhow!("Failed to write config: {:?}", cli.config.config))?;
|
||||
.with_context(|| format!("Failed to write config: {:?}", cli.config.config))?;
|
||||
|
||||
if cli.patch.delete_on_success {
|
||||
for path in [full_ota_patched, stripped_ota_patched] {
|
||||
fs::remove_file(&path).with_context(|| anyhow!("Failed to delete file: {path:?}"))?;
|
||||
fs::remove_file(&path).with_context(|| format!("Failed to delete file: {path:?}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_subcommand(cli: &DownloadCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
fn download_subcommand(cli: &DownloadCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (config, _) = config::load_config(&cli.config.config)?;
|
||||
let devices = filter_devices(&config, &cli.device)?;
|
||||
|
||||
@@ -667,7 +663,7 @@ fn download_subcommand(cli: &DownloadCli, cancel_signal: &Arc<AtomicBool>) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_subcommand(cli: &TestCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
|
||||
fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (config, _) = config::load_config(&cli.config.config)?;
|
||||
let devices = filter_devices(&config, &cli.device)?;
|
||||
|
||||
@@ -745,7 +741,7 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &Arc<AtomicBool>) -> Result<()>
|
||||
];
|
||||
|
||||
fs::remove_file(&patched_file)
|
||||
.with_context(|| anyhow!("Failed to delete file: {patched_file:?}"))?;
|
||||
.with_context(|| format!("Failed to delete file: {patched_file:?}"))?;
|
||||
|
||||
patch_image(&image_file, &patched_file, &prepatched_args, cancel_signal)?;
|
||||
|
||||
@@ -753,7 +749,7 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &Arc<AtomicBool>) -> Result<()>
|
||||
|
||||
if cli.patch.delete_on_success {
|
||||
fs::remove_file(&patched_file)
|
||||
.with_context(|| anyhow!("Failed to delete file: {patched_file:?}"))?;
|
||||
.with_context(|| format!("Failed to delete file: {patched_file:?}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/hfuzz_target/
|
||||
HONGGFUZZ.REPORT.TXT
|
||||
*.honggfuzz.cov
|
||||
*.fuzz
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "fuzz"
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
repository.workspace = true
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
# There's currently no good way to exclude a workspace member for a specific
|
||||
# platform, so we just compile empty stubs on Windows (which isn't supported by
|
||||
# honggfuzz).
|
||||
# https://github.com/rust-lang/cargo/issues/5220
|
||||
# https://github.com/rust-lang/cargo/issues/6179
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
avbroot = { path = "../avbroot" }
|
||||
honggfuzz = "0.5.55"
|
||||
@@ -0,0 +1,53 @@
|
||||
# Fuzzing
|
||||
|
||||
While avbroot's parsers are all memory-safe, it is still possible for panics or crashes to occur, for example due to excessive memory allocation, integer overflow, or division by zero. Fuzzing helps to identify these issues by randomizing inputs in a way that tries to increase code coverage.
|
||||
|
||||
## Running the fuzzers
|
||||
|
||||
1. Install the cargo honggfuzz commands.
|
||||
|
||||
```bash
|
||||
cargo install honggfuzz
|
||||
```
|
||||
|
||||
2. Pick a fuzz target to run. A fuzz target is the name of the source file in [`src/bin/`](./src/bin) without the `.rs` extension.
|
||||
|
||||
The list of targets can be queried programmatically with:
|
||||
|
||||
```bash
|
||||
cargo read-manifest | jq -r '.targets[].name'
|
||||
```
|
||||
|
||||
3. Run the fuzzer.
|
||||
|
||||
```bash
|
||||
cargo hfuzz run <fuzz target>
|
||||
```
|
||||
|
||||
This will run forever until it is manually killed. At the top of the screen, a summary section like the following is shown:
|
||||
|
||||
```
|
||||
Iterations : 31,243 [31.24k]
|
||||
Mode [1/3] : Feedback Driven Dry Run [2486/4085]
|
||||
Target : hfuzz_target/x86_64-unknown-linux-gnu/release/bootimage
|
||||
Threads : 8, CPUs: 16, CPU%: 800% [50%/CPU]
|
||||
Speed : 36,126/sec [avg: 31,243]
|
||||
Crashes : 53 [unique: 1, blocklist: 0, verified: 0]
|
||||
Timeouts : 0 [1 sec]
|
||||
Corpus Size : 1,424, max: 24,576 bytes, init: 4,085 files
|
||||
Cov Update : 0 days 00 hrs 00 mins 00 secs ago
|
||||
Coverage : edge: 897/224,621 [0%] pc: 2 cmp: 34,736
|
||||
```
|
||||
|
||||
When a crash occurs, the `Crashes` counter will increment and the input data that triggered the crash will be written to `hfuzz_workspace/<fuzz target>/*.fuzz`. New files are only written for unique crashes.
|
||||
|
||||
4. If a crash occurs, run the following command to trigger the crash in a debugger.
|
||||
|
||||
```bash
|
||||
cargo hfuzz run-debug <fuzz target> \
|
||||
hfuzz_workspace/<fuzz_target>/<input file>.fuzz
|
||||
```
|
||||
|
||||
This defaults to using `rust-lldb`. To use `rust-gdb` instead, set the `HFUZZ_DEBUGGER` environment variable to `rust-gdb`.
|
||||
|
||||
Alternatively, just feed the input file to the appropriate avbroot command directly (eg. `avbroot boot info -i hfuzz_workspace/<fuzz_target>/<input file>.fuzz` for boot images).
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vbmeta_appended_hash.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vbmeta_appended_hash_tree.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vbmeta_root.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v0.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v1.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v2.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v3.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v4.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/boot_v4_vts.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vendor_v3.img
|
||||
@@ -0,0 +1 @@
|
||||
../../../../avbroot/tests/data/vendor_v4.img
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,21 @@
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::Cursor;
|
||||
|
||||
use avbroot::format::avb;
|
||||
use honggfuzz::fuzz;
|
||||
|
||||
pub fn main() {
|
||||
loop {
|
||||
fuzz!(|data: &[u8]| {
|
||||
let reader = Cursor::new(data);
|
||||
let _ = avb::load_image(reader);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(windows))]
|
||||
fuzz::main();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::io::Cursor;
|
||||
|
||||
use avbroot::{format::bootimage::BootImage, stream::FromReader};
|
||||
use honggfuzz::fuzz;
|
||||
|
||||
pub fn main() {
|
||||
loop {
|
||||
fuzz!(|data: &[u8]| {
|
||||
let reader = Cursor::new(data);
|
||||
let _ = BootImage::from_reader(reader);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(windows))]
|
||||
fuzz::main();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::{io::Cursor, sync::atomic::AtomicBool};
|
||||
|
||||
use avbroot::format::cpio;
|
||||
use honggfuzz::fuzz;
|
||||
|
||||
pub fn main() {
|
||||
loop {
|
||||
fuzz!(|data: &[u8]| {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let reader = Cursor::new(data);
|
||||
let _ = cpio::load(reader, true, &cancel_signal);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(windows))]
|
||||
fuzz::main();
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#[cfg(not(windows))]
|
||||
mod fuzz {
|
||||
use std::{io::Cursor, sync::atomic::AtomicBool};
|
||||
|
||||
use avbroot::{
|
||||
format::fec::FecImage,
|
||||
stream::{FromReader, SharedCursor, WriteZerosExt},
|
||||
};
|
||||
use honggfuzz::fuzz;
|
||||
|
||||
pub fn main() {
|
||||
loop {
|
||||
fuzz!(|data: &[u8]| {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
if let Ok(fec) = FecImage::from_reader(reader) {
|
||||
let mut input = SharedCursor::new();
|
||||
|
||||
// Allow verify() to get further, but don't blow up the host
|
||||
// with excessive memory usage.
|
||||
if fec.data_size < 64 * 1024 * 1024 {
|
||||
input.write_zeros_exact(fec.data_size).unwrap();
|
||||
}
|
||||
|
||||
let _ = fec.verify(|| Ok(Box::new(input.reopen())), &cancel_signal);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(not(windows))]
|
||||
fuzz::main();
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
id=com.chiller3.avbroot.clearotacerts
|
||||
name=clearotacerts
|
||||
version=v2.0.0
|
||||
versionCode=131072
|
||||
version=v2.2.0
|
||||
versionCode=131584
|
||||
author=chenxiaolong
|
||||
description=Block A/B OTAs by clearing verification certificates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=com.chiller3.avbroot.oemunlockonboot
|
||||
name=oemunlockonboot
|
||||
version=v2.0.0
|
||||
versionCode=131072
|
||||
version=v2.2.0
|
||||
versionCode=131584
|
||||
author=chenxiaolong
|
||||
description=Enable OEM unlocking on every boot
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ clap = { version = "4.4.1", features = ["derive"] }
|
||||
human-sort = "0.2.2"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
tempfile = "3.8.0"
|
||||
toml_edit = "0.19.14"
|
||||
toml_edit = "0.20.1"
|
||||
walkdir = "2.3.3"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
|
||||
+5
-22
@@ -20,16 +20,11 @@ use crate::WORKSPACE_DIR;
|
||||
struct LinkRef {
|
||||
link_type: String,
|
||||
number: u32,
|
||||
user: Option<String>,
|
||||
}
|
||||
|
||||
impl fmt::Display for LinkRef {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "[{} #{}", self.link_type, self.number)?;
|
||||
if let Some(u) = &self.user {
|
||||
write!(f, " @{}", u)?;
|
||||
}
|
||||
write!(f, "]")
|
||||
write!(f, "[{} #{}]", self.link_type, self.number)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +50,7 @@ fn check_brackets(line: &str) -> Result<()> {
|
||||
|
||||
fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
|
||||
let re_standalone_link = Regex::new(r"\[([^\]]+)\]($|[^\(\[])")?;
|
||||
let re_auto_link = Regex::new(r"^(Issue|PR) #([0-9]+)(?: @([a-zA-Z0-9\-]+))?$")?;
|
||||
let re_auto_link = Regex::new(r"^(Issue|PR) #([0-9]+)?$")?;
|
||||
let mut links = BTreeMap::<LinkRef, String>::new();
|
||||
|
||||
let raw_reader = File::open(path)?;
|
||||
@@ -85,22 +80,11 @@ fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
|
||||
let link_ref = captures.get(0).unwrap().as_str();
|
||||
let link_type = captures.get(1).unwrap().as_str();
|
||||
let number: u32 = captures.get(2).unwrap().as_str().parse()?;
|
||||
let user = captures.get(3).map(|c| c.as_str());
|
||||
|
||||
let link = match link_type {
|
||||
"Issue" => {
|
||||
if user.is_some() {
|
||||
bail!("{link_ref} should not have a username");
|
||||
}
|
||||
format!("{base_url}/issues/{number}")
|
||||
}
|
||||
"PR" => {
|
||||
if user.is_none() {
|
||||
bail!("{link_ref} should have a username");
|
||||
}
|
||||
format!("{base_url}/pull/{number}")
|
||||
}
|
||||
t => bail!("Unknown link type: {t:?}"),
|
||||
"Issue" => format!("{base_url}/issues/{number}"),
|
||||
"PR" => format!("{base_url}/pull/{number}"),
|
||||
t => bail!("Unknown link type in {link_ref:?}: {t:?}"),
|
||||
};
|
||||
|
||||
// #0 is used for examples only.
|
||||
@@ -109,7 +93,6 @@ fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
|
||||
LinkRef {
|
||||
link_type: link_type.to_owned(),
|
||||
number,
|
||||
user: user.map(|u| u.to_owned()),
|
||||
},
|
||||
link,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user