|
| 1 | +// |
| 2 | +// Copyright (c) 2024 Jeff Garzik |
| 3 | +// |
| 4 | +// This file is part of the pax-rs project covered under |
| 5 | +// the MIT License. For the full license text, please see the LICENSE |
| 6 | +// file in the root directory of this project. |
| 7 | +// SPDX-License-Identifier: MIT |
| 8 | +// |
| 9 | + |
| 10 | +use crate::error::PaxResult; |
| 11 | +use std::collections::HashMap; |
| 12 | +use std::path::{Path, PathBuf}; |
| 13 | + |
| 14 | +/// Type of archive entry |
| 15 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 16 | +pub enum EntryType { |
| 17 | + /// Regular file |
| 18 | + #[default] |
| 19 | + Regular, |
| 20 | + /// Directory |
| 21 | + Directory, |
| 22 | + /// Symbolic link |
| 23 | + Symlink, |
| 24 | + /// Hard link to another file |
| 25 | + Hardlink, |
| 26 | + /// Block device |
| 27 | + BlockDevice, |
| 28 | + /// Character device |
| 29 | + CharDevice, |
| 30 | + /// FIFO (named pipe) |
| 31 | + Fifo, |
| 32 | + /// Socket (not typically stored in archives, but recognized) |
| 33 | + Socket, |
| 34 | +} |
| 35 | + |
| 36 | +/// Metadata for an archive entry |
| 37 | +#[derive(Debug, Clone, Default)] |
| 38 | +pub struct ArchiveEntry { |
| 39 | + /// Path of the file within the archive |
| 40 | + pub path: PathBuf, |
| 41 | + /// File mode (permissions) |
| 42 | + pub mode: u32, |
| 43 | + /// User ID |
| 44 | + pub uid: u32, |
| 45 | + /// Group ID |
| 46 | + pub gid: u32, |
| 47 | + /// File size in bytes |
| 48 | + pub size: u64, |
| 49 | + /// Modification time (seconds since epoch) |
| 50 | + pub mtime: u64, |
| 51 | + /// Modification time nanoseconds (for pax format) |
| 52 | + pub mtime_nsec: u32, |
| 53 | + /// Access time (seconds since epoch, for pax format) |
| 54 | + pub atime: Option<u64>, |
| 55 | + /// Access time nanoseconds (for pax format) |
| 56 | + pub atime_nsec: u32, |
| 57 | + /// Type of entry |
| 58 | + pub entry_type: EntryType, |
| 59 | + /// Link target for symlinks and hardlinks |
| 60 | + pub link_target: Option<PathBuf>, |
| 61 | + /// User name (optional) |
| 62 | + pub uname: Option<String>, |
| 63 | + /// Group name (optional) |
| 64 | + pub gname: Option<String>, |
| 65 | + /// Device ID (for hard link tracking) |
| 66 | + pub dev: u64, |
| 67 | + /// Inode number (for hard link tracking) |
| 68 | + pub ino: u64, |
| 69 | + /// Number of hard links |
| 70 | + pub nlink: u32, |
| 71 | + /// Device major number (for block/char devices) |
| 72 | + pub devmajor: u32, |
| 73 | + /// Device minor number (for block/char devices) |
| 74 | + pub devminor: u32, |
| 75 | +} |
| 76 | + |
| 77 | +impl ArchiveEntry { |
| 78 | + /// Create a new archive entry with default values |
| 79 | + pub fn new(path: PathBuf, entry_type: EntryType) -> Self { |
| 80 | + ArchiveEntry { |
| 81 | + path, |
| 82 | + mode: 0o644, |
| 83 | + uid: 0, |
| 84 | + gid: 0, |
| 85 | + size: 0, |
| 86 | + mtime: 0, |
| 87 | + mtime_nsec: 0, |
| 88 | + atime: None, |
| 89 | + atime_nsec: 0, |
| 90 | + entry_type, |
| 91 | + link_target: None, |
| 92 | + uname: None, |
| 93 | + gname: None, |
| 94 | + dev: 0, |
| 95 | + ino: 0, |
| 96 | + nlink: 1, |
| 97 | + devmajor: 0, |
| 98 | + devminor: 0, |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + /// Check if this entry is a special device file |
| 103 | + pub fn is_device(&self) -> bool { |
| 104 | + matches!( |
| 105 | + self.entry_type, |
| 106 | + EntryType::BlockDevice | EntryType::CharDevice |
| 107 | + ) |
| 108 | + } |
| 109 | + |
| 110 | + /// Check if this is a directory |
| 111 | + pub fn is_dir(&self) -> bool { |
| 112 | + self.entry_type == EntryType::Directory |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +/// Trait for reading archives |
| 117 | +pub trait ArchiveReader { |
| 118 | + /// Read the next entry from the archive |
| 119 | + /// Returns None when the archive is exhausted |
| 120 | + fn read_entry(&mut self) -> PaxResult<Option<ArchiveEntry>>; |
| 121 | + |
| 122 | + /// Read the data for the current entry |
| 123 | + fn read_data(&mut self, buf: &mut [u8]) -> PaxResult<usize>; |
| 124 | + |
| 125 | + /// Skip the data for the current entry |
| 126 | + fn skip_data(&mut self) -> PaxResult<()>; |
| 127 | +} |
| 128 | + |
| 129 | +/// Trait for writing archives |
| 130 | +pub trait ArchiveWriter { |
| 131 | + /// Write an entry header to the archive |
| 132 | + fn write_entry(&mut self, entry: &ArchiveEntry) -> PaxResult<()>; |
| 133 | + |
| 134 | + /// Write data for the current entry |
| 135 | + fn write_data(&mut self, data: &[u8]) -> PaxResult<()>; |
| 136 | + |
| 137 | + /// Finish writing data for the current entry (handles padding) |
| 138 | + fn finish_entry(&mut self) -> PaxResult<()>; |
| 139 | + |
| 140 | + /// Write the archive trailer |
| 141 | + fn finish(&mut self) -> PaxResult<()>; |
| 142 | +} |
| 143 | + |
| 144 | +/// Tracks hard links during archive creation |
| 145 | +#[derive(Debug, Default)] |
| 146 | +pub struct HardLinkTracker { |
| 147 | + /// Maps (dev, ino) to the first path seen |
| 148 | + seen: HashMap<(u64, u64), PathBuf>, |
| 149 | +} |
| 150 | + |
| 151 | +impl HardLinkTracker { |
| 152 | + /// Create a new tracker |
| 153 | + pub fn new() -> Self { |
| 154 | + HardLinkTracker { |
| 155 | + seen: HashMap::new(), |
| 156 | + } |
| 157 | + } |
| 158 | + |
| 159 | + /// Check if we've seen this file before (by dev/ino) |
| 160 | + /// Returns the original path if this is a hard link |
| 161 | + pub fn check(&mut self, entry: &ArchiveEntry) -> Option<PathBuf> { |
| 162 | + if entry.nlink <= 1 { |
| 163 | + return None; |
| 164 | + } |
| 165 | + |
| 166 | + let key = (entry.dev, entry.ino); |
| 167 | + if let Some(original) = self.seen.get(&key) { |
| 168 | + Some(original.clone()) |
| 169 | + } else { |
| 170 | + self.seen.insert(key, entry.path.clone()); |
| 171 | + None |
| 172 | + } |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +/// Tracks extracted files for hard link creation during extraction |
| 177 | +#[derive(Debug, Default)] |
| 178 | +pub struct ExtractedLinks { |
| 179 | + /// Maps (dev, ino) to the extracted path |
| 180 | + extracted: HashMap<(u64, u64), PathBuf>, |
| 181 | +} |
| 182 | + |
| 183 | +impl ExtractedLinks { |
| 184 | + /// Create a new tracker |
| 185 | + pub fn new() -> Self { |
| 186 | + ExtractedLinks { |
| 187 | + extracted: HashMap::new(), |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + /// Record that we extracted a file |
| 192 | + pub fn record(&mut self, entry: &ArchiveEntry, path: &Path) { |
| 193 | + if entry.nlink > 1 { |
| 194 | + let key = (entry.dev, entry.ino); |
| 195 | + self.extracted |
| 196 | + .entry(key) |
| 197 | + .or_insert_with(|| path.to_path_buf()); |
| 198 | + } |
| 199 | + } |
| 200 | + |
| 201 | + /// Get the path to link to, if this is a hard link |
| 202 | + pub fn get_link_target(&self, entry: &ArchiveEntry) -> Option<&PathBuf> { |
| 203 | + if entry.nlink <= 1 { |
| 204 | + return None; |
| 205 | + } |
| 206 | + let key = (entry.dev, entry.ino); |
| 207 | + self.extracted.get(&key) |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | +/// Archive format type |
| 212 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 213 | +pub enum ArchiveFormat { |
| 214 | + /// POSIX ustar tar format |
| 215 | + Ustar, |
| 216 | + /// POSIX cpio format |
| 217 | + Cpio, |
| 218 | + /// POSIX pax format (extended tar with extended headers) |
| 219 | + Pax, |
| 220 | +} |
| 221 | + |
| 222 | +impl std::fmt::Display for ArchiveFormat { |
| 223 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 224 | + match self { |
| 225 | + ArchiveFormat::Ustar => write!(f, "ustar"), |
| 226 | + ArchiveFormat::Cpio => write!(f, "cpio"), |
| 227 | + ArchiveFormat::Pax => write!(f, "pax"), |
| 228 | + } |
| 229 | + } |
| 230 | +} |
0 commit comments