|
1 | 1 | pub(crate) mod iter; |
| 2 | +#[cfg(unix)] |
| 3 | +pub(crate) mod mtree; |
2 | 4 | pub(crate) mod path; |
3 | 5 | mod path_filter; |
4 | 6 | pub(crate) mod path_lock; |
@@ -36,6 +38,29 @@ use std::{ |
36 | 38 | }; |
37 | 39 | pub(crate) use time_filter::{TimeFilter, TimeFilters}; |
38 | 40 |
|
| 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 | + |
39 | 64 | /// Options controlling how filesystem items are collected for archiving. |
40 | 65 | /// |
41 | 66 | /// This struct groups all traversal and filtering options that were previously |
@@ -1610,6 +1635,72 @@ pub(crate) fn transform_archive_entries<R: io::Read>( |
1610 | 1635 | } |
1611 | 1636 |
|
1612 | 1637 | /// 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))] |
1613 | 1704 | pub(crate) fn read_archive_source( |
1614 | 1705 | source: &ArchiveSource, |
1615 | 1706 | create_options: &CreateOptions, |
@@ -1927,4 +2018,68 @@ mod tests { |
1927 | 2018 | assert!(super::validate_no_duplicate_stdin(&sources).is_ok()); |
1928 | 2019 | } |
1929 | 2020 | } |
| 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 | + } |
1930 | 2085 | } |
0 commit comments