Compare commits

..

12 Commits

Author SHA1 Message Date
Andrew Gunnerson 8ca4eb5ad9 Version 3.17.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 19:07:13 -04:00
Andrew Gunnerson e2b1ccb7a1 CHANGELOG.md: Add entry for PR #467
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 19:06:37 -04:00
Andrew Gunnerson 71a31ae01b Bump Magisk version upper bound to 30100
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 18:55:14 -04:00
Andrew Gunnerson 83d7ffbc5e CHANGELOG.md: Add entry for PR #464
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 20:20:54 -04:00
Andrew Gunnerson e397998d9e Update dependencies
The zip crate gained support for streaming writes in its master branch,
so we can finally upgrade from our ancient fork of it. The new
implementation is done a bit differently, with seekable writers having
the ZipWriter<W> type and streaming writers having the
ZipWriter<StreamWriter<W>> type. This forces us to add a new wrapper
type since we have to switch between them at runtime.

We still need to maintain a (hopefully temporary) fork of the crate due
to a few issues:

1. There's no way to get the original underlying writer instance back
   after finalizing a streaming zip. A fix for this has been submitted
   upstream:
   https://github.com/zip-rs/zip2/pull/367

2. The streaming writes implementation does not include the magic
   signature for data descriptors. While the zip spec says the magic
   value is optional and parsers should not require it, older versions
   of Android's libziparchive do. A fix for this has been submitted
   upstream:
   https://github.com/zip-rs/zip2/pull/368

3. There is currently no way to get the data offset of zip entries.
   avbroot requires this to fill in the OTA metadata's "property files"
   entries, which Android uses to read file data without parsing the zip
   file structures.

