mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f95ef1f8c1 | |||
| e5a373b59b | |||
| ddb8611e27 | |||
| 02b172b1c9 | |||
| 8cbcb44e23 | |||
| 144e635257 | |||
| 0c17db4fc4 | |||
| a06b7ade86 | |||
| 22a0196cae | |||
| f12e2633ce | |||
| 4fc694039e | |||
| 1b4789a337 | |||
| 6387b8ae4d | |||
| f82db7aafc | |||
| fce0bfabab | |||
| 07b6901d8e | |||
| b2b3d98005 | |||
| 21c1dc86ce | |||
| 462084ac18 | |||
| 4c3e1efd99 | |||
| 9b7f27e384 | |||
| 2120827755 | |||
| 8b33645026 | |||
| eafc6ad3b4 | |||
| 188d256d03 | |||
| c1836d01a4 | |||
| e8532870b1 | |||
| b854654cab | |||
| f68a492131 | |||
| c22a5d6eca | |||
| 2631228e8d | |||
| f2b5d9646e | |||
| 4991c5192e | |||
| 8ef9f89cdd |
@@ -47,7 +47,7 @@ jobs:
|
||||
android_api: '31'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
key: ${{ matrix.artifact.name }}
|
||||
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
run: cp LICENSE README.md target/output/
|
||||
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
|
||||
path: |
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
|
||||
uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2.0.20
|
||||
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
echo "version=${version}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
|
||||
@@ -7,6 +7,41 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.30.2
|
||||
|
||||
* Fix the symlink target path being incorrectly XZ-compressed when the Magisk patcher replaces a symlink with a regular file ([Issue #610], [PR #611])
|
||||
|
||||
### Version 3.30.1
|
||||
|
||||
* Fix `avbroot payload pack` and `avbroot zip pack --payload` for non-VABC devices ([Issue #607], [PR #608])
|
||||
|
||||
### Version 3.30.0
|
||||
|
||||
* Add `avbroot ota list` subcommand to list all partitions in an OTA ([Issue #603], [PR #606])
|
||||
* Update dependencies ([PR #605])
|
||||
|
||||
### Version 3.29.1
|
||||
|
||||
* Increase memory limit for payload manifest size to 16 MiB ([Issue #593], [PR #594])
|
||||
|
||||
### Version 3.29.0
|
||||
|
||||
* Add support for unpacking/packing payloads at the same time as the OTA in `avbroot zip {unpack,pack}` ([Discussion #582], [PR #586])
|
||||
* Add new `--skeleton` option to `avbroot payload unpack` ([Discussion #582], [PR #587])
|
||||
|
||||
### Version 3.28.0
|
||||
|
||||
* Add support for unpacking metadata from delta payloads in `avbroot payload unpack` ([Discussion #582], [PR #584])
|
||||
|
||||
### Version 3.27.0
|
||||
|
||||
* Add support for LZMA compression for ramdisks ([Issue #578], [PR #580])
|
||||
* Update dependencies ([PR #579])
|
||||
|
||||
### Version 3.26.1
|
||||
|
||||
* Add support for Magisk 30700 ([PR #573])
|
||||
|
||||
### Version 3.26.0
|
||||
|
||||
* Add new `--re-sign` option to `avbroot ota patch` for re-signing partitions that would otherwise be unmodified ([Issue #569], [PR #570])
|
||||
@@ -432,6 +467,7 @@ Behind-the-scenes changes:
|
||||
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
|
||||
[Discussion #417]: https://github.com/chenxiaolong/avbroot/discussions/417
|
||||
[Discussion #426]: https://github.com/chenxiaolong/avbroot/discussions/426
|
||||
[Discussion #582]: https://github.com/chenxiaolong/avbroot/discussions/582
|
||||
[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
|
||||
@@ -470,6 +506,11 @@ Behind-the-scenes changes:
|
||||
[Issue #537]: https://github.com/chenxiaolong/avbroot/issues/537
|
||||
[Issue #554]: https://github.com/chenxiaolong/avbroot/issues/554
|
||||
[Issue #569]: https://github.com/chenxiaolong/avbroot/issues/569
|
||||
[Issue #578]: https://github.com/chenxiaolong/avbroot/issues/578
|
||||
[Issue #593]: https://github.com/chenxiaolong/avbroot/issues/593
|
||||
[Issue #603]: https://github.com/chenxiaolong/avbroot/issues/603
|
||||
[Issue #607]: https://github.com/chenxiaolong/avbroot/issues/607
|
||||
[Issue #610]: https://github.com/chenxiaolong/avbroot/issues/610
|
||||
[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
|
||||
@@ -694,3 +735,14 @@ Behind-the-scenes changes:
|
||||
[PR #565]: https://github.com/chenxiaolong/avbroot/pull/565
|
||||
[PR #570]: https://github.com/chenxiaolong/avbroot/pull/570
|
||||
[PR #571]: https://github.com/chenxiaolong/avbroot/pull/571
|
||||
[PR #573]: https://github.com/chenxiaolong/avbroot/pull/573
|
||||
[PR #579]: https://github.com/chenxiaolong/avbroot/pull/579
|
||||
[PR #580]: https://github.com/chenxiaolong/avbroot/pull/580
|
||||
[PR #584]: https://github.com/chenxiaolong/avbroot/pull/584
|
||||
[PR #586]: https://github.com/chenxiaolong/avbroot/pull/586
|
||||
[PR #587]: https://github.com/chenxiaolong/avbroot/pull/587
|
||||
[PR #594]: https://github.com/chenxiaolong/avbroot/pull/594
|
||||
[PR #605]: https://github.com/chenxiaolong/avbroot/pull/605
|
||||
[PR #606]: https://github.com/chenxiaolong/avbroot/pull/606
|
||||
[PR #608]: https://github.com/chenxiaolong/avbroot/pull/608
|
||||
[PR #611]: https://github.com/chenxiaolong/avbroot/pull/611
|
||||
|
||||
Generated
+281
-189
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.26.0"
|
||||
version = "3.30.2"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
+7
-3
@@ -340,7 +340,7 @@ avbroot payload unpack -i <input payload>
|
||||
|
||||
This subcommand unpacks the payload header information to `payload.toml` and the partition images to the `payload_images` directory.
|
||||
|
||||
Only full payload binaries can be unpacked. Delta payload binaries from incremental OTAs are not supported.
|
||||
Data can only be unpacked from full payload binaries. For delta payloads, either use `--no-output-images` to skip image extraction or use `--skeleton` to create properly sized sparse files with no data.
|
||||
|
||||
### Packing a payload binary
|
||||
|
||||
@@ -429,19 +429,23 @@ This set of commands is for working with raw OTA zip files. They are intentional
|
||||
### Unpacking an OTA zip
|
||||
|
||||
```bash
|
||||
avbroot zip unpack -i <input OTA>
|
||||
avbroot zip unpack -i <input OTA> [--payload]
|
||||
```
|
||||
|
||||
This subcommand unpacks the OTA metadata to `ota.toml` and the OTA files to the `ota_files` directory.
|
||||
|
||||
When `--payload` is specified, the payload and OTA zip will be unpacked at the same time, which is significantly more efficient that unpack the payload and OTA zip separately. The payload unpacking process writes to the same `payload.toml` and `payload_images` outputs as `avbroot payload unpack`.
|
||||
|
||||
### Packing an OTA zip
|
||||
|
||||
```bash
|
||||
avbroot zip pack -o <output OTA> -k <OTA private key>
|
||||
avbroot zip pack -o <output OTA> -k <OTA private key> [--payload]
|
||||
```
|
||||
|
||||
This subcommand packs a new OTA zip from the `ota.toml` file and `ota_files` directory. Any files in the `ota_files` directory that don't have a corresponding entry in `ota.toml` are silently ignored.
|
||||
|
||||
When `--payload` is specified, the payload and OTA zip will be packed at the same time, which is significantly more efficient than packing the payload and OTA zip separately. The payload packing process reads the same `payload.toml` and `payload_images` inputs as `avbroot payload pack`.
|
||||
|
||||
When packing an OTA zip, the `metadata.property_files` field in `ota.toml` may potentially be recomputed. To write a TOML file containing the new values, use `--output-info <output TOML>`. It is safe to overwrite the existing `ota.toml` if desired.
|
||||
|
||||
### Repacking an OTA zip
|
||||
|
||||
@@ -134,9 +134,26 @@ The commands above are provided for convenience. avbroot is compatible with any
|
||||
|
||||
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 (triggering a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
|
||||
|
||||
### AOSP keys
|
||||
|
||||
If you're building Android from source, this is how avbroot's keys correspond to AOSP's keys:
|
||||
|
||||
| avbroot | AOSP |
|
||||
|----------------|-----------------------|
|
||||
| `avb.key` | `avb.pem` |
|
||||
| `avb_pkmd.bin` | `avb_pkmd.bin` |
|
||||
| `ota.key` | `releasekey.pk8` |
|
||||
| `ota.crt` | `releasekey.x509.pem` |
|
||||
|
||||
To use an AOSP `releasekey.pk8` key with avbroot, it must be converted from DER to PEM first:
|
||||
|
||||
```bash
|
||||
openssl pkcs8 -topk8 -scrypt -in releasekey.pk8 -out ota.key -outform PEM
|
||||
```
|
||||
|
||||
## Initial setup
|
||||
|
||||
1. Make sure that the version of fastboot is 34 or newer. Older versions have bugs that prevent the `fastboot flashall` command (required later) from working properly.
|
||||
1. Make sure that the version of fastboot is 35 or newer. Older versions have bugs that prevent the `fastboot flashall` command (required later) from working properly.
|
||||
|
||||
```bash
|
||||
fastboot --version
|
||||
@@ -503,6 +520,16 @@ By default, this only extracts the images that could potentially be patched by a
|
||||
|
||||
This command also supports extracting the embedded OTA certificate and AVB public key using the `--cert-ota` and `--public-key-avb` options. To extract only these components, pass in `--none` to skip extracting partition images.
|
||||
|
||||
### Listing partitions in OTA
|
||||
|
||||
To list all partitions in an OTA, run:
|
||||
|
||||
```bash
|
||||
avbroot ota list --input ota.zip
|
||||
```
|
||||
|
||||
The output format is one partition per line with no formatting.
|
||||
|
||||
### Zip write mode
|
||||
|
||||
By default, avbroot uses streaming writes for the output OTA during patching. This means it computes the sha256 digest for the digital signature as the file is being written. This mode causes the zip file to contain data descriptors, which is part of the zip standard and works on the vast majority of devices. However, some devices may have broken zip file parsers and fail to properly read OTA zip files containing data descriptors. If this is the case, pass in `--zip-mode seekable` when patching.
|
||||
@@ -614,4 +641,4 @@ Contributions are welcome! However, I'm unlikely to accept changes for supportin
|
||||
|
||||
## License
|
||||
|
||||
avbroot is licensed under GPLv3. Please see [`LICENSE`](./LICENSE) for the full license text.
|
||||
avbroot is licensed under GPL-3.0-only. Please see [`LICENSE`](./LICENSE) for the full license text.
|
||||
|
||||
+12
-2
@@ -134,7 +134,7 @@ avbroot совместим с любым стандартным 4096-битны
|
||||
|
||||
## Первоначальная настройка
|
||||
|
||||
1. Убедитесь, что вы используете утилиту fastboot версии 34 или новее. Предыдущие версии содержат баги, что не позволяют команде `fastboot flashall` (которая понадобится по ходу инструкции) работать правильно.
|
||||
1. Убедитесь, что вы используете утилиту fastboot версии 35 или новее. Предыдущие версии содержат баги, что не позволяют команде `fastboot flashall` (которая понадобится по ходу инструкции) работать правильно.
|
||||
|
||||
```bash
|
||||
fastboot --version
|
||||
@@ -419,6 +419,16 @@ avbroot поддерживает подмену целых образов в OTA
|
||||
|
||||
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
|
||||
|
||||
### Переподписание разделов
|
||||
|
||||
avbroot автоматически переподписывает все разделы в OTA, которые он изменяет. Однако и те разделы, что не были затронуты, также можно переподписать с помощью параметра `--re-sign <имя раздела>`. Это полезно, например, когда OTA содержит разделы, подписанные публичным тестовым ключом AOSP (test key).
|
||||
|
||||
### Загрузка подписанных GSI
|
||||
|
||||
Функция [динамических обновлений системы (DSU)](https://developer.android.com/topic/dsu?hl=ru) в Android использует корень доверия, отличный от используемого обычной системой. Вместо `avb_custom_key` в загрузчике, она получает доверенные ключи из файлов `first_stage_ramdisk/avb/*.avbpubkey` в ramdisk'е разделов `init_boot` или `vendor_boot`. Эти файлы закодированы в том же двоичном формате, что и файл `avb_pkmd.bin`.
|
||||
|
||||
avbroot может добавить пользовательский публичный ключ AVB в эту директорию с помощью параметра `--dsu` при патчинге OTA. Это позволяет запускать [общие образы системы (GSI)](https://developer.android.com/topic/generic-system-image?hl=ru), подписанные пользовательским ключом AVB.
|
||||
|
||||
### Очистка флагов vbmeta
|
||||
|
||||
Некоторые сборки Android-прошивок могут поставляться с образом `vbmeta`, в котором флаги установлены таким образом, что AVB фактически отключен. Если avbroot сталкивается с такими образом, процесс патчинга завершается ошибкой с сообщением следующего типа:
|
||||
@@ -598,5 +608,5 @@ cargo android build --release --target aarch64-linux-android
|
||||
|
||||
## Лицензия
|
||||
|
||||
avbroot распространяется по лицензии GPLv3. Полный текст лицензии см. в [`LICENSE`.](./LICENSE)
|
||||
avbroot распространяется по лицензии GPL-3.0-only. Полный текст лицензии см. в [`LICENSE`.](./LICENSE)
|
||||
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ dlv-list = "0.6.0"
|
||||
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
lz4_flex = "0.12.0"
|
||||
lz4_flex = "0.13.0"
|
||||
lzma-rust2 = "0.16.1"
|
||||
memchr = "2.6.0"
|
||||
# We can't upgrade to 0.9.0 until rsa updates its dependency.
|
||||
|
||||
+59
-2
@@ -1228,11 +1228,11 @@ fn patch_ota_payload(
|
||||
.with_context(|| format!("Failed to copy from original payload: {name}"))?;
|
||||
}
|
||||
|
||||
let (_, _, properties, metadata_size) = payload_writer
|
||||
let (_, header, properties) = payload_writer
|
||||
.finish()
|
||||
.context("Failed to finalize payload")?;
|
||||
|
||||
Ok((properties, metadata_size))
|
||||
Ok((properties, header.blob_offset))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -2254,11 +2254,58 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_subcommand(cli: &ListCli) -> Result<()> {
|
||||
let raw_reader = File::open(&cli.input)
|
||||
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
|
||||
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
|
||||
let zip = ZipArchive::from_read_at(&raw_reader, &mut buffer)
|
||||
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
|
||||
let mut entries = zip.entries_safe(&mut buffer);
|
||||
|
||||
while let Some((cd_entry, _)) =
|
||||
entries.next_entry().context("Failed to list zip entries")?
|
||||
{
|
||||
let path = cd_entry
|
||||
.file_path_utf8()
|
||||
.context("Zip contains non-UTF-8 paths")?;
|
||||
|
||||
if path != ota::PATH_PAYLOAD {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry_reader = zip
|
||||
.get_entry(cd_entry.wayfinder())
|
||||
.and_then(|e| zip::verifying_reader(&e, cd_entry.compression_method()))
|
||||
.with_context(|| format!("Failed to open zip entry: {path}"))?;
|
||||
let header = PayloadHeader::from_reader(entry_reader)
|
||||
.context("Failed to load OTA payload header")?;
|
||||
|
||||
for partition in header.manifest.partitions {
|
||||
let name = &partition.partition_name;
|
||||
|
||||
util::path_ensure_single(name)?;
|
||||
|
||||
// Ensure the output cannot be misparsed since we use newlines as a
|
||||
// separator.
|
||||
if name.contains('\n') {
|
||||
bail!("Partition name contains newline: {name:?}");
|
||||
}
|
||||
|
||||
println!("{name}");
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bail!("Zip entry not found: {}", ota::PATH_PAYLOAD);
|
||||
}
|
||||
|
||||
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),
|
||||
OtaCommand::Verify(c) => verify_subcommand(c, cancel_signal),
|
||||
OtaCommand::List(c) => list_subcommand(c),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2612,12 +2659,22 @@ pub struct VerifyCli {
|
||||
fail_if_missing: bool,
|
||||
}
|
||||
|
||||
/// List partitions contained in an OTA.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ListCli {
|
||||
/// Path to OTA zip.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
pub input: PathBuf,
|
||||
}
|
||||
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum OtaCommand {
|
||||
Patch(PatchCli),
|
||||
Extract(ExtractCli),
|
||||
Verify(VerifyCli),
|
||||
List(ListCli),
|
||||
}
|
||||
|
||||
/// Patch or extract OTA images.
|
||||
|
||||
+144
-60
@@ -1,11 +1,11 @@
|
||||
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2024-2026 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
ffi::OsString,
|
||||
fs::{self, File},
|
||||
io::{BufReader, BufWriter, Seek, SeekFrom},
|
||||
io::{BufReader, BufWriter, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
@@ -18,31 +18,34 @@ use crate::{
|
||||
cli::ota,
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::payload::{PayloadHeader, PayloadWriter},
|
||||
stream::{self, FromReader},
|
||||
stream::{self, FromReader, SectionReader},
|
||||
util,
|
||||
};
|
||||
|
||||
fn open_reader(path: &Path, allow_delta: bool) -> Result<(BufReader<File>, PayloadHeader)> {
|
||||
let mut reader = File::open(path)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open payload for reading: {path:?}"))?;
|
||||
fn open_raw_reader(path: &Path) -> Result<File> {
|
||||
File::open(path).with_context(|| format!("Failed to open payload for reading: {path:?}"))
|
||||
}
|
||||
|
||||
fn open_reader(path: &Path) -> Result<(BufReader<File>, PayloadHeader)> {
|
||||
let mut reader = open_raw_reader(path).map(BufReader::new)?;
|
||||
let header = PayloadHeader::from_reader(&mut reader)
|
||||
.with_context(|| format!("Failed to read payload header: {path:?}"))?;
|
||||
if !allow_delta && !header.is_full_ota() {
|
||||
bail!("Payload is a delta OTA, not a full OTA");
|
||||
}
|
||||
|
||||
Ok((reader, header))
|
||||
}
|
||||
|
||||
fn open_raw_writer(path: &Path) -> Result<BufWriter<File>> {
|
||||
File::create(path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open payload for writing: {path:?}"))
|
||||
}
|
||||
|
||||
fn open_writer(
|
||||
path: &Path,
|
||||
header: PayloadHeader,
|
||||
key: RsaSigningKey,
|
||||
) -> Result<PayloadWriter<BufWriter<File>>> {
|
||||
let writer = File::create(path)
|
||||
.map(BufWriter::new)
|
||||
.with_context(|| format!("Failed to open payload for writing: {path:?}"))?;
|
||||
let writer = open_raw_writer(path)?;
|
||||
let payload_writer = PayloadWriter::new(writer, header, key)
|
||||
.with_context(|| format!("Failed to write payload header: {path:?}"))?;
|
||||
|
||||
@@ -67,8 +70,8 @@ fn write_info(path: &Path, manifest: &PayloadHeader) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_header(cli: &PayloadCli, header: &PayloadHeader) {
|
||||
if !cli.quiet {
|
||||
fn display_header(quiet: bool, header: &PayloadHeader) {
|
||||
if !quiet {
|
||||
println!("{header:#?}");
|
||||
}
|
||||
}
|
||||
@@ -99,52 +102,105 @@ fn load_key(group: &KeyGroup) -> Result<RsaSigningKey> {
|
||||
Ok(signing_key)
|
||||
}
|
||||
|
||||
fn unpack_subcommand(
|
||||
payload_cli: &PayloadCli,
|
||||
cli: &UnpackCli,
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn unpack_payload(
|
||||
quiet: bool,
|
||||
output_info: &Path,
|
||||
output_images: &Path,
|
||||
no_output_images: bool,
|
||||
skeleton: bool,
|
||||
reader: &File,
|
||||
payload_offset: u64,
|
||||
payload_size: u64,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let (mut reader, header) = open_reader(&cli.input, false)?;
|
||||
let payload_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
|
||||
let section_reader = SectionReader::new(reader, payload_offset, payload_size)
|
||||
.context("Failed to directly open payload section")?;
|
||||
let header =
|
||||
PayloadHeader::from_reader(section_reader).context("Failed to read payload header")?;
|
||||
|
||||
display_header(payload_cli, &header);
|
||||
display_header(quiet, &header);
|
||||
|
||||
write_info(&cli.output_info, &header)?;
|
||||
write_info(output_info, &header)?;
|
||||
|
||||
if !cli.no_output_images {
|
||||
fs::create_dir_all(&cli.output_images)
|
||||
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
|
||||
if !no_output_images {
|
||||
fs::create_dir_all(output_images)
|
||||
.with_context(|| format!("Failed to create directory: {output_images:?}"))?;
|
||||
|
||||
ota::extract_payload(
|
||||
&reader.into_inner(),
|
||||
&cli.output_images,
|
||||
0,
|
||||
payload_size,
|
||||
&header,
|
||||
&header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| &p.partition_name)
|
||||
.cloned()
|
||||
.collect(),
|
||||
cancel_signal,
|
||||
)?;
|
||||
if skeleton {
|
||||
for partition in &header.manifest.partitions {
|
||||
let name = &partition.partition_name;
|
||||
let path = util::path_join_single(output_images, format!("{name}.img"))?;
|
||||
let size = partition
|
||||
.new_partition_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.size)
|
||||
.ok_or_else(|| anyhow!("Size not found for partition: {name}"))?;
|
||||
|
||||
let file = File::create(&path)
|
||||
.with_context(|| format!("Failed to create file: {path:?}"))?;
|
||||
|
||||
file.set_len(size)
|
||||
.with_context(|| format!("Failed to truncate file: {path:?}"))?;
|
||||
}
|
||||
} else {
|
||||
if !header.is_full_ota() {
|
||||
bail!("Cannot extract images from a delta payload");
|
||||
}
|
||||
|
||||
ota::extract_payload(
|
||||
reader,
|
||||
output_images,
|
||||
payload_offset,
|
||||
payload_size,
|
||||
&header,
|
||||
&header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| &p.partition_name)
|
||||
.cloned()
|
||||
.collect(),
|
||||
cancel_signal,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pack_subcommand(
|
||||
fn unpack_subcommand(
|
||||
payload_cli: &PayloadCli,
|
||||
cli: &PackCli,
|
||||
cli: &UnpackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let signing_key = load_key(&cli.key)?;
|
||||
let mut reader = open_raw_reader(&cli.input)?;
|
||||
let payload_size = reader
|
||||
.seek(SeekFrom::End(0))
|
||||
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
|
||||
|
||||
let mut header = read_info(&cli.input_info)?;
|
||||
unpack_payload(
|
||||
payload_cli.quiet,
|
||||
&cli.output_info,
|
||||
&cli.output_images,
|
||||
cli.no_output_images,
|
||||
cli.skeleton,
|
||||
&reader,
|
||||
0,
|
||||
payload_size,
|
||||
cancel_signal,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn pack_payload(
|
||||
quiet: bool,
|
||||
input_info: &Path,
|
||||
input_images: &Path,
|
||||
writer: &mut dyn Write,
|
||||
signing_key: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(String, u64)> {
|
||||
let mut header = read_info(input_info)?;
|
||||
|
||||
// Pre-open all of the image files.
|
||||
let input_files = header
|
||||
@@ -152,8 +208,7 @@ fn pack_subcommand(
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let path =
|
||||
util::path_join_single(&cli.input_images, format!("{}.img", p.partition_name))?;
|
||||
let path = util::path_join_single(input_images, format!("{}.img", p.partition_name))?;
|
||||
let file = File::open(&path)
|
||||
.map(Arc::new)
|
||||
.with_context(|| format!("Failed to open file: {path:?}"))?;
|
||||
@@ -164,8 +219,11 @@ fn pack_subcommand(
|
||||
|
||||
for (name, input_file) in &input_files {
|
||||
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
|
||||
continue;
|
||||
break;
|
||||
};
|
||||
if !dpm.vabc_enabled() {
|
||||
break;
|
||||
}
|
||||
|
||||
if !dpm.groups.iter().any(|g| g.partition_names.contains(name)) {
|
||||
continue;
|
||||
@@ -199,7 +257,8 @@ fn pack_subcommand(
|
||||
|
||||
// Now we can write the actual payload. With everything precomputed, this is
|
||||
// mostly just a simple copy.
|
||||
let mut payload_writer = open_writer(&cli.output, header.clone(), signing_key)?;
|
||||
let mut payload_writer = PayloadWriter::new(writer, header.clone(), signing_key.clone())
|
||||
.context("Failed to write payload header")?;
|
||||
|
||||
while payload_writer
|
||||
.begin_next_operation()
|
||||
@@ -235,14 +294,35 @@ fn pack_subcommand(
|
||||
.with_context(|| format!("Failed to copy from replacement image: {name}"))?;
|
||||
}
|
||||
|
||||
let (raw_writer, header, properties, _) = payload_writer
|
||||
let (_, header, properties) = payload_writer
|
||||
.finish()
|
||||
.context("Failed to finalize payload")?;
|
||||
|
||||
raw_writer.into_inner().context("Failed to flush payload")?;
|
||||
|
||||
// Display the header information now that it has been finalized.
|
||||
display_header(payload_cli, &header);
|
||||
display_header(quiet, &header);
|
||||
|
||||
Ok((properties, header.blob_offset))
|
||||
}
|
||||
|
||||
fn pack_subcommand(
|
||||
payload_cli: &PayloadCli,
|
||||
cli: &PackCli,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let signing_key = load_key(&cli.key)?;
|
||||
|
||||
let mut writer = open_raw_writer(&cli.output)?;
|
||||
|
||||
let (properties, _) = pack_payload(
|
||||
payload_cli.quiet,
|
||||
&cli.input_info,
|
||||
&cli.input_images,
|
||||
&mut writer,
|
||||
&signing_key,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
writer.into_inner().context("Failed to flush payload")?;
|
||||
|
||||
// Optionally, write payload_properties.txt.
|
||||
if let Some(path) = &cli.output_properties {
|
||||
@@ -260,7 +340,7 @@ fn repack_subcommand(
|
||||
) -> Result<()> {
|
||||
let signing_key = load_key(&cli.key)?;
|
||||
|
||||
let (mut reader, header) = open_reader(&cli.input, true)?;
|
||||
let (mut reader, header) = open_reader(&cli.input)?;
|
||||
|
||||
info!("Generating new OTA payload");
|
||||
|
||||
@@ -299,12 +379,12 @@ fn repack_subcommand(
|
||||
.with_context(|| format!("Failed to copy from original payload: {name}"))?;
|
||||
}
|
||||
|
||||
let (_, header, properties, _) = payload_writer
|
||||
let (_, header, properties) = payload_writer
|
||||
.finish()
|
||||
.context("Failed to finalize payload")?;
|
||||
|
||||
// Display the header information now that it has been finalized.
|
||||
display_header(payload_cli, &header);
|
||||
display_header(payload_cli.quiet, &header);
|
||||
|
||||
// Optionally, write payload_properties.txt.
|
||||
if let Some(path) = &cli.output_properties {
|
||||
@@ -316,9 +396,9 @@ fn repack_subcommand(
|
||||
}
|
||||
|
||||
fn info_subcommand(payload_cli: &PayloadCli, cli: &InfoCli) -> Result<()> {
|
||||
let (_, header) = open_reader(&cli.input, true)?;
|
||||
let (_, header) = open_reader(&cli.input)?;
|
||||
|
||||
display_header(payload_cli, &header);
|
||||
display_header(payload_cli.quiet, &header);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -391,8 +471,12 @@ struct UnpackCli {
|
||||
output_images: PathBuf,
|
||||
|
||||
/// Do not output images.
|
||||
#[arg(long, conflicts_with = "output_images")]
|
||||
#[arg(long, conflicts_with_all = ["output_images", "skeleton"])]
|
||||
no_output_images: bool,
|
||||
|
||||
/// Only create empty sparse files for output images.
|
||||
#[arg(long)]
|
||||
skeleton: bool,
|
||||
}
|
||||
|
||||
/// Pack a payload binary.
|
||||
|
||||
+151
-29
@@ -5,7 +5,7 @@ use std::{
|
||||
collections::BTreeMap,
|
||||
ffi::OsString,
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{BufReader, Seek},
|
||||
io::{BufReader, Seek, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
@@ -21,6 +21,7 @@ use tracing::warn;
|
||||
use x509_cert::Certificate;
|
||||
|
||||
use crate::{
|
||||
cli::payload,
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
format::{
|
||||
ota::{self, SigningWriter, ZipEntry, ZipMode},
|
||||
@@ -137,8 +138,8 @@ fn write_info(path: &Path, info: &OtaInfo) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_info(cli: &ZipCli, info: &OtaInfo) {
|
||||
if !cli.quiet {
|
||||
fn display_info(quiet: bool, info: &OtaInfo) {
|
||||
if !quiet {
|
||||
println!("{info:#?}");
|
||||
}
|
||||
}
|
||||
@@ -285,11 +286,33 @@ fn finalize_ota(
|
||||
fn unpack_subcommand(zip_cli: &ZipCli, cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let (zip_reader, metadata, input_entries) = open_reader(&cli.input)?;
|
||||
|
||||
if !cli.no_output_files {
|
||||
for (path, input_entry) in &input_entries {
|
||||
let entry = zip_reader
|
||||
.get_entry(input_entry.wayfinder)
|
||||
.with_context(|| format!("Failed to open zip entry: {path}"))?;
|
||||
for (path, input_entry) in &input_entries {
|
||||
let entry = zip_reader
|
||||
.get_entry(input_entry.wayfinder)
|
||||
.with_context(|| format!("Failed to open zip entry: {path}"))?;
|
||||
|
||||
if cli.payload && path == ota::PATH_PAYLOAD {
|
||||
let (payload_offset, payload_size) = {
|
||||
let range = entry.compressed_data_range();
|
||||
|
||||
(range.0, range.1 - range.0)
|
||||
};
|
||||
|
||||
payload::unpack_payload(
|
||||
zip_cli.quiet,
|
||||
&cli.output_payload_info,
|
||||
&cli.output_payload_images,
|
||||
cli.no_output_payload_images,
|
||||
cli.payload_skeleton,
|
||||
&zip_reader.get_ref().0,
|
||||
payload_offset,
|
||||
payload_size,
|
||||
cancel_signal,
|
||||
)?;
|
||||
} else if cli.payload && path == ota::PATH_PROPERTIES {
|
||||
// We intentionally ignore this since it's recomputed when
|
||||
// packing with --payload.
|
||||
} else if !cli.no_output_files {
|
||||
let mut entry_reader = zip::verifying_reader(&entry, input_entry.compression_method)
|
||||
.with_context(|| format!("Failed to open zip entry: {path}"))?;
|
||||
|
||||
@@ -312,7 +335,7 @@ fn unpack_subcommand(zip_cli: &ZipCli, cli: &UnpackCli, cancel_signal: &AtomicBo
|
||||
files: input_entries.into_keys().collect(),
|
||||
};
|
||||
|
||||
display_info(zip_cli, &info);
|
||||
display_info(zip_cli.quiet, &info);
|
||||
write_info(&cli.output_info, &info)?;
|
||||
|
||||
Ok(())
|
||||
@@ -331,10 +354,15 @@ fn pack_subcommand(zip_cli: &ZipCli, cli: &PackCli, cancel_signal: &AtomicBool)
|
||||
continue;
|
||||
}
|
||||
|
||||
let input_path = util::path_join(&cli.input_files, path)?;
|
||||
let input_size = fs::metadata(&input_path)
|
||||
.map(|m| m.len())
|
||||
.with_context(|| format!("Failed to get file size: {input_path:?}"))?;
|
||||
let input_size =
|
||||
if cli.payload && (path == ota::PATH_PAYLOAD || path == ota::PATH_PROPERTIES) {
|
||||
0
|
||||
} else {
|
||||
let input_path = util::path_join(&cli.input_files, path)?;
|
||||
fs::metadata(&input_path)
|
||||
.map(|m| m.len())
|
||||
.with_context(|| format!("Failed to get file size: {input_path:?}"))?
|
||||
};
|
||||
|
||||
input_entries.insert(path, input_size >= 0xffffffff);
|
||||
}
|
||||
@@ -342,28 +370,56 @@ fn pack_subcommand(zip_cli: &ZipCli, cli: &PackCli, cancel_signal: &AtomicBool)
|
||||
let mut payload_metadata_size = None;
|
||||
let mut metadata_entries = vec![];
|
||||
|
||||
// Only used when packing the zip and payload at the same time.
|
||||
let mut payload_properties = None;
|
||||
|
||||
for (&path, &is_zip64) in &input_entries {
|
||||
let (offset, mut data_writer) =
|
||||
start_entry(&mut zip_writer, path, cli.zip_mode.zip_mode, is_zip64)?;
|
||||
|
||||
let input_path = util::path_join(&cli.input_files, path)?;
|
||||
let mut input_file = File::open(&input_path)
|
||||
.with_context(|| format!("Failed to open for reading: {input_path:?}"))?;
|
||||
if path == ota::PATH_PAYLOAD && cli.payload {
|
||||
// Pack the payload directly into the zip.
|
||||
let (properties, metadata_size) = payload::pack_payload(
|
||||
zip_cli.quiet,
|
||||
&cli.input_payload_info,
|
||||
&cli.input_payload_images,
|
||||
&mut data_writer,
|
||||
&signing_key,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
if path == ota::PATH_PAYLOAD {
|
||||
let header = PayloadHeader::from_reader(&mut input_file)
|
||||
.with_context(|| format!("Failed to read payload header: {input_path:?}"))?;
|
||||
payload_metadata_size = Some(metadata_size);
|
||||
payload_properties = Some(properties);
|
||||
} else if path == ota::PATH_PROPERTIES && cli.payload {
|
||||
// We process files in order, so this would only happen if the file
|
||||
// list did not contain the payload.
|
||||
let properties = payload_properties
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Payload has not been processed yet"))?;
|
||||
|
||||
payload_metadata_size = Some(header.blob_offset);
|
||||
data_writer
|
||||
.write_all(properties.as_bytes())
|
||||
.with_context(|| format!("Failed to write zip entry: {path}"))?;
|
||||
} else {
|
||||
let input_path = util::path_join(&cli.input_files, path)?;
|
||||
let mut input_file = File::open(&input_path)
|
||||
.with_context(|| format!("Failed to open for reading: {input_path:?}"))?;
|
||||
|
||||
input_file
|
||||
.rewind()
|
||||
.with_context(|| format!("Failed to seek file: {input_path:?}"))?;
|
||||
if path == ota::PATH_PAYLOAD {
|
||||
let header = PayloadHeader::from_reader(&mut input_file)
|
||||
.with_context(|| format!("Failed to read payload header: {input_path:?}"))?;
|
||||
|
||||
payload_metadata_size = Some(header.blob_offset);
|
||||
|
||||
input_file
|
||||
.rewind()
|
||||
.with_context(|| format!("Failed to seek file: {input_path:?}"))?;
|
||||
}
|
||||
|
||||
stream::copy(&mut input_file, &mut data_writer, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy zip entry: {path}"))?;
|
||||
}
|
||||
|
||||
stream::copy(&mut input_file, &mut data_writer, cancel_signal)
|
||||
.with_context(|| format!("Failed to copy zip entry: {path}"))?;
|
||||
|
||||
finalize_entry(path, offset, data_writer, &mut metadata_entries)?;
|
||||
}
|
||||
|
||||
@@ -382,7 +438,7 @@ fn pack_subcommand(zip_cli: &ZipCli, cli: &PackCli, cancel_signal: &AtomicBool)
|
||||
info.files.retain(|p| !is_excluded_path(p));
|
||||
info.files.sort();
|
||||
|
||||
display_info(zip_cli, &info);
|
||||
display_info(zip_cli.quiet, &info);
|
||||
|
||||
if let Some(path) = &cli.output_info {
|
||||
write_info(path, &info)?;
|
||||
@@ -446,7 +502,7 @@ fn repack_subcommand(zip_cli: &ZipCli, cli: &RepackCli, cancel_signal: &AtomicBo
|
||||
files: input_entries.into_keys().collect(),
|
||||
};
|
||||
|
||||
display_info(zip_cli, &info);
|
||||
display_info(zip_cli.quiet, &info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -458,7 +514,7 @@ fn info_subcommand(zip_cli: &ZipCli, cli: &InfoCli) -> Result<()> {
|
||||
files: input_entries.into_keys().collect(),
|
||||
};
|
||||
|
||||
display_info(zip_cli, &info);
|
||||
display_info(zip_cli.quiet, &info);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -541,6 +597,45 @@ struct UnpackCli {
|
||||
/// Do not output files.
|
||||
#[arg(long, conflicts_with = "output_files")]
|
||||
no_output_files: bool,
|
||||
|
||||
/// Also unpack the payload in the OTA.
|
||||
///
|
||||
/// Unpacking the zip and payload at the same time is more efficient than
|
||||
/// unpacking them separately.
|
||||
#[arg(long)]
|
||||
payload: bool,
|
||||
|
||||
/// Path to output payload info TOML.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "payload.toml",
|
||||
requires = "payload"
|
||||
)]
|
||||
output_payload_info: PathBuf,
|
||||
|
||||
/// Path to output payload images directory.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DIR",
|
||||
value_parser,
|
||||
default_value = "payload_images",
|
||||
requires = "payload"
|
||||
)]
|
||||
output_payload_images: PathBuf,
|
||||
|
||||
/// Do not output payload images.
|
||||
#[arg(
|
||||
long,
|
||||
conflicts_with_all = ["output_payload_images", "payload_skeleton"],
|
||||
requires = "payload"
|
||||
)]
|
||||
no_output_payload_images: bool,
|
||||
|
||||
/// Only create empty sparse files for output payload images.
|
||||
#[arg(long, requires = "payload")]
|
||||
payload_skeleton: bool,
|
||||
}
|
||||
|
||||
/// Pack an OTA zip.
|
||||
@@ -573,6 +668,33 @@ struct PackCli {
|
||||
#[arg(long, value_name = "DIR", value_parser, default_value = "ota_files")]
|
||||
input_files: PathBuf,
|
||||
|
||||
/// Also pack the payload in the OTA.
|
||||
///
|
||||
/// Packing the zip and payload at the same time is more efficient than
|
||||
/// packing them separately.
|
||||
#[arg(long)]
|
||||
payload: bool,
|
||||
|
||||
/// Path to input payload info TOML.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "FILE",
|
||||
value_parser,
|
||||
default_value = "payload.toml",
|
||||
requires = "payload"
|
||||
)]
|
||||
input_payload_info: PathBuf,
|
||||
|
||||
/// Path to input payload images directory.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DIR",
|
||||
value_parser,
|
||||
default_value = "payload_images",
|
||||
requires = "payload"
|
||||
)]
|
||||
input_payload_images: PathBuf,
|
||||
|
||||
#[command(flatten)]
|
||||
key: KeyGroup,
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2026 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
@@ -9,7 +9,7 @@ use flate2::{
|
||||
write::{DeflateEncoder, GzEncoder},
|
||||
};
|
||||
use lz4_flex::frame::FrameDecoder;
|
||||
use lzma_rust2::{CheckType, XzOptions, XzReader, XzWriter};
|
||||
use lzma_rust2::{CheckType, LzmaOptions, LzmaReader, LzmaWriter, XzOptions, XzReader, XzWriter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -18,6 +18,14 @@ use crate::stream::ReadFixedSizeExt;
|
||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
||||
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
// This is not actually a magic value, but is good enough. The first byte
|
||||
// encodes (lc=3, lp=0, pb=2) and the next two bytes are two of the U32LE bytes
|
||||
// encoding the dictionary size. If this heuristic is good enough for libmagic,
|
||||
// then it's good enough for us too.
|
||||
// https://nigeltao.github.io/blog/2024/xz-lzma-part-5-xz.html
|
||||
static LZMA_MAGIC: &[u8; 3] = b"\x5d\x00\x00";
|
||||
|
||||
const LZMA_MAX_MEMORY_KIB: u32 = 16 * 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
@@ -26,9 +34,13 @@ pub enum Error {
|
||||
#[error("I/O error when autodetecting compression format")]
|
||||
AutoDetect(#[source] io::Error),
|
||||
#[error("Failed to initialize legacy LZ4 encoder")]
|
||||
Lz4Init(#[source] io::Error),
|
||||
Lz4EncodeInit(#[source] io::Error),
|
||||
#[error("Failed to initialize XZ encoder")]
|
||||
XzInit(#[source] io::Error),
|
||||
XzEncodeInit(#[source] io::Error),
|
||||
#[error("Failed to initialize LZMA decoder")]
|
||||
LzmaDecodeInit(#[source] io::Error),
|
||||
#[error("Failed to initialize LZMA encoder")]
|
||||
LzmaEncodeInit(#[source] io::Error),
|
||||
}
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -113,6 +125,7 @@ pub enum CompressedFormat {
|
||||
Gzip,
|
||||
Lz4Legacy,
|
||||
Xz,
|
||||
Lzma,
|
||||
}
|
||||
|
||||
pub enum CompressedReader<R: Read> {
|
||||
@@ -123,16 +136,21 @@ pub enum CompressedReader<R: Read> {
|
||||
Lz4(FrameDecoder<R>),
|
||||
/// Boxed because the [`XzReader`] is nearly 4 KiB.
|
||||
Xz(Box<XzReader<R>>),
|
||||
Lzma(Box<LzmaReader<R>>),
|
||||
}
|
||||
|
||||
impl<R: Read> CompressedReader<R> {
|
||||
pub fn with_format(reader: R, format: CompressedFormat) -> Self {
|
||||
pub fn with_format(reader: R, format: CompressedFormat) -> Result<Self> {
|
||||
match format {
|
||||
CompressedFormat::None => Self::None(reader),
|
||||
CompressedFormat::Deflate => Self::Deflate(DeflateDecoder::new(reader)),
|
||||
CompressedFormat::Gzip => Self::Gzip(GzDecoder::new(reader)),
|
||||
CompressedFormat::Lz4Legacy => Self::Lz4(FrameDecoder::new(reader)),
|
||||
CompressedFormat::Xz => Self::Xz(Box::new(XzReader::new(reader, false))),
|
||||
CompressedFormat::None => Ok(Self::None(reader)),
|
||||
CompressedFormat::Deflate => Ok(Self::Deflate(DeflateDecoder::new(reader))),
|
||||
CompressedFormat::Gzip => Ok(Self::Gzip(GzDecoder::new(reader))),
|
||||
CompressedFormat::Lz4Legacy => Ok(Self::Lz4(FrameDecoder::new(reader))),
|
||||
CompressedFormat::Xz => Ok(Self::Xz(Box::new(XzReader::new(reader, false)))),
|
||||
CompressedFormat::Lzma => Ok(Self::Lzma(Box::new(
|
||||
LzmaReader::new_mem_limit(reader, LZMA_MAX_MEMORY_KIB, None)
|
||||
.map_err(Error::LzmaDecodeInit)?,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +161,7 @@ impl<R: Read> CompressedReader<R> {
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
Self::Lzma(_) => CompressedFormat::Lzma,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +172,7 @@ impl<R: Read> CompressedReader<R> {
|
||||
Self::Gzip(r) => r.into_inner(),
|
||||
Self::Lz4(r) => r.into_inner(),
|
||||
Self::Xz(r) => r.into_inner(),
|
||||
Self::Lzma(r) => r.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,17 +183,21 @@ impl<R: Read + Seek> CompressedReader<R> {
|
||||
|
||||
reader.rewind().map_err(Error::AutoDetect)?;
|
||||
|
||||
if &magic[0..2] == GZIP_MAGIC {
|
||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||
let format = if &magic[0..2] == GZIP_MAGIC {
|
||||
CompressedFormat::Gzip
|
||||
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
|
||||
Ok(Self::Lz4(FrameDecoder::new(reader)))
|
||||
CompressedFormat::Lz4Legacy
|
||||
} else if &magic == XZ_MAGIC {
|
||||
Ok(Self::Xz(Box::new(XzReader::new(reader, false))))
|
||||
CompressedFormat::Xz
|
||||
} else if &magic[0..3] == LZMA_MAGIC {
|
||||
CompressedFormat::Lzma
|
||||
} else if raw_if_unknown {
|
||||
Ok(Self::None(reader))
|
||||
CompressedFormat::None
|
||||
} else {
|
||||
Err(Error::UnknownFormat)
|
||||
}
|
||||
return Err(Error::UnknownFormat);
|
||||
};
|
||||
|
||||
Self::with_format(reader, format)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +209,7 @@ impl<R: Read> Read for CompressedReader<R> {
|
||||
Self::Gzip(r) => r.read(buf),
|
||||
Self::Lz4(r) => r.read(buf),
|
||||
Self::Xz(r) => r.read(buf),
|
||||
Self::Lzma(r) => r.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,7 +220,8 @@ pub enum CompressedWriter<W: Write> {
|
||||
Deflate(DeflateEncoder<W>),
|
||||
Gzip(GzEncoder<W>),
|
||||
Lz4Legacy(Lz4LegacyEncoder<W>),
|
||||
Xz(XzWriter<W>),
|
||||
Xz(Box<XzWriter<W>>),
|
||||
Lzma(Box<LzmaWriter<W>>),
|
||||
}
|
||||
|
||||
impl<W: Write> CompressedWriter<W> {
|
||||
@@ -210,7 +236,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||
}
|
||||
CompressedFormat::Lz4Legacy => {
|
||||
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4Init)?;
|
||||
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4EncodeInit)?;
|
||||
Ok(Self::Lz4Legacy(encoder))
|
||||
}
|
||||
CompressedFormat::Xz => {
|
||||
@@ -218,8 +244,17 @@ impl<W: Write> CompressedWriter<W> {
|
||||
let mut options = XzOptions::with_preset(6);
|
||||
options.set_check_sum_type(CheckType::Crc32);
|
||||
|
||||
let xz_writer = XzWriter::new(writer, options).map_err(Error::XzInit)?;
|
||||
Ok(Self::Xz(xz_writer))
|
||||
let xz_writer = XzWriter::new(writer, options).map_err(Error::XzEncodeInit)?;
|
||||
Ok(Self::Xz(Box::new(xz_writer)))
|
||||
}
|
||||
CompressedFormat::Lzma => {
|
||||
// Defaults are already (lc=3, lp=0, pb=2) and has preset 6.
|
||||
let options = LzmaOptions::default();
|
||||
|
||||
// We only support streaming output.
|
||||
let lzma_writer = LzmaWriter::new_use_header(writer, &options, None)
|
||||
.map_err(Error::LzmaEncodeInit)?;
|
||||
Ok(Self::Lzma(Box::new(lzma_writer)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,6 +266,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
Self::Lzma(_) => CompressedFormat::Lzma,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +277,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Self::Gzip(w) => w.finish(),
|
||||
Self::Lz4Legacy(w) => w.finish(),
|
||||
Self::Xz(w) => w.finish(),
|
||||
Self::Lzma(w) => w.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,6 +290,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
Self::Gzip(w) => w.write(buf),
|
||||
Self::Lz4Legacy(w) => w.write(buf),
|
||||
Self::Xz(w) => w.write(buf),
|
||||
Self::Lzma(w) => w.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +301,79 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
Self::Gzip(w) => w.flush(),
|
||||
Self::Lz4Legacy(w) => w.flush(),
|
||||
Self::Xz(w) => w.flush(),
|
||||
Self::Lzma(w) => w.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Cursor;
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn round_trip(format: CompressedFormat, can_autodetect: bool) {
|
||||
let mut data = vec![0u8; 1024 * 1024];
|
||||
rand::thread_rng().fill(data.as_mut_slice());
|
||||
|
||||
let mut writer = CompressedWriter::new(Cursor::new(Vec::new()), format).unwrap();
|
||||
writer.write_all(&data).unwrap();
|
||||
|
||||
let mut compressed = writer.finish().unwrap();
|
||||
compressed.rewind().unwrap();
|
||||
|
||||
let mut reader = if can_autodetect {
|
||||
CompressedReader::new(compressed, true).unwrap()
|
||||
} else {
|
||||
CompressedReader::with_format(compressed, format).unwrap()
|
||||
};
|
||||
assert_eq!(reader.format(), format);
|
||||
|
||||
let mut buf = [0u8; 8192];
|
||||
let mut total_read = 0;
|
||||
|
||||
loop {
|
||||
let n = reader.read(&mut buf).unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
assert_eq!(&buf[..n], &data[total_read..][..n]);
|
||||
total_read += n;
|
||||
}
|
||||
|
||||
assert_eq!(total_read, data.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_none() {
|
||||
round_trip(CompressedFormat::None, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_deflate() {
|
||||
round_trip(CompressedFormat::Deflate, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_gzip() {
|
||||
round_trip(CompressedFormat::Gzip, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_lz4_legacy() {
|
||||
round_trip(CompressedFormat::Lz4Legacy, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_xz() {
|
||||
round_trip(CompressedFormat::Xz, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_lzma() {
|
||||
round_trip(CompressedFormat::Lzma, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2022-2026 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -45,7 +45,7 @@ use crate::{
|
||||
const PAYLOAD_MAGIC: &[u8; 4] = b"CrAU";
|
||||
const PAYLOAD_VERSION: u64 = 2;
|
||||
|
||||
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
|
||||
const MANIFEST_MAX_SIZE: usize = 16 * 1024 * 1024;
|
||||
|
||||
/// Size of each extent. This matches what AOSP's delta_generator does. We also
|
||||
/// require this to be a multiple of the block size and a multiple of the
|
||||
@@ -436,11 +436,11 @@ impl<W: Write> PayloadWriter<W> {
|
||||
|
||||
/// Finalize the payload. If this function is not called, the payload will
|
||||
/// be left in an incomplete state. Returns the original writer, the final
|
||||
/// header, the contents that should be written for `payload_properties.txt`
|
||||
/// and the length of the header + manifest + manifest signature sections
|
||||
/// (for constructing the `payload_metadata.bin` OTA metadata property files
|
||||
/// entry).
|
||||
pub fn finish(mut self) -> Result<(W, PayloadHeader, String, u64)> {
|
||||
/// header and the data that should be written for `payload_properties.txt`.
|
||||
/// [`PayloadHeader::blob_offset`] indicates the length of the header +
|
||||
/// manifest + manifest signature sections (for constructing the
|
||||
/// `payload_metadata.bin` OTA metadata property files entry).
|
||||
pub fn finish(mut self) -> Result<(W, PayloadHeader, String)> {
|
||||
// Append payload signature.
|
||||
let payload_partial_hash = self.h_partial.clone().finish();
|
||||
let payload_sig = sign_digest(payload_partial_hash.as_ref(), &self.key)?;
|
||||
@@ -465,7 +465,9 @@ impl<W: Write> PayloadWriter<W> {
|
||||
self.metadata_size as u64,
|
||||
);
|
||||
|
||||
Ok((self.inner, self.header, properties, metadata_with_sig_size))
|
||||
self.header.blob_offset = metadata_with_sig_size;
|
||||
|
||||
Ok((self.inner, self.header, properties))
|
||||
}
|
||||
|
||||
/// Prepare for writing the next source data blob corresponding to an
|
||||
@@ -1457,7 +1459,7 @@ pub fn compress_image(
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
.map_err(Error::ChunkWrite)?;
|
||||
|
||||
operations.extend(group_operations.into_iter());
|
||||
operations.extend(group_operations);
|
||||
}
|
||||
|
||||
let digest_uncompressed = context_uncompressed.finish();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2025 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2025-2026 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -249,7 +249,8 @@ pub fn compressed_reader<'archive, R: ReaderAt>(
|
||||
) -> Result<CompressedReader<ZipReader<&'archive R>>, rawzip::Error> {
|
||||
let format = compression_method_to_format(compression_method)?;
|
||||
|
||||
Ok(CompressedReader::with_format(entry.reader(), format))
|
||||
CompressedReader::with_format(entry.reader(), format)
|
||||
.map_err(|e| rawzip::ErrorKind::InvalidInput { msg: e.to_string() }.into())
|
||||
}
|
||||
|
||||
pub fn compressed_slice_reader<'archive>(
|
||||
@@ -259,7 +260,8 @@ pub fn compressed_slice_reader<'archive>(
|
||||
let format = compression_method_to_format(compression_method)?;
|
||||
let raw_reader = Cursor::new(entry.data());
|
||||
|
||||
Ok(CompressedReader::with_format(raw_reader, format))
|
||||
CompressedReader::with_format(raw_reader, format)
|
||||
.map_err(|e| rawzip::ErrorKind::InvalidInput { msg: e.to_string() }.into())
|
||||
}
|
||||
|
||||
pub fn verifying_reader<'archive, R: ReaderAt>(
|
||||
@@ -286,7 +288,12 @@ pub fn compressed_writer<W: Write>(
|
||||
|
||||
match CompressedWriter::new(writer, format) {
|
||||
Ok(w) => Ok(w),
|
||||
Err(Error::Lz4Init(e) | Error::XzInit(e)) => Err(e.into()),
|
||||
Err(
|
||||
Error::Lz4EncodeInit(e)
|
||||
| Error::XzEncodeInit(e)
|
||||
| Error::LzmaDecodeInit(e)
|
||||
| Error::LzmaEncodeInit(e),
|
||||
) => Err(e.into()),
|
||||
Err(Error::UnknownFormat | Error::AutoDetect(_)) => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -306,7 +313,7 @@ pub trait ZipArchiveReadAtExt {
|
||||
|
||||
impl ZipArchiveReadAtExt for ZipArchive<()> {}
|
||||
|
||||
pub struct ReaderAtWrapper<R: ReadAt>(R);
|
||||
pub struct ReaderAtWrapper<R: ReadAt>(pub R);
|
||||
|
||||
impl<R: ReadAt> ReaderAt for ReaderAtWrapper<R> {
|
||||
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
|
||||
@@ -32,7 +32,7 @@ use crate::{
|
||||
avb::{self, AppendedDescriptorMut, Footer, Header},
|
||||
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
|
||||
compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
|
||||
cpio::{self, CpioEntry, CpioEntryData},
|
||||
cpio::{self, CpioEntry, CpioEntryData, CpioEntryType},
|
||||
zip::{self, ZipEntriesSafeExt, ZipFileHeaderRecordExt, ZipSliceEntriesSafeExt},
|
||||
},
|
||||
patch::otacert::{self, OtaCertBuildFlags},
|
||||
@@ -211,7 +211,7 @@ impl MagiskRootPatcher {
|
||||
// replaced by PREINITDEVICE
|
||||
// - Versions newer than the latest supported version are assumed to support
|
||||
// the same features as the latest version
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..30700];
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..30800];
|
||||
const VER_PREINIT_DEVICE: RangeFrom<u32> = 25211..;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
const VER_PATCH_VBMETA: Range<u32> = Self::VERS_SUPPORTED[0].start..26202;
|
||||
@@ -407,7 +407,10 @@ impl MagiskRootPatcher {
|
||||
let mut new_path = b".backup/".to_vec();
|
||||
new_path.extend(&old_entry.path);
|
||||
|
||||
let new_data = if xz_compress && let CpioEntryData::Data(data) = &old_entry.data {
|
||||
let new_data = if xz_compress
|
||||
&& old_entry.file_type == CpioEntryType::Regular
|
||||
&& let CpioEntryData::Data(data) = &old_entry.data
|
||||
{
|
||||
new_path.extend(b".xz");
|
||||
|
||||
let reader = Cursor::new(data);
|
||||
@@ -1415,7 +1418,7 @@ pub fn patch_boot_images<'a>(
|
||||
|
||||
// Regroup data so we can parallelize by target.
|
||||
let mut groups = HashMap::<&str, (BootImageInfo, Vec<&Box<dyn BootImagePatch + Sync>>)>::new();
|
||||
for (patcher, targets) in patchers.iter().zip(all_targets.into_iter()) {
|
||||
for (patcher, targets) in patchers.iter().zip(all_targets) {
|
||||
for target in targets {
|
||||
groups
|
||||
.entry(target)
|
||||
|
||||
+6
-6
@@ -53,11 +53,11 @@ data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v4_gki.hashes_streaming]
|
||||
original = "ea96196191e3a4133db4aff45d47aa3468514e29e0a724faac8081cbf4adf808"
|
||||
patched = "c073cbb271099aaf338a8c70f3565ec70db47a87b4c9bb3efffd96ccee1a0c27"
|
||||
patched = "6b88761a7927ea4a3fdd953cbb7ffb78361e51648f5ab7a34b0dffad2bc163d5"
|
||||
|
||||
[profile.pixel_v4_gki.hashes_seekable]
|
||||
original = "f6615ae355eba38689d24aa535981d09175d4832e7c65c04cdd89aa95d21f09d"
|
||||
patched = "65ce91a95574a308ffc5279696efe7f6f7e4e847795dc9ae43d9ffd012a7ce3f"
|
||||
patched = "e9c0b85a0f9384d3e022f3c83a4653511cd03bfbfa26e660b35cf52986dec888"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
@@ -141,11 +141,11 @@ data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v3.hashes_streaming]
|
||||
original = "4b864b906a13ec98b1d6c3440a5eca7404ec63d073080e80a3b84afabcbd2fa2"
|
||||
patched = "da3b9a4d6a58bb5aaadfc63006cb36b7af9c1c5945e81b38587c245bd5ad1f5a"
|
||||
patched = "cdb9f92e357d8fdf43640704cf7c73917f026e0f759991871ccbab4bfd8c303b"
|
||||
|
||||
[profile.pixel_v3.hashes_seekable]
|
||||
original = "e0e93ee11de56992f3a5f543858c1d11858b4deb76a91a4caf4fb0f3f34be855"
|
||||
patched = "71682a4f31a5920784a0e2e38a6ee5fe3c65213436c7bf1ce347b121cd01a367"
|
||||
patched = "bd57432a97feb62c5e4116f0d5bc3b5db61dd46ffa6e436403d197f091a19a55"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
@@ -174,8 +174,8 @@ data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes_streaming]
|
||||
original = "abc0ad7f80020018101437c2494d6564a877df663d208a123267fc100734c175"
|
||||
patched = "132fb4d90e3c19a7a5ee0da2082767228255228ea5fb66811df1ccd5ff600bb5"
|
||||
patched = "deea7ebf412d413743f14792fbf5605dea1a2a099114b33b9e05d464d90e1228"
|
||||
|
||||
[profile.pixel_v2.hashes_seekable]
|
||||
original = "57d4ac5ab7d362a593f3c02f3d2044b5400c10db0f732741a2d18e025d4231ec"
|
||||
patched = "b365eb32f463ac4ba13f7bed4e6535a68483379640dc6e842c591f194db042c2"
|
||||
patched = "71f49a27c801b837a219927430b41ba37126084d88d141485263a011d2fb9d70"
|
||||
|
||||
+2
-2
@@ -704,11 +704,11 @@ fn create_payload(
|
||||
.with_context(|| format!("Failed to copy from image: {name}"))?;
|
||||
}
|
||||
|
||||
let (_, _, properties, metadata_size) = payload_writer
|
||||
let (_, header, properties) = payload_writer
|
||||
.finish()
|
||||
.context("Failed to finalize payload")?;
|
||||
|
||||
Ok((properties, metadata_size))
|
||||
Ok((properties, header.blob_offset))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ This will update the main [`Cargo.toml`](../Cargo.toml) file.
|
||||
|
||||
## Updating changelog links
|
||||
|
||||
To add an entry to [`CHANGELOG.md`](../CHANGELOG.md), the user should manually type in a message and add repository references, like `[Issue #0]` or `[PR #0 @user]`.
|
||||
To add an entry to [`CHANGELOG.md`](../CHANGELOG.md), the user should manually type in a message and add repository references, like `[Issue #0]` or `[PR #0]`.
|
||||
|
||||
Then, run the following command to generate the appropriate link references:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user