Add CLI for packing and unpacking cpio archive

The new commands behave exactly like the existing `avb` and `boot`
subcommands. This completes the last piece of the puzzle for exposing
useful interfaces to avbroot's internal parsers.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit is contained in:
Andrew Gunnerson
2023-09-30 00:04:00 -04:00
parent 2735a6558e
commit f25196f0a2
11 changed files with 550 additions and 183 deletions
+42 -24
View File
@@ -94,6 +94,48 @@ avbroot boot info -i <input boot image>
All of the `boot` subcommands show the boot image information. This specific subcommand just does it without performing any other operation. To show avbroot's internal representation of the information, pass in `-d`.
## `avbroot cpio`
### Unpacking a cpio archive
```bash
avbroot cpio unpack -i <input cpio archive>
```
This subcommand unpacks the cpio archive. The list of file entries and their metadata, like permissions, are written to `cpio.toml`. The contents of regular files are extracted to `cpio_tree/`. Other file types, like symlinks and block devices, are not extracted at all. Their information only exists in the TOML file.
The files inside the tree will have default permissions, ownership, and modification timestamps. This metadata exists only inside the TOML file in order to ensure that the behavior is the same across all platforms.
Both uncompressed archives and compressed archives (gzip or legacy lz4) are supported.
### Packing a cpio archive
```bash
avbroot cpio pack -o <output cpio archive>
```
This subcommand packs a new cpio archive from the file entries listed in `cpio.toml` and the file contents in `cpio_tree/`. Note that **only entries listed in the TOML file are packed**. Extra files inside the tree are silently ignored.
The files are packed in the order listed in the TOML file. When packing ramdisks specifically, it's important to ensure that files are listed after their parent directories (which also **must** exist). Otherwise, the kernel will ignore them. As long as the file paths don't contain anything weird (eg. `a//b` or `a/./b`), sorting the entires with `--sort` should do the trick.
The new archive will be written in the same format (compressed or uncompressed) as the original archive.
### Repacking a cpio archive
```bash
avbroot cpio repack -i <input cpio archive> -o <output cpio archive>
```
This is almost equivalent to running `avbroot cpio unpack` followed by `avbroot cpio pack`, except inode numbers will not be reassigned.
### Showing information about a cpio archive
```bash
avbroot cpio info -i <input cpio archive>
```
All of the `cpio` subcommands show details about all the entries in the archive. This specific subcommand just does it without performing any other operation.
## `avbroot fec`
This set of commands is for working with dm-verity FEC (forward error correction) data. The FEC data allows small errors in partition data to be corrected. This increases reliability of the system because when dm-verity encounters data that doesn't match the expected checksum, it will either trigger a kernel panic or reboot the system.
@@ -150,27 +192,3 @@ avbroot fec repair -i <input/output data file> -f <input FEC file>
This will repair the file in place. As described above, in each column, up to `parity / 2` bytes can be corrected.
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, the file can potentially be "successfully repaired" to some incorrect data.
## `avbroot ramdisk`
### Dumping a cpio archive
```bash
avbroot ramdisk dump -i <cpio archive>
```
This subcommand dumps all information about a cpio archive to stdout. This includes the compression format, all header fields (including the trailer entry), and all data. If an entry's data can be decoded as UTF-8, then it is printed out as text. Otherwise, the binary data is printed out `\x##`-encoded for non-ASCII bytes. The escape-encoded data is truncated to 512 bytes by default to avoid outputting too much data, but this behavior can be disabled with `--no-truncate`.
### Repacking a cpio archive
```bash
avbroot ramdisk repack -i <input cpio archive> -o <output cpio archive>
```
This subcommand repacks a cpio archive, including recompression if needed. This is useful for roundtrip testing of avbroot's cpio parser and compression handling. The uncompressed output should be identical to the uncompressed input, except:
* files are sorted by name
* inodes are reassigned, starting from 300000
* there is no excess padding at the end of the file
The compressed output may differ from what other tools produce due to differences in compression levels and header metadata. avbroot avoids specifying header information where possible (eg. gzip timestamp) for reproducibility.
+3 -3
View File
@@ -8,7 +8,7 @@ use std::sync::atomic::AtomicBool;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::cli::{avb, boot, completion, fec, key, ota, ramdisk};
use crate::cli::{avb, boot, completion, cpio, fec, key, ota};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
@@ -16,10 +16,10 @@ pub enum Command {
Avb(avb::AvbCli),
Boot(boot::BootCli),
Completion(completion::CompletionCli),
Cpio(cpio::CpioCli),
Fec(fec::FecCli),
Key(key::KeyCli),
Ota(ota::OtaCli),
Ramdisk(ramdisk::RamdiskCli),
/// (Deprecated: Use `avbroot ota patch` instead.)
Patch(ota::PatchCli),
/// (Deprecated: Use `avbroot ota extract` instead.)
@@ -42,10 +42,10 @@ pub fn main(cancel_signal: &AtomicBool) -> Result<()> {
Command::Avb(c) => avb::avb_main(&c, cancel_signal),
Command::Boot(c) => boot::boot_main(&c),
Command::Completion(c) => completion::completion_main(&c),
Command::Cpio(c) => cpio::cpio_main(&c, cancel_signal),
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
Command::Key(c) => key::key_main(&c),
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
Command::Ramdisk(c) => ramdisk::ramdisk_main(&c, cancel_signal),
// Deprecated aliases.
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
+2 -2
View File
@@ -34,7 +34,7 @@ use crate::{
util,
};
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct AvbInfo {
header: Header,
footer: Option<Footer>,
@@ -782,7 +782,7 @@ enum AvbCommand {
Verify(VerifyCli),
}
/// Show information about AVB-protected images.
/// Pack, unpack, and inspect AVB-protected images.
#[derive(Debug, Parser)]
pub struct AvbCli {
#[command(subcommand)]
+1 -1
View File
@@ -483,7 +483,7 @@ enum BootCommand {
MagiskInfo(MagiskInfoCli),
}
/// Pack or unpack boot images.
/// Pack, unpack, and inspect boot images.
#[derive(Debug, Parser)]
pub struct BootCli {
#[command(subcommand)]
+387
View File
@@ -0,0 +1,387 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fs::{self, File},
io::{BufReader, BufWriter, Seek},
path::{Path, PathBuf},
str,
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, Context, Result};
use bstr::ByteSlice;
use cap_std::{ambient_authority, fs::Dir};
use clap::{Parser, Subcommand};
use num_traits::ToPrimitive;
use serde::{Deserialize, Serialize};
use crate::{
format::{
compression::{CompressedFormat, CompressedReader, CompressedWriter},
cpio::{self, CpioEntry, CpioEntryData, CpioEntryType, CpioReader, CpioWriter},
},
stream, util,
};
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct CpioInfo {
format: CompressedFormat,
entries: Vec<CpioEntry>,
}
fn open_reader(
path: &Path,
include_trailer: bool,
) -> Result<(
CpioReader<CompressedReader<BufReader<File>>>,
CompressedFormat,
)> {
let file =
File::open(path).with_context(|| format!("Failed to open cpio for reading: {path:?}"))?;
let reader = CompressedReader::new(BufReader::new(file), true)
.with_context(|| format!("Failed to open decompressor: {path:?}"))?;
let format = reader.format();
let cpio_reader = CpioReader::new(reader, include_trailer);
Ok((cpio_reader, format))
}
fn open_writer(
path: &Path,
format: CompressedFormat,
) -> Result<CpioWriter<CompressedWriter<BufWriter<File>>>> {
let file =
File::create(path).with_context(|| format!("Failed to open cpio for writing: {path:?}"))?;
let writer = CompressedWriter::new(BufWriter::new(file), format)
.with_context(|| format!("Failed to open compressor: {path:?}"))?;
let cpio_writer = CpioWriter::new(writer, false);
Ok(cpio_writer)
}
fn flush_writer(writer: CpioWriter<CompressedWriter<BufWriter<File>>>) -> Result<()> {
let compressed_writer = writer.finish().context("Failed to flush cpio writer")?;
let buf_writer = compressed_writer
.finish()
.context("Failed to flush compressor")?;
buf_writer.into_inner().context("Failed to flush file")?;
Ok(())
}
/// Read cpio information from TOML file.
fn read_info(path: &Path) -> Result<CpioInfo> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read cpio info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
.with_context(|| format!("Failed to parse cpio info TOML: {path:?}"))?;
Ok(info)
}
/// Write cpio information to TOML file.
fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(info)
.with_context(|| format!("Failed to serialize cpio info TOML: {path:?}"))?;
fs::write(path, data).with_context(|| format!("Failed to write cpio info TOML: {path:?}"))?;
Ok(())
}
/// Open reader to the corresponding file inside the tree if the entry is a
/// regular file. Unsafe paths will result in an error.
fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let mut reader = tree
.open(path)
.map(|f| BufReader::new(f.into_std()))
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let file_size = reader
.seek(std::io::SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {path:?}"))?
.to_u32()
.ok_or_else(|| anyhow!("File is too large: {path:?}"))?;
reader
.rewind()
.with_context(|| format!("Failed to seek file: {path:?}"))?;
Ok(Some((reader, file_size)))
} else {
Ok(None)
}
}
/// Open writer to the corresponding file inside the tree if the entry is a
/// regular file. Intermediate directories are automatically created as needed.
/// Unsafe paths will result in an error.
fn create_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let parent = util::parent_path(path);
tree.create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
let writer = tree
.create(path)
.map(|f| BufWriter::new(f.into_std()))
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
Ok(Some(writer))
} else {
Ok(None)
}
}
fn display_format(cli: &CpioCli, format: CompressedFormat) {
if !cli.quiet {
println!("Compression format: {format:?}");
}
}
fn display_entry(cli: &CpioCli, entry: &CpioEntry) {
if !cli.quiet {
println!();
println!("{entry}");
}
}
fn unpack_subcommand(
cpio_cli: &CpioCli,
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, false)?;
let mut info = CpioInfo {
format,
entries: vec![],
};
display_format(cpio_cli, format);
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_tree, authority)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
let tree = Dir::open_ambient_dir(&cli.output_tree, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_tree))?;
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
if let Some(mut writer) = create_tree_file(&tree, &entry)? {
let file_size = entry.data.size()?;
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
.context("Failed to copy data")?;
writer.into_inner().context("Failed to flush data")?;
}
info.entries.push(entry);
}
write_info(&cli.output_info, &info)?;
Ok(())
}
fn pack_subcommand(cpio_cli: &CpioCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut info = read_info(&cli.input_info)?;
let mut writer = open_writer(&cli.output, info.format)?;
let mut inode = 300000;
display_format(cpio_cli, info.format);
if cli.sort {
cpio::sort(&mut info.entries);
}
let authority = ambient_authority();
let tree = Dir::open_ambient_dir(&cli.input_tree, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_tree))?;
for entry in &mut info.entries {
entry.inode = inode;
inode += 1;
let out = open_tree_file(&tree, entry)?;
if let Some((_, file_size)) = &out {
entry.data = CpioEntryData::Size(*file_size);
}
display_entry(cpio_cli, entry);
writer
.start_entry(entry)
.context("Failed to write cpio entry")?;
if let Some((mut reader, file_size)) = out {
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
.context("Failed to copy data")?;
}
}
flush_writer(writer)?;
Ok(())
}
fn repack_subcommand(
cpio_cli: &CpioCli,
cli: &RepackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, false)?;
let mut writer = open_writer(&cli.output, format)?;
display_format(cpio_cli, format);
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
writer
.start_entry(&entry)
.context("Failed to write cpio entry")?;
if let CpioEntryData::Size(s) = &entry.data {
stream::copy_n(&mut reader, &mut writer, u64::from(*s), cancel_signal)
.context("Failed to copy cpio entry data")?;
}
}
flush_writer(writer)?;
Ok(())
}
fn info_subcommand(cpio_cli: &CpioCli, cli: &InfoCli) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, cli.trailer)?;
display_format(cpio_cli, format);
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
}
Ok(())
}
pub fn cpio_main(cli: &CpioCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
CpioCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
CpioCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
CpioCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
CpioCommand::Info(c) => info_subcommand(cli, c),
}
}
/// Unpack a cpio archive.
///
/// Regular files will be extracted to the output tree directory, but not any
/// other type of file (eg. symlinks). All file metadata is written to the info
/// TOML file, like the UID/GID, permissions, and symlink targets.
///
/// If any paths inside the cpio archive are unsafe, the extraction process will
/// fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output cpio info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
output_info: PathBuf,
/// Path to output files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
output_tree: PathBuf,
}
/// Pack a cpio archive.
///
/// The new cpio archive will *only* contain files listed in the info TOML file.
/// Extra files inside the input tree directory that aren't listed will be
/// silently ignored. Entries are added to the archive in the order that they
/// are listed unless --sort is specified.
///
/// All fields inside the info TOML are used as-is, except for the inode
/// numbers, which will be regenerated. If any fields for an entry are missing,
/// they will be set to 0.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to input info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
input_info: PathBuf,
/// Path to input files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
input_tree: PathBuf,
/// Sort entries before packing.
#[arg(long)]
sort: bool,
}
/// Repack a cpio archive.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
}
/// Display cpio entry information.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Show cpio trailer entry.
#[arg(long, global = true)]
trailer: bool,
}
#[derive(Debug, Subcommand)]
enum CpioCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect cpio archives.
#[derive(Debug, Parser)]
pub struct CpioCli {
#[command(subcommand)]
command: CpioCommand,
/// Don't print cpio entry information.
#[arg(short, long, global = true)]
quiet: bool,
}
+1 -1
View File
@@ -7,10 +7,10 @@ pub mod args;
pub mod avb;
pub mod boot;
pub mod completion;
pub mod cpio;
pub mod fec;
pub mod key;
pub mod ota;
pub mod ramdisk;
macro_rules! status {
($($arg:tt)*) => {
-131
View File
@@ -1,131 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fs::File,
io::{BufReader, BufWriter},
path::{Path, PathBuf},
str,
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::{
format::{
compression::{CompressedFormat, CompressedReader, CompressedWriter},
cpio::{CpioEntryData, CpioReader, CpioWriter},
},
stream,
};
fn open_reader(
path: &Path,
include_trailer: bool,
) -> Result<(
CpioReader<CompressedReader<BufReader<File>>>,
CompressedFormat,
)> {
let file = File::open(path)?;
let reader = CompressedReader::new(BufReader::new(file), true)?;
let format = reader.format();
let cpio_reader = CpioReader::new(reader, include_trailer);
Ok((cpio_reader, format))
}
fn open_writer(
path: &Path,
format: CompressedFormat,
) -> Result<CpioWriter<CompressedWriter<BufWriter<File>>>> {
let file = File::create(path)?;
let writer = CompressedWriter::new(BufWriter::new(file), format)?;
let cpio_writer = CpioWriter::new(writer, false);
Ok(cpio_writer)
}
fn flush_writer(writer: CpioWriter<CompressedWriter<BufWriter<File>>>) -> Result<()> {
let compressed_writer = writer.finish()?;
let buf_writer = compressed_writer.finish()?;
buf_writer.into_inner()?;
Ok(())
}
pub fn ramdisk_main(cli: &RamdiskCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
RamdiskCommand::Dump(c) => {
let (mut reader, format) = open_reader(&c.input, true)
.with_context(|| format!("Failed to read cpio: {:?}", c.input))?;
println!("Compression format: {format:?}");
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
println!();
println!("{entry}");
}
}
RamdiskCommand::Repack(c) => {
let (mut reader, format) = open_reader(&c.input, false)
.with_context(|| format!("Failed to open cpio for reading: {:?}", c.input))?;
let mut writer = open_writer(&c.output, format)
.with_context(|| format!("Failed to open cpio for writing: {:?}", c.output))?;
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
writer
.start_entry(&entry)
.context("Failed to write cpio entry")?;
if let CpioEntryData::Size(s) = &entry.data {
stream::copy_n(&mut reader, &mut writer, u64::from(*s), cancel_signal)
.context("Failed to copy cpio entry data")?;
}
}
flush_writer(writer).context("Failed to flush cpio writer")?;
}
}
Ok(())
}
/// Dump cpio headers and data.
#[derive(Debug, Parser)]
struct DumpCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Do not truncate binary file contents.
#[arg(long)]
no_truncate: bool,
}
/// Repack cpio archive.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
}
#[derive(Debug, Subcommand)]
enum RamdiskCommand {
Dump(DumpCli),
Repack(RepackCli),
}
/// Show information about ramdisk cpio archives.
#[derive(Debug, Parser)]
pub struct RamdiskCli {
#[command(subcommand)]
command: RamdiskCommand,
}
+2 -1
View File
@@ -8,6 +8,7 @@ use std::io::{self, Read, Seek, Write};
use byteorder::{LittleEndian, WriteBytesExt};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use lz4_flex::frame::FrameDecoder;
use serde::{Deserialize, Serialize};
use thiserror::Error;
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
@@ -96,7 +97,7 @@ impl<W: Write> Write for Lz4LegacyEncoder<W> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum CompressedFormat {
None,
Gzip,
+58 -20
View File
@@ -11,11 +11,14 @@ use std::{
};
use bstr::ByteSlice;
use num_traits::ToPrimitive;
use num_traits::{ToPrimitive, Zero};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
escape,
format::padding,
octal,
stream::{
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt,
},
@@ -109,7 +112,7 @@ fn read_data(reader: impl Read, size: usize, cancel_signal: &AtomicBool) -> io::
Ok(cursor.into_inner())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum CpioEntryType {
Pipe,
Char,
@@ -174,7 +177,8 @@ impl Default for CpioEntryType {
}
}
#[derive(Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum CpioEntryData {
/// Size of entry's data. [`CpioReader`] and [`CpioWriter`] use this for
/// [`CpioEntryType::Regular`] entries to allow for lazy reads and writes.
@@ -184,7 +188,7 @@ pub enum CpioEntryData {
/// content. [`CpioReader`] will never use this when reading regular files.
/// [`CpioWriter`] will write this immediately and lazy writes will not be
/// allowed.
Data(Vec<u8>),
Data(#[serde(with = "escape")] Vec<u8>),
}
impl fmt::Debug for CpioEntryData {
@@ -214,45 +218,77 @@ impl CpioEntryData {
Ok(size)
}
fn is_size(&self) -> bool {
matches!(self, CpioEntryData::Size(_))
}
}
#[derive(Clone, Default, PartialEq, Eq)]
#[derive(Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpioEntry {
/// File path.
#[serde(with = "escape")]
pub path: Vec<u8>,
/// File data.
#[serde(default, skip_serializing_if = "CpioEntryData::is_size")]
pub data: CpioEntryData,
/// Inode number.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub inode: u32,
/// File type portion of the `st_mode`-style mode.
pub file_type: CpioEntryType,
/// Permissions portion of the `st_mode`-style mode.
#[serde(default, skip_serializing_if = "Zero::is_zero", with = "octal")]
pub file_mode: u16,
/// Owner user ID.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub uid: u32,
/// Owner group ID.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub gid: u32,
/// Number of paths referencing the inode.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub nlink: u32,
/// Modification timestamp in Unix time.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub mtime: u32,
/// Major ID (class of device) for the device containing the inode.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub dev_maj: u32,
/// Minor ID (specific device instance) for the device containing the inode.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub dev_min: u32,
/// Major ID (class of device) represented by this entry. This is only
/// relevant for [`CpioEntryType::Char`] and [`CpioEntryType::Block`].
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub rdev_maj: u32,
/// Minor ID (specific device instance) represented by this entry. This is
/// only relevant for [`CpioEntryType::Char`] and [`CpioEntryType::Block`].
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub rdev_min: u32,
/// CRC32 checksum.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub crc32: u32,
/// File path.
pub path: Vec<u8>,
/// File data.
pub data: CpioEntryData,
}
impl fmt::Debug for CpioEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CpioEntry")
.field("path", &self.path.as_bstr())
.field("data", &self.data)
.field("inode", &self.inode)
.field("file_type", &self.file_type)
.field("file_mode", &self.file_mode)
@@ -265,8 +301,6 @@ impl fmt::Debug for CpioEntry {
.field("rdev_maj", &self.rdev_maj)
.field("rdev_min", &self.rdev_min)
.field("crc32", &self.crc32)
.field("path", &self.path.as_bstr())
.field("data", &self.data)
.finish()
}
}
@@ -304,44 +338,48 @@ impl fmt::Display for CpioEntry {
impl CpioEntry {
pub fn new_trailer() -> Self {
Self {
path: CPIO_TRAILER.to_vec(),
// Must be 1 for CRC format.
nlink: 1,
path: CPIO_TRAILER.to_vec(),
..Default::default()
}
}
pub fn new_symlink(path: &[u8], link_target: &[u8]) -> Self {
Self {
path: path.to_owned(),
data: CpioEntryData::Data(link_target.to_owned()),
file_type: CpioEntryType::Symlink,
file_mode: 0o777,
nlink: 1,
path: path.to_owned(),
data: CpioEntryData::Data(link_target.to_owned()),
..Default::default()
}
}
pub fn new_directory(path: &[u8], mode: u16) -> Self {
Self {
path: path.to_owned(),
file_type: CpioEntryType::Directory,
file_mode: mode,
nlink: 1,
path: path.to_owned(),
..Default::default()
}
}
pub fn new_file(path: &[u8], mode: u16, data: CpioEntryData) -> Self {
Self {
path: path.to_owned(),
data,
file_type: CpioEntryType::Regular,
file_mode: mode,
nlink: 1,
path: path.to_owned(),
data,
..Default::default()
}
}
pub fn is_trailer(&self) -> bool {
self.path == CPIO_TRAILER
}
}
impl<R: Read> FromReader<R> for CpioEntry {
@@ -407,6 +445,8 @@ impl<R: Read> FromReader<R> for CpioEntry {
};
Ok(Self {
path,
data,
inode,
file_type,
file_mode: (mode & 0o7777) as u16,
@@ -419,8 +459,6 @@ impl<R: Read> FromReader<R> for CpioEntry {
rdev_maj,
rdev_min,
crc32,
path,
data,
})
}
}
@@ -522,7 +560,7 @@ impl<R: Read> CpioReader<R> {
let entry = CpioEntry::from_reader(&mut self.reader)?;
if entry.path == CPIO_TRAILER {
if entry.is_trailer() {
self.done = true;
if !self.include_trailer {
+1
View File
@@ -18,6 +18,7 @@ pub mod cli;
pub mod crypto;
pub mod escape;
pub mod format;
pub mod octal;
pub mod protobuf;
pub mod stream;
pub mod util;
+53
View File
@@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
//! Hack to format an integer as an octal string because toml_edit can't output
//! octal-formatted integers and many other toml parsers can't parse it either.
use std::{
fmt::{self, Octal},
marker::PhantomData,
};
use num_traits::{Num, PrimInt};
use serde::{de::Visitor, Deserializer, Serializer};
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: PrimInt + Octal,
{
serializer.serialize_str(&format!("{data:o}"))
}
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: PrimInt,
<T as Num>::FromStrRadixErr: fmt::Display,
{
struct OctalStrVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
where
T: PrimInt,
<T as Num>::FromStrRadixErr: fmt::Display,
{
type Value = T;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a string containing an octal number")
}
fn visit_str<E>(self, data: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
T::from_str_radix(data, 8).map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(OctalStrVisitor(PhantomData))
}