Skip to content

Commit 1efda3a

Browse files
committed
⚗️ Add .mtree support
1 parent 1865b14 commit 1efda3a

4 files changed

Lines changed: 1161 additions & 0 deletions

File tree

cli/src/command/core.rs

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
pub(crate) mod iter;
2+
#[cfg(unix)]
3+
pub(crate) mod mtree;
24
pub(crate) mod path;
35
mod path_filter;
46
pub(crate) mod path_lock;
@@ -36,6 +38,29 @@ use std::{
3638
};
3739
pub(crate) use time_filter::{TimeFilter, TimeFilters};
3840

41+
/// Detected format of an @archive source.
42+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43+
pub(crate) enum SourceFormat {
44+
/// PNA archive format (detected by magic bytes)
45+
Pna,
46+
/// mtree manifest format (text-based)
47+
Mtree,
48+
}
49+
50+
/// Detects the format of an @archive source by examining the first bytes.
51+
///
52+
/// Returns `SourceFormat::Pna` if the data starts with PNA magic bytes,
53+
/// otherwise returns `SourceFormat::Mtree`.
54+
pub(crate) fn detect_format<R: io::BufRead>(reader: &mut R) -> io::Result<SourceFormat> {
55+
let buf = reader.fill_buf()?;
56+
57+
if buf.len() >= PNA_HEADER.len() && buf[..PNA_HEADER.len()] == *PNA_HEADER {
58+
return Ok(SourceFormat::Pna);
59+
}
60+
61+
Ok(SourceFormat::Mtree)
62+
}
63+
3964
/// Options controlling how filesystem items are collected for archiving.
4065
///
4166
/// This struct groups all traversal and filtering options that were previously
@@ -1610,6 +1635,72 @@ pub(crate) fn transform_archive_entries<R: io::Read>(
16101635
}
16111636

16121637
/// Reads entries from an archive source (file or stdin) and transforms them.
1638+
///
1639+
/// This function auto-detects the format of the source:
1640+
/// - PNA archive: Copies entries with optional transformation
1641+
/// - mtree manifest: Reads files from filesystem with metadata overrides (Unix only)
1642+
#[cfg(unix)]
1643+
pub(crate) fn read_archive_source(
1644+
source: &ArchiveSource,
1645+
create_options: &CreateOptions,
1646+
filter: &PathFilter<'_>,
1647+
time_filters: &TimeFilters,
1648+
password: Option<&[u8]>,
1649+
) -> io::Result<Vec<io::Result<Option<NormalEntry>>>> {
1650+
fn process_source<R: io::BufRead>(
1651+
mut reader: R,
1652+
source_name: &str,
1653+
create_options: &CreateOptions,
1654+
filter: &PathFilter<'_>,
1655+
time_filters: &TimeFilters,
1656+
password: Option<&[u8]>,
1657+
) -> io::Result<Vec<io::Result<Option<NormalEntry>>>> {
1658+
let format = detect_format(&mut reader)
1659+
.map_err(|e| io::Error::new(e.kind(), format!("{}: {}", source_name, e)))?;
1660+
1661+
match format {
1662+
SourceFormat::Pna => {
1663+
transform_archive_entries(reader, create_options, filter, time_filters, password)
1664+
}
1665+
SourceFormat::Mtree => {
1666+
mtree::transform_mtree_entries(reader, create_options, filter, time_filters)
1667+
}
1668+
}
1669+
.map_err(|e| io::Error::new(e.kind(), format!("{}: {}", source_name, e)))
1670+
}
1671+
1672+
match source {
1673+
ArchiveSource::File(path) => {
1674+
let file = fs::File::open(path)
1675+
.map_err(|e| io::Error::new(e.kind(), format!("{}: {}", path.display(), e)))?;
1676+
let reader = io::BufReader::with_capacity(64 * 1024, file);
1677+
process_source(
1678+
reader,
1679+
&path.display().to_string(),
1680+
create_options,
1681+
filter,
1682+
time_filters,
1683+
password,
1684+
)
1685+
}
1686+
ArchiveSource::Stdin => {
1687+
let reader = io::BufReader::new(io::stdin().lock());
1688+
process_source(
1689+
reader,
1690+
"<stdin>",
1691+
create_options,
1692+
filter,
1693+
time_filters,
1694+
password,
1695+
)
1696+
}
1697+
}
1698+
}
1699+
1700+
/// Reads entries from an archive source (file or stdin) and transforms them.
1701+
///
1702+
/// On non-Unix platforms, only PNA format is supported. mtree format is not available.
1703+
#[cfg(not(unix))]
16131704
pub(crate) fn read_archive_source(
16141705
source: &ArchiveSource,
16151706
create_options: &CreateOptions,
@@ -1927,4 +2018,68 @@ mod tests {
19272018
assert!(super::validate_no_duplicate_stdin(&sources).is_ok());
19282019
}
19292020
}
2021+
2022+
mod detect_format_tests {
2023+
use super::*;
2024+
use std::io::Cursor;
2025+
2026+
#[test]
2027+
fn pna_magic() {
2028+
// Full PNA header: 0x89 P N A \r \n 0x1A \n
2029+
let data = [0x89, 0x50, 0x4E, 0x41, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00];
2030+
let mut reader = Cursor::new(&data[..]);
2031+
let format = detect_format(&mut reader).unwrap();
2032+
assert_eq!(format, SourceFormat::Pna);
2033+
}
2034+
2035+
#[test]
2036+
fn mtree_header() {
2037+
let data = b"#mtree\n/set type=file mode=0644\n";
2038+
let mut reader = Cursor::new(&data[..]);
2039+
let format = detect_format(&mut reader).unwrap();
2040+
assert_eq!(format, SourceFormat::Mtree);
2041+
}
2042+
2043+
#[test]
2044+
fn mtree_set_directive() {
2045+
let data = b"/set type=file mode=0644\n";
2046+
let mut reader = Cursor::new(&data[..]);
2047+
let format = detect_format(&mut reader).unwrap();
2048+
assert_eq!(format, SourceFormat::Mtree);
2049+
}
2050+
2051+
#[test]
2052+
fn mtree_entry_line() {
2053+
let data = b"usr/bin/hello mode=0755\n";
2054+
let mut reader = Cursor::new(&data[..]);
2055+
let format = detect_format(&mut reader).unwrap();
2056+
assert_eq!(format, SourceFormat::Mtree);
2057+
}
2058+
2059+
#[test]
2060+
fn empty_falls_back_to_mtree() {
2061+
let data = b"";
2062+
let mut reader = Cursor::new(&data[..]);
2063+
let format = detect_format(&mut reader).unwrap();
2064+
assert_eq!(format, SourceFormat::Mtree);
2065+
}
2066+
2067+
#[test]
2068+
fn partial_pna_magic_is_mtree() {
2069+
// Only 4 bytes matching old PNA magic (not full 8-byte header)
2070+
let data = [0x89, 0x50, 0x4E, 0x41, 0x00, 0x00, 0x00, 0x00];
2071+
let mut reader = Cursor::new(&data[..]);
2072+
let format = detect_format(&mut reader).unwrap();
2073+
assert_eq!(format, SourceFormat::Mtree);
2074+
}
2075+
2076+
#[test]
2077+
fn short_buffer_is_mtree() {
2078+
// Less than 8 bytes
2079+
let data = [0x89, 0x50, 0x4E, 0x41];
2080+
let mut reader = Cursor::new(&data[..]);
2081+
let format = detect_format(&mut reader).unwrap();
2082+
assert_eq!(format, SourceFormat::Mtree);
2083+
}
2084+
}
19302085
}

0 commit comments

Comments
 (0)