@@ -20,7 +20,7 @@ use pna::{
2020use 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 ) ]
293294pub ( 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