Skip to content

Commit 0875bcb

Browse files
committed
⚗️ Add .mtree support
1 parent 0bec0bc commit 0875bcb

4 files changed

Lines changed: 1254 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
@@ -1622,6 +1647,72 @@ pub(crate) fn transform_archive_entries<R: io::Read>(
16221647
}
16231648

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

0 commit comments

Comments
 (0)