This new zip update produces files that are slightly different to
before. The "version made by" and "version needed to extract" fields are
now set to their minimum possible values. Previously, the zip crate was
hardcoded to use versions 4.6 and 2.0, respectively.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 18:55:08 -04:00
Andrew Gunnerson 8ef22508f5 CHANGELOG.md: Add entry for PR #463
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 18:49:32 -04:00
Andrew Gunnerson bf42a6a75c e2e: Split streaming and seekable work directories
Makes troubleshooting easier when files aren't being overwritten.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 17:49:00 -04:00
lefuglyduck a2fe6fc9d8 Update README.md
Clarify the updates section so it doesn't "loop back" to the usage section. 

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-12 16:12:13 -07:00
lefuglyduck faeb1fe988 Update README.md
Minor grammatical change.

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-12 17:53:52 -04:00
Andrew Gunnerson f1b2c6f468 Merge PR #458
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-12 17:51:40 -04:00
lefuglyduck 2f964bf113 Update README.md
Clarify step 3 of usage section.

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-11 22:36:24 -07:00
Andrew Gunnerson f393d7adc4 README.md: Add warning about post-installation snapshot merge operation
Issue: #454

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-08 17:00:21 -04:00
39 changed files with 527 additions and 329 deletions
+9
View File
@@ -7,6 +7,12 @@
to update the actual links at the bottom of the file.
-->
### Version 3.17.1
* Update end-to-end tests to place streaming and seekable OTAs in separate directories for easier troubleshooting ([PR #463])
* Update dependencies ([PR #464])
* Add support for Magisk 30000 ([PR #467])
### Version 3.17.0
* Fix reserved space error when patching OTA zips larger than ~10 GB ([Issue #451], [PR #452])
@@ -523,3 +529,6 @@ Behind-the-scenes changes:
[PR #449]: https://github.com/chenxiaolong/avbroot/pull/449
[PR #452]: https://github.com/chenxiaolong/avbroot/pull/452
[PR #453]: https://github.com/chenxiaolong/avbroot/pull/453
[PR #463]: https://github.com/chenxiaolong/avbroot/pull/463
[PR #464]: https://github.com/chenxiaolong/avbroot/pull/464
[PR #467]: https://github.com/chenxiaolong/avbroot/pull/467
Generated
+260 -174
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.17.0"
version = "3.17.1"
license = "GPL-3.0-only"
edition = "2024"
repository = "https://github.com/chenxiaolong/avbroot"
+11 -5
View File
@@ -51,6 +51,8 @@ avbroot applies the following patches to the partition images:
3. Follow the steps to [generate signing keys](#generating-keys).
Skip this step if you're updating Android, Magisk, or KernelSU after you've performed an [initial setup](#initial-setup). [Updates](#updates) do not require signing keys since you have already generated them in the initial setup.
4. Patch the OTA zip. The base command is:
```bash
@@ -223,21 +225,25 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using the [`OEMUnlockOnBoot` module](https://github.com/chenxiaolong/OEMUnlockOnBoot) to automatically ensure OEM unlocking is enabled on every boot.
10. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
10. That's it! To update the OS, Magisk, or KernelSU see the [next section](#updates).
## Updates
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
Updates to Android, Magisk, and KernelSU are all done the same way: by patching (or repatching) the OTA.
1. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
1. Generate a new patched OTA by following the steps in the [usage section](#usage).
2. Follow the step in the [usage section](#usage) to patch the new OTA.
2. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
3. Reboot to recovery mode. If the screen is stuck at a `No command` message, press the volume up button once while holding down the power button.
4. Sideload the patched OTA with `adb sideload`.
5. That's it!
5. Restart your phone. Note: the phone will likely take a long time to startup after an OS update (a few minutes in some cases).
**Warning**: Due to how virtual A/B works, there is a snapshot merge operation that Android runs invisibly in the background after installing an OTA and rebooting. During the snapshot merge process, it's not possible to sideload another OTA from recovery mode. Avoid doing anything that could result in a boot loop (eg. installing modules) until this process is complete because there is no way to recover, aside from unlocking the bootloader (and wiping) again.
The status can be found by running `adb logcat -v color -s update_engine`. Alternatively, if [Custota](https://github.com/chenxiaolong/Custota) is installed (even if it's not configured to point to a custom OTA server), it will show a notification until the snapshot merge operation completes.
## Reverting to stock firmware
+10 -8
View File
@@ -13,7 +13,7 @@ anyhow = "1.0.75"
base64 = "0.22.1"
bitflags = { version = "2.4.1", features = ["serde"] }
bstr = "1.6.2"
bzip2 = { version = "0.5.1", default-features = false, features = ["libbz2-rs-sys"] }
bzip2 = "0.6.0"
cap-std = "3.0.0"
cap-tempfile = "3.0.0"
clap = { version = "4.4.1", features = ["derive"] }
@@ -34,9 +34,9 @@ memchr = "2.6.0"
num-bigint-dig = "0.8.4"
num-traits = "0.2.16"
passterm = "2.0.3"
phf = { version = "0.11.2", features = ["macros"] }
phf = { version = "0.12.1", features = ["macros"] }
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
prost = "0.13.1"
prost = "0.14.1"
# We can't upgrade to 0.9.0 until rsa updates its rand_core dependency.
rand = "0.8.5"
rayon = "1.7.0"
@@ -60,10 +60,12 @@ x509-cert = { version = "0.2.4", features = ["builder"] }
zerocopy = { version = "0.8.10", features = ["std"] }
zerocopy-derive = "0.8.5"
# https://github.com/zip-rs/zip/pull/383
# https://github.com/zip-rs/zip2/pull/367
# https://github.com/zip-rs/zip2/pull/368
# For getting the data offset when writing new zip entries.
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
git = "https://github.com/chenxiaolong/zip2"
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
default-features = false
features = ["deflate"]
@@ -73,8 +75,8 @@ rustix = { version = "1.0.3", default-features = false, features = ["process"] }
[build-dependencies]
constcat = "0.6.0"
prost-build = "0.13.1"
protox = "0.8.0"
prost-build = "0.14.1"
protox = "0.9.0"
[dev-dependencies]
assert_matches = "1.5.0"
+1 -1
View File
@@ -10,7 +10,7 @@ use std::{
use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use tracing::{debug, Level};
use tracing::{Level, debug};
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
+3 -3
View File
@@ -10,7 +10,7 @@ use std::{
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use cap_std::{
ambient_authority,
fs::{Dir, OpenOptions},
@@ -19,7 +19,7 @@ use clap::{Args, Parser, Subcommand};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use rsa::RsaPublicKey;
use serde::{Deserialize, Serialize};
use tracing::{debug_span, info, warn, Span};
use tracing::{Span, debug_span, info, warn};
use crate::{
crypto::{self, PassphraseSource, RsaSigningKey},
@@ -27,7 +27,7 @@ use crate::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
},
stream::{self, check_cancel, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter},
stream::{self, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter, check_cancel},
util,
};
+1 -1
View File
@@ -7,7 +7,7 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use crate::{
+1 -1
View File
@@ -9,7 +9,7 @@ use std::{
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
use bstr::ByteSlice;
use cap_std::{ambient_authority, fs::Dir};
use clap::{Parser, Subcommand};
+1 -1
View File
@@ -9,7 +9,7 @@ use std::{
sync::atomic::AtomicBool,
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use cap_std::{ambient_authority, fs::Dir};
use clap::{CommandFactory, Parser, Subcommand};
use rayon::iter::{
+18 -15
View File
@@ -11,20 +11,20 @@ use std::{
ops::Range,
path::{Path, PathBuf},
str::FromStr,
sync::{atomic::AtomicBool, Mutex},
sync::{Mutex, atomic::AtomicBool},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use bitflags::bitflags;
use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempDir;
use clap::{value_parser, ArgAction, Args, Parser, Subcommand};
use clap::{ArgAction, Args, Parser, Subcommand, value_parser};
use rayon::{iter::IntoParallelRefIterator, prelude::ParallelIterator};
use tempfile::NamedTempFile;
use topological_sort::TopologicalSort;
use tracing::{debug_span, error, info, warn};
use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use zip::{CompressionMethod, DateTime, ZipArchive, write::SimpleFileOptions};
use crate::{
cli,
@@ -34,6 +34,7 @@ use crate::{
ota::{self, SigningWriter, ZipEntry, ZipMode},
padding,
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcAlgo, VabcParams},
zip::ZipWriterWrapper,
},
patch::{
boot::{
@@ -495,7 +496,9 @@ fn update_security_descriptors(
*pd = cd.clone();
}
_ => {
bail!("{child_name} descriptor ({child_type}) does not match entry in {parent_name} ({parent_type})");
bail!(
"{child_name} descriptor ({child_type}) does not match entry in {parent_name} ({parent_type})"
);
}
}
} else {
@@ -505,7 +508,9 @@ fn update_security_descriptors(
child_header.public_key.clone_into(&mut pd.public_key);
}
_ => {
bail!("{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor");
bail!(
"{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor"
);
}
}
}
@@ -1111,7 +1116,7 @@ fn patch_ota_payload(
fn patch_ota_zip(
raw_reader: &PSeekFile,
zip_reader: &mut ZipArchive<impl Read + Seek>,
mut zip_writer: &mut ZipWriter<impl Write>,
mut zip_writer: &mut ZipWriterWrapper<impl Write>,
external_images: &HashMap<String, PathBuf>,
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
skip_system_ota_cert: bool,
@@ -1165,7 +1170,8 @@ fn patch_ota_zip(
// threshold. This should be sufficient since the output file is likely
// to be larger.
let use_zip64 = reader.size() >= 0xffffffff;
let options = FileOptions::default()
let options = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(CompressionMethod::Stored)
.large_file(use_zip64);
@@ -1202,12 +1208,9 @@ fn patch_ota_zip(
}
// All remaining entries are written immediately.
zip_writer
.start_file_with_extra_data(path, options)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let offset = zip_writer
.end_extra_data()
.with_context(|| format!("Failed to end new zip entry: {path}"))?;
.start_file(path, options)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let mut writer = CountingWriter::new(&mut zip_writer);
match path.as_str() {
@@ -1540,11 +1543,11 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
let mut zip_writer = match cli.zip_mode {
ZipMode::Streaming => {
let signing_writer = SigningWriter::new_streaming(temp_writer);
ZipWriter::new_streaming(signing_writer)
ZipWriterWrapper::new_streaming(signing_writer)
}
ZipMode::Seekable => {
let signing_writer = SigningWriter::new_seekable(temp_writer);
ZipWriter::new(signing_writer)
ZipWriterWrapper::new_seekable(signing_writer)
}
};
+1 -1
View File
@@ -10,7 +10,7 @@ use std::{
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use cap_std::{ambient_authority, fs::Dir};
use clap::{Args, Parser, Subcommand};
use tracing::info;
+2 -2
View File
@@ -10,10 +10,10 @@ use std::{
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use clap::{Parser, Subcommand};
use crc32fast::Hasher;
use zerocopy::{little_endian, IntoBytes};
use zerocopy::{IntoBytes, little_endian};
use crate::{
format::{
+4 -4
View File
@@ -21,25 +21,25 @@ use cms::{
};
use passterm::PromptError;
use pkcs8::{
pkcs5::{pbes2, scrypt},
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
LineEnding, PrivateKeyInfo,
pkcs5::{pbes2, scrypt},
};
use rand::RngCore;
use rsa::{
pkcs1v15::SigningKey, traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey,
Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, pkcs1v15::SigningKey, traits::PublicKeyParts,
};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
use thiserror::Error;
use x509_cert::{
Certificate,
builder::{Builder, CertificateBuilder, Profile},
der::{pem::PemLabel, referenced::OwnedToRef, Any, Decode, DecodePem, EncodePem},
der::{Any, Decode, DecodePem, EncodePem, pem::PemLabel, referenced::OwnedToRef},
serial_number::SerialNumber,
spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned},
time::Validity,
Certificate,
};
use crate::util::DebugString;
+1 -1
View File
@@ -4,7 +4,7 @@
use std::{fmt, marker::PhantomData};
use bstr::{ByteSlice, ByteVec};
use serde::{de::Visitor, Deserializer, Serializer};
use serde::{Deserializer, Serializer, de::Visitor};
use thiserror::Error;
#[derive(Clone, Debug, Error)]
+2 -2
View File
@@ -14,10 +14,10 @@ use bstr::ByteSlice;
use num_bigint_dig::{ModInverse, ToBigInt};
use num_traits::{Pow, ToPrimitive};
use ring::digest::{Algorithm, Context};
use rsa::{traits::PublicKeyParts, BigUint, RsaPublicKey};
use rsa::{BigUint, RsaPublicKey, traits::PublicKeyParts};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zerocopy::{big_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, big_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
+3 -11
View File
@@ -13,7 +13,7 @@ use num_traits::ToPrimitive;
use ring::digest::Context;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zerocopy::{little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
@@ -669,11 +669,7 @@ impl fmt::Display for BootImageV3Through4 {
impl BootImageExt for BootImageV3Through4 {
fn header_version(&self) -> u32 {
if self.v4_extra.is_some() {
4
} else {
3
}
if self.v4_extra.is_some() { 4 } else { 3 }
}
fn header_size(&self) -> u32 {
@@ -1094,11 +1090,7 @@ impl fmt::Display for VendorBootImageV3Through4 {
impl BootImageExt for VendorBootImageV3Through4 {
fn header_version(&self) -> u32 {
if self.v4_extra.is_some() {
4
} else {
3
}
if self.v4_extra.is_some() { 4 } else { 3 }
}
fn header_size(&self) -> u32 {
+1 -1
View File
@@ -3,7 +3,7 @@
use std::io::{self, Read, Seek, Write};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use liblzma::{
read::XzDecoder,
stream::{Check, Stream},
+1 -5
View File
@@ -771,11 +771,7 @@ pub fn sort(entries: &mut [CpioEntry]) {
/// 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)
}
if i == u32::MAX { 1 } else { i.wrapping_add(1) }
}
// (dev maj, dev min) -> (inode set, last assigned inode)
+2 -2
View File
@@ -16,7 +16,7 @@ use rayon::{
slice::{ParallelSlice, ParallelSliceMut},
};
use thiserror::Error;
use zerocopy::{little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
@@ -827,7 +827,7 @@ impl<W: Write> ToWriter<W> for FecImage {
mod tests {
use std::{
io::{Cursor, Seek},
sync::{atomic::AtomicBool, Arc},
sync::{Arc, atomic::AtomicBool},
};
use assert_matches::assert_matches;
+1 -1
View File
@@ -16,7 +16,7 @@ use rayon::{
};
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use zerocopy::{little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
+3 -3
View File
@@ -13,7 +13,7 @@ use bitflags::bitflags;
use bstr::ByteSlice;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zerocopy::{byteorder::little_endian, FromBytes, FromZeros, Immutable, IntoBytes};
use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, byteorder::little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
@@ -21,7 +21,7 @@ use crate::{
stream::{
CountingReader, FromReader, ReadDiscardExt, ReadFixedSizeExt, ToWriter, WriteZerosExt,
},
util::{self, is_zero, DebugString},
util::{self, DebugString, is_zero},
};
/// Magic value for [`RawGeometry::magic`].
@@ -812,7 +812,7 @@ impl RawExtent {
return Err(Error::ExtentInvalidType {
index,
extent_type: n,
})
});
}
}
+2 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod avb;
@@ -13,3 +13,4 @@ pub mod padding;
pub mod payload;
pub mod sparse;
pub mod verityrs;
pub mod zip;
+18 -25
View File
@@ -12,18 +12,21 @@ use std::{
use clap::ValueEnum;
use cms::signed_data::SignedData;
use const_oid::{db::rfc5912, ObjectIdentifier};
use const_oid::{ObjectIdentifier, db::rfc5912};
use memchr::memmem;
use prost::Message;
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use x509_cert::{der::Encode, Certificate};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use x509_cert::{Certificate, der::Encode};
use zip::{CompressionMethod, DateTime, ZipArchive, result::ZipError, write::SimpleFileOptions};
use crate::{
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
format::payload::{self, PayloadHeader},
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
format::{
payload::{self, PayloadHeader},
zip::ZipWriterWrapper,
},
protobuf::build::tools::releasetools::{OtaMetadata, ota_metadata::OtaType},
stream::{self, FromReader, HashingReader, HashingWriter, ReadFixedSizeExt},
};
@@ -435,14 +438,16 @@ impl fmt::Display for ZipMode {
/// directory would start.
pub fn add_metadata(
zip_entries: &[ZipEntry],
zip_writer: &mut ZipWriter<impl Write>,
zip_writer: &mut ZipWriterWrapper<impl Write>,
next_offset: u64,
metadata: &OtaMetadata,
payload_metadata_size: u64,
zip_mode: ZipMode,
) -> Result<OtaMetadata> {
let mut metadata = metadata.clone();
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
let options = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(CompressionMethod::Stored);
let mut zip_entries = zip_entries.to_owned();
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
@@ -462,25 +467,19 @@ pub fn add_metadata(
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
let raw_writer = Cursor::new(Vec::new());
let mut writer = match zip_mode {
ZipMode::Streaming => ZipWriter::new_streaming(raw_writer),
ZipMode::Seekable => ZipWriter::new(raw_writer),
ZipMode::Streaming => ZipWriterWrapper::new_streaming(raw_writer),
ZipMode::Seekable => ZipWriterWrapper::new_seekable(raw_writer),
};
writer
.start_file_with_extra_data(PATH_METADATA, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
let legacy_offset = writer
.end_extra_data()
.start_file(PATH_METADATA, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
writer
.write_all(legacy_raw.as_bytes())
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
writer
.start_file_with_extra_data(PATH_METADATA_PB, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
let modern_offset = writer
.end_extra_data()
.start_file(PATH_METADATA_PB, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
writer
.write_all(&modern_raw)
@@ -509,21 +508,15 @@ pub fn add_metadata(
{
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
zip_writer
.start_file_with_extra_data(PATH_METADATA, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
let legacy_offset = zip_writer
.end_extra_data()
.start_file(PATH_METADATA, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
zip_writer
.write_all(legacy_raw.as_bytes())
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
zip_writer
.start_file_with_extra_data(PATH_METADATA_PB, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
let modern_offset = zip_writer
.end_extra_data()
.start_file(PATH_METADATA_PB, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
zip_writer
.write_all(&modern_raw)
+8 -6
View File
@@ -11,10 +11,10 @@ use std::{
sync::atomic::AtomicBool,
};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use bzip2::write::BzDecoder;
use flate2::{write::GzEncoder, Compression};
use flate2::{Compression, write::GzEncoder};
use liblzma::{
stream::{Check, Stream},
write::XzDecoder,
@@ -30,14 +30,14 @@ use ring::digest::{Context, Digest};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use x509_cert::Certificate;
use zerocopy::{big_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, big_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
protobuf::chromeos_update_engine::{
install_operation::Type, signatures::Signature, DeltaArchiveManifest, Extent,
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
DeltaArchiveManifest, Extent, InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
install_operation::Type, signatures::Signature,
},
stream::{
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadFixedSizeExt,
@@ -121,7 +121,9 @@ pub enum Error {
DataWrite(&'static str, #[source] io::Error),
#[error("Expected {expected} bytes, but only wrote {actual} bytes")]
UnwrittenData { actual: u64, expected: u64 },
#[error("I/O error when applying {op_type:?} operation for {num_blocks} blocks starting at {start_block}")]
#[error(
"I/O error when applying {op_type:?} operation for {num_blocks} blocks starting at {start_block}"
)]
OperationApply {
op_type: Type,
start_block: u64,
+2 -2
View File
@@ -11,7 +11,7 @@ use std::{
use crc32fast::Hasher;
use dlv_list::{Index, VecList};
use thiserror::Error;
use zerocopy::{byteorder::little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, byteorder::little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::stream::ReadDiscardExt;
@@ -225,7 +225,7 @@ impl RawChunk {
return Err(Error::InvalidChunkType {
index,
chunk_type: t,
})
});
}
};
+103
View File
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Seek, SeekFrom, Write};
use zip::{
ZipWriter,
result::ZipResult,
write::{FileOptionExtension, FileOptions, StreamWriter},
};
/// A wrapper around a seekable writer. `W` must implement [`Seek`], but only
/// during the creation of a new instance. The resulting type can be stored in a
/// parent container where the generic type does not implement [`Seek`].
pub struct SeekWriter<W: Write> {
inner: W,
seek_fn: fn(&mut W, SeekFrom) -> io::Result<u64>,
}
impl<W: Write> SeekWriter<W> {
pub fn into_inner(self) -> W {
self.inner
}
}
impl<W: Write + Seek> SeekWriter<W> {
pub fn new(inner: W) -> Self {
Self {
inner,
seek_fn: W::seek,
}
}
}
impl<W: Write> Write for SeekWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<W: Write> Seek for SeekWriter<W> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
(self.seek_fn)(&mut self.inner, pos)
}
}
/// This is an ugly hack to have a single type represent both seekable and
/// streaming [`ZipWriter`]s. `W` only needs to implement [`Seek`] when creating
/// a seekable instance via [`Self::new_seekable`].
pub enum ZipWriterWrapper<W: Write> {
Streaming(ZipWriter<StreamWriter<W>>),
Seekable(ZipWriter<SeekWriter<W>>),
}
impl<W: Write + Seek> ZipWriterWrapper<W> {
pub fn new_seekable(inner: W) -> Self {
Self::Seekable(ZipWriter::new(SeekWriter::new(inner)))
}
}
impl<W: Write> ZipWriterWrapper<W> {
pub fn new_streaming(inner: W) -> Self {
Self::Streaming(ZipWriter::new_stream(inner))
}
pub fn start_file(
&mut self,
name: impl ToString,
options: FileOptions<impl FileOptionExtension>,
) -> ZipResult<u64> {
match self {
Self::Streaming(z) => z.start_file(name, options),
Self::Seekable(z) => z.start_file(name, options),
}
}
pub fn finish(self) -> ZipResult<W> {
match self {
Self::Streaming(z) => Ok(z.finish()?.into_inner()),
Self::Seekable(z) => Ok(z.finish()?.into_inner()),
}
}
}
impl<W: Write> Write for ZipWriterWrapper<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Streaming(z) => z.write(buf),
Self::Seekable(z) => z.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Streaming(z) => z.flush(),
Self::Seekable(z) => z.flush(),
}
}
}
+1 -1
View File
@@ -4,8 +4,8 @@
use std::{
process::ExitCode,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
};
+1 -1
View File
@@ -10,7 +10,7 @@ use std::{
};
use num_traits::{Num, PrimInt};
use serde::{de::Visitor, Deserializer, Serializer};
use serde::{Deserializer, Serializer, de::Visitor};
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
where
+3 -3
View File
@@ -24,9 +24,9 @@ use regex::bytes::Regex;
use ring::digest::Context;
use rsa::RsaPublicKey;
use thiserror::Error;
use tracing::{debug, debug_span, trace, warn, Span};
use tracing::{Span, debug, debug_span, trace, warn};
use x509_cert::Certificate;
use zip::{result::ZipError, ZipArchive};
use zip::{ZipArchive, result::ZipError};
use crate::{
crypto::{self, RsaSigningKey},
@@ -194,7 +194,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..29100];
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..30100];
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;
+5 -3
View File
@@ -6,8 +6,8 @@ use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
use bitflags::bitflags;
use thiserror::Error;
use tracing::trace;
use x509_cert::{der::asn1::BitString, Certificate};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
use x509_cert::{Certificate, der::asn1::BitString};
use zip::{CompressionMethod, DateTime, ZipWriter, result::ZipError, write::SimpleFileOptions};
use crate::{crypto, format::ota};
@@ -79,7 +79,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
CompressionMethod::Stored
};
let options = FileOptions::default().compression_method(compression_method);
let options = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(compression_method);
let name = "ota.x509.pem";
writer.start_file(name, options).map_err(Error::ZipWrite)?;
+1 -1
View File
@@ -10,7 +10,7 @@ use std::{
use memchr::memmem;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use thiserror::Error;
use tracing::{debug, debug_span, trace, Span};
use tracing::{Span, debug, debug_span, trace};
use x509_cert::Certificate;
use zip::ZipArchive;
+1 -1
View File
@@ -5,8 +5,8 @@ use std::{
fs::File,
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
atomic::{AtomicBool, Ordering},
},
};
+1 -1
View File
@@ -69,5 +69,5 @@ bypass = [
unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
"https://github.com/chenxiaolong/zip",
"https://github.com/chenxiaolong/zip2",
]
+5 -3
View File
@@ -24,10 +24,12 @@ tracing = "0.1.40"
tracing-subscriber = "0.3.18"
x509-cert = "0.2.5"
# https://github.com/zip-rs/zip/pull/383
# https://github.com/zip-rs/zip2/pull/367
# https://github.com/zip-rs/zip2/pull/368
# For getting the data offset when writing new zip entries.
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
git = "https://github.com/chenxiaolong/zip2"
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
default-features = false
[features]
+16 -16
View File
@@ -51,12 +51,12 @@ data.version = "vendor_v4"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes_streaming]
original = "55310b8b2fc95ed1ea0df3d4e742afb690f6cafeb8b528cd222e398f9a3b8d1a"
patched = "f666bf02a18f8395827502fa998b925387e204e776c93c01e786f286bdaafdd5"
original = "ef6261cd9ebea90f036e52a46160a400c5b8f6ef24ed2469c4a1e9689987aa06"
patched = "37fd353a766a7b9a339fbf51fa79c703e94640dc6a2c6310d79357aaefcc7ca1"
[profile.pixel_v4_gki.hashes_seekable]
original = "b281face363cd0a0c82cef164e94c762b322a5eff715f00c9cd6a2dc74728acc"
patched = "8c98b5b296a84098beb5ef64a5afb166ab1ba99a0eb79a6dc2f273b505922c22"
original = "8a2c717607c10dfa5483d6f9a9f37b3d978acaf8d2ea18e36544af267943e750"
patched = "2c4734c9e1d028ee6aaf02bb416e5e173857faffd2ca067366790655147b3afa"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
@@ -93,12 +93,12 @@ data.version = "vendor_v4"
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes_streaming]
original = "c3762b138056b053632a10bd8da8dc40aa0b639200d0dba8d7184a88d54212d1"
patched = "fd9921db535c9a8bfaa218b5aabb465b0f0f8c81e45df6d9a5fb66d859f04028"
original = "630220ef813a2b4743d1941179cc9705da86ad4805f1c52341dcb38fbce3d29e"
patched = "b725e91751fe58aed20495aecbf9b4bdc14d2799cd88dcbd58f3a3b02b3af15b"
[profile.pixel_v4_non_gki.hashes_seekable]
original = "522e6de6eb90712dfa371302dd9358fa3ed67ea1683220c767f668f31362b0b0"
patched = "e722443735bd658a9c482a7a40a57712de25517e5298dd5beeb9d5cce9be0b30"
original = "1afbe6867ded345d941098ee7c7fcf94a3df52c50ff96ab8f3a67b2ab957259a"
patched = "4357b977249006b101002c961916f962787315a80b8608494c6a1f0cf09cecd1"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
@@ -136,12 +136,12 @@ data.version = "vendor_v3"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes_streaming]
original = "154e20d00c73b15ffa7d350262d8124a0476d1643742c57a748029609c2a6a2a"
patched = "1aff6ceb3b07507b4d3d39d495d975b65498344f1a8d24434593ec284095c475"
original = "9b65037343d45211e0f9706929cba34643a9c54274d1b39740c43f45974984e0"
patched = "fb23ab9616968b38b96d1e5e6a503154f89aebc1741e89a9e9dfd2c4d9946b05"
[profile.pixel_v3.hashes_seekable]
original = "fb37620bec9e5e104f86ed398f7e08f6c2187173665eb336e4c7ae58e8b50b0e"
patched = "c85e4b9491b53c9e4cbdfb40ef06d5377fbe2b3587d5372b1e20159e65da31df"
original = "e581934887dd93b8a9d9c3aa5dec1d48aa7e01bf01ac507e8c5fb256b59cbe7d"
patched = "669a826abc6d67e7e0b1def724aa7b470461087255c65663d195df1426a355f0"
# Google Pixel 4a
# What's unique: boot (boot v2)
@@ -169,9 +169,9 @@ data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes_streaming]
original = "3715823384e592f76e76f7177a11aec80b9fcb574d049d44efa2d7064c1f51aa"
patched = "b87d9d979beaae57bde2ad21cca612c7330549cfb62661d87a62d30c3ca6e0ba"
original = "f10ee15c900a474cc6bbefa705f272cef42636ea096e75563d2d78f6c4327fd1"
patched = "6929f65909037f5550a53982b71e96bdf69ab876bc5e86702c469ed601be8a9a"
[profile.pixel_v2.hashes_seekable]
original = "5bd47981d15e5795d27f63eb083b5b0dc9c84542a4388cbfcb7f850e26c69f9f"
patched = "67d36e758366386038fb8440aceb689c317d59a565648020faa633734a7cda2e"
original = "4e863d251b9ff6eaa1511f9c03e9bdb8919650b2e0eaf23e33892a639edafcaf"
patched = "9a103222e73df70a097281525546d25c850df2ae7a2ba715aa5dfbbba3f7972b"
+20 -19
View File
@@ -14,12 +14,12 @@ use std::{
path::{Path, PathBuf},
slice,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use avbroot::{
cli::ota::{ExtractCli, PatchCli, VerifyCli},
crypto::{self, PassphraseSource, RsaSigningKey},
@@ -37,10 +37,11 @@ use avbroot::{
ota::{self, SigningWriter, ZipEntry, ZipMode},
padding,
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcParams},
zip::ZipWriterWrapper,
},
patch::otacert::{self, OtaCertBuildFlags},
protobuf::{
build::tools::releasetools::{ota_metadata::OtaType, DeviceState, OtaMetadata},
build::tools::releasetools::{DeviceState, OtaMetadata, ota_metadata::OtaType},
chromeos_update_engine::{
DeltaArchiveManifest, DynamicPartitionGroup, DynamicPartitionMetadata, PartitionUpdate,
},
@@ -48,12 +49,12 @@ use avbroot::{
stream::{self, CountingWriter, FromReader, HashingReader, PSeekFile, Reopen, ToWriter},
};
use clap::Parser;
use rsa::{rand_core::OsRng, traits::PublicKeyParts, BigUint};
use rsa::{BigUint, rand_core::OsRng, traits::PublicKeyParts};
use tempfile::TempDir;
use topological_sort::TopologicalSort;
use tracing::{info, info_span};
use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
use zip::{CompressionMethod, DateTime, ZipWriter, write::SimpleFileOptions};
use crate::{
cli::{Cli, Command, HelperCli, ListCli, PassSource, ProfileGroup, TestCli},
@@ -374,7 +375,7 @@ fn create_boot_image(
.ramdisks
.iter()
.map(|c_list| {
if c_list.iter().any(|c| *c == RamdiskContent::Dlkm) {
if c_list.contains(&RamdiskContent::Dlkm) {
RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM,
ramdisk_name: "dlkm".to_owned(),
@@ -741,14 +742,15 @@ fn create_ota(
let mut zip_writer = match zip_mode {
ZipMode::Streaming => {
let signing_writer = SigningWriter::new_streaming(raw_writer);
ZipWriter::new_streaming(signing_writer)
ZipWriterWrapper::new_streaming(signing_writer)
}
ZipMode::Seekable => {
let signing_writer = SigningWriter::new_seekable(raw_writer);
ZipWriter::new(signing_writer)
ZipWriterWrapper::new_seekable(signing_writer)
}
};
let options = FileOptions::default()
let options = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(CompressionMethod::Stored)
.large_file(false);
@@ -758,12 +760,9 @@ fn create_ota(
for path in [ota::PATH_OTACERT, ota::PATH_PAYLOAD, ota::PATH_PROPERTIES] {
// All remaining entries are written immediately.
zip_writer
.start_file_with_extra_data(path, options)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let offset = zip_writer
.end_extra_data()
.with_context(|| format!("Failed to end new zip entry: {path}"))?;
.start_file(path, options)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let mut writer = CountingWriter::new(&mut zip_writer);
match path {
@@ -864,6 +863,7 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
let raw_writer =
File::create(output).with_context(|| format!("Failed to open for writing: {output:?}"))?;
let mut zip_writer = ZipWriter::new(raw_writer);
let options = SimpleFileOptions::default().last_modified_time(DateTime::default());
for path in [
"assets/stub.apk",
@@ -880,12 +880,12 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
"lib/x86_64/libmagisk64.so",
"lib/x86_64/libmagiskinit.so",
] {
zip_writer.start_file(path, FileOptions::default())?;
zip_writer.start_file(path, options)?;
write!(zip_writer, "dummy contents for {path}")?;
}
// avbroot looks for the version number in this file.
zip_writer.start_file("assets/util_functions.sh", FileOptions::default())?;
zip_writer.start_file("assets/util_functions.sh", options)?;
zip_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
Ok(())
@@ -1140,7 +1140,7 @@ fn clean_boot_image_certs(path: &Path, cancel_signal: &AtomicBool) -> Result<()>
.iter_mut()
.find(|e| e.path == b"system/etc/security/otacerts.zip")
{
let mut zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
let zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
let empty_zip = zip_writer.finish()?.into_inner();
entry.data = CpioEntryData::Data(empty_zip);
@@ -1234,8 +1234,9 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
] {
let _span = info_span!("profile", name, %zip_mode).entered();
// Can't used NamedTempFile because avbroot does atomic replaces.
let profile_dir = work_dir.join(name);
// Can't use NamedTempFile because avbroot does atomic replaces.
let mut profile_dir = work_dir.join(name);
profile_dir.push(zip_mode.to_string());
let out_original = profile_dir.join("ota.zip");
let out_magisk = profile_dir.join("ota_magisk.zip");
let out_prepatched = profile_dir.join("ota_prepatched.zip");
+1 -1
View File
@@ -9,7 +9,7 @@ use std::{
path::Path,
};
use anyhow::{anyhow, bail, Result};
use anyhow::{Result, anyhow, bail};
use regex::Regex;
use crate::WORKSPACE_DIR;
+2 -2
View File
@@ -7,9 +7,9 @@ use std::{
path::Path,
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use clap::Parser;
use toml_edit::{value, DocumentMut};
use toml_edit::{DocumentMut, value};
use crate::WORKSPACE_DIR;