Skip to content

Commit fcaba85

Browse files
committed
@ support
Redesign @archive inclusion to preserve CLI argument order exactly. The previous implementation separated filesystem paths and @Archives before processing, which lost the original ordering. Key changes: - Add ItemSource, ArchiveSource, CollectedItem types to core.rs - Add collect_items_from_sources for unified processing - Process arguments one by one, maintaining order - Support @- for reading archives from stdin - Detect stdin conflicts (input archive from stdin vs @-)
1 parent f2f9d47 commit fcaba85

4 files changed

Lines changed: 1030 additions & 38 deletions

File tree

cli/src/command/core.rs

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use pna::{
2020
use std::{
2121
borrow::Cow,
2222
collections::HashMap,
23-
fs,
23+
fmt, fs,
2424
io::{self, prelude::*},
2525
path::{Path, PathBuf},
2626
time::SystemTime,
@@ -290,13 +290,159 @@ impl Ignore {
290290
}
291291
}
292292

293+
#[derive(Clone, Debug)]
293294
pub(crate) enum StoreAs {
294295
File,
295296
Dir,
296297
Symlink,
297298
Hardlink(PathBuf),
298299
}
299300

301+
/// Reader for archive sources, avoiding dynamic dispatch.
302+
pub(crate) enum ArchiveReader<'a> {
303+
File(io::BufReader<fs::File>),
304+
Stdin(io::StdinLock<'a>),
305+
}
306+
307+
impl Read for ArchiveReader<'_> {
308+
#[inline]
309+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
310+
match self {
311+
Self::File(r) => r.read(buf),
312+
Self::Stdin(r) => r.read(buf),
313+
}
314+
}
315+
}
316+
317+
/// Source of an archive to include (file path or stdin).
318+
#[derive(Clone, Debug)]
319+
pub(crate) enum ArchiveSource {
320+
File(PathBuf),
321+
Stdin,
322+
}
323+
324+
impl ArchiveSource {
325+
pub(crate) fn open(&self) -> io::Result<ArchiveReader<'_>> {
326+
match self {
327+
Self::File(path) => {
328+
let file = fs::File::open(path)?;
329+
Ok(ArchiveReader::File(io::BufReader::with_capacity(
330+
64 * 1024,
331+
file,
332+
)))
333+
}
334+
Self::Stdin => Ok(ArchiveReader::Stdin(io::stdin().lock())),
335+
}
336+
}
337+
}
338+
339+
impl fmt::Display for ArchiveSource {
340+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341+
match self {
342+
Self::File(path) => write!(f, "{}", path.display()),
343+
Self::Stdin => f.write_str("-"),
344+
}
345+
}
346+
}
347+
348+
/// Represents a CLI file argument that can be either a filesystem path or an archive inclusion.
349+
///
350+
/// Archive inclusions start with '@' and reference entries from an existing archive.
351+
/// This follows bsdtar's convention for including archives.
352+
#[derive(Clone, Debug)]
353+
pub(crate) enum ItemSource {
354+
/// A regular filesystem path (file or directory).
355+
Filesystem(PathBuf),
356+
/// An archive to include entries from.
357+
Archive(ArchiveSource),
358+
}
359+
360+
impl ItemSource {
361+
/// Parses a single CLI argument into an `ItemSource`.
362+
///
363+
/// - `@-` → `Archive(Stdin)`
364+
/// - `@path` → `Archive(File(canonicalize(path)))`
365+
/// - `path` → `Filesystem(path)`
366+
///
367+
/// Archive paths are canonicalized immediately so they remain valid
368+
/// after working directory changes (via -C option).
369+
pub(crate) fn parse(arg: &str) -> io::Result<Self> {
370+
if let Some(archive_path) = arg.strip_prefix('@') {
371+
if archive_path == "-" {
372+
Ok(Self::Archive(ArchiveSource::Stdin))
373+
} else {
374+
let canonical = fs::canonicalize(archive_path)?;
375+
Ok(Self::Archive(ArchiveSource::File(canonical)))
376+
}
377+
} else {
378+
Ok(Self::Filesystem(PathBuf::from(arg)))
379+
}
380+
}
381+
382+
/// Parses multiple CLI arguments into `ItemSource` values.
383+
pub(crate) fn parse_many(args: &[String]) -> io::Result<Vec<Self>> {
384+
args.iter().map(|s| Self::parse(s)).collect()
385+
}
386+
387+
/// Checks if any of the sources use stdin for archive reading.
388+
pub(crate) fn has_stdin_archive(sources: &[Self]) -> bool {
389+
sources
390+
.iter()
391+
.any(|s| matches!(s, Self::Archive(ArchiveSource::Stdin)))
392+
}
393+
}
394+
395+
/// Represents a collected item ready for archive creation.
396+
///
397+
/// This preserves the CLI argument order while separating filesystem items
398+
/// (which need entry building) from archive markers (which need entry copying).
399+
#[derive(Clone, Debug)]
400+
pub(crate) enum CollectedItem {
401+
/// A filesystem item with its path and storage strategy.
402+
Filesystem(PathBuf, StoreAs),
403+
/// A marker indicating where to insert entries from an archive source.
404+
ArchiveMarker(ArchiveSource),
405+
}
406+
407+
/// Collects items from mixed filesystem and archive sources, preserving order.
408+
///
409+
/// For filesystem sources, uses the existing collection logic with shared
410+
/// hardlink detection. For archive sources, returns markers that indicate
411+
/// where archive entries should be inserted.
412+
///
413+
/// # Order Guarantee
414+
/// - Between arguments: strictly preserved
415+
/// - Within a single filesystem argument: walkdir traversal order
416+
///
417+
/// # Hardlink Detection
418+
/// A single `HardlinkResolver` is shared across all filesystem paths,
419+
/// enabling cross-path hardlink detection.
420+
pub(crate) fn collect_items_from_sources(
421+
sources: impl IntoIterator<Item = ItemSource>,
422+
options: &CollectOptions<'_>,
423+
) -> io::Result<Vec<CollectedItem>> {
424+
let mut hardlink_resolver = HardlinkResolver::new(options.follow_links);
425+
let mut results = Vec::new();
426+
427+
for source in sources {
428+
match source {
429+
ItemSource::Filesystem(path) => {
430+
let items = collect_items_with_state(&path, options, &mut hardlink_resolver)?;
431+
results.extend(
432+
items
433+
.into_iter()
434+
.map(|(p, s)| CollectedItem::Filesystem(p, s)),
435+
);
436+
}
437+
ItemSource::Archive(archive_source) => {
438+
results.push(CollectedItem::ArchiveMarker(archive_source));
439+
}
440+
}
441+
}
442+
443+
Ok(results)
444+
}
445+
300446
/// Collects items from multiple paths, preserving CLI argument order.
301447
///
302448
/// State such as hardlink detection is shared across all paths, enabling

0 commit comments

Comments
 (0)