|
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 |
@@ -1622,6 +1647,72 @@ pub(crate) fn transform_archive_entries<R: io::Read>( |
1622 | 1647 | } |
1623 | 1648 |
|
1624 | 1649 | /// 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))] |
1625 | 1716 | pub(crate) fn read_archive_source( |
1626 | 1717 | source: &ArchiveSource, |
1627 | 1718 | create_options: &CreateOptions, |
@@ -1939,4 +2030,68 @@ mod tests { |
1939 | 2030 | assert!(super::validate_no_duplicate_stdin(&sources).is_ok()); |
1940 | 2031 | } |
1941 | 2032 | } |
| 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 | + } |
1942 | 2097 | } |
0 commit comments