Skip to main content

stuffr_core/source/
file.rs

1use std::fs::File;
2use std::io::Read;
3use std::path::Path;
4
5use super::{GuardedSeek, SeekRead, Source, SourceCaps};
6use crate::error::Result;
7
8/// A source backed by an opened file. The Exact rung when it is a regular
9/// file (or, on unix, a block device): those seek.
10///
11/// A path can name something else — a FIFO, a character device, a socket —
12/// and `File::open` succeeds on all of them, but their first `lseek` fails
13/// with `ESPIPE`. Claiming `seekable` for one sent a container straight to
14/// its seek path, and `stuffr unpack fifo -C dir` answered "Illegal seek" at
15/// exit 1 (the 2026-10-31 audit follow-ups). Such a file is now a stream,
16/// exactly as standard input is: no length, no `as_seek`, so the stream
17/// ladder reads it forward or spills it.
18pub struct FileSource {
19    /// Wrapped, never bare: a container seeks to positions the ARCHIVE
20    /// declares, and a `u64` above `i64::MAX` reaches `lseek(2)` as a
21    /// negative offset and comes back as an unclassified `EINVAL`. See
22    /// [`GuardedSeek`]'s module doc.
23    file: GuardedSeek<File>,
24    /// The content's length: a regular file's size. `None` for anything
25    /// else, whose metadata size is no content length (a FIFO says 0, and so
26    /// does a block device on macOS).
27    len: Option<u64>,
28    /// Whether `file` can seek: a regular file or a block device.
29    seekable: bool,
30}
31
32impl FileSource {
33    pub fn open(path: &Path) -> Result<Self> {
34        Self::from_file(File::open(path)?)
35    }
36
37    pub fn from_file(file: File) -> Result<Self> {
38        let meta = file.metadata()?;
39        let seekable = seeks(&meta.file_type());
40        Ok(Self {
41            file: GuardedSeek::new(file),
42            len: meta.is_file().then_some(meta.len()),
43            seekable,
44        })
45    }
46}
47
48/// Whether a file of this type seeks: a regular file, or a block device on
49/// unix. A FIFO, character device or socket does not.
50fn seeks(kind: &std::fs::FileType) -> bool {
51    #[cfg(unix)]
52    {
53        use std::os::unix::fs::FileTypeExt;
54        kind.is_file() || kind.is_block_device()
55    }
56    #[cfg(not(unix))]
57    {
58        kind.is_file()
59    }
60}
61
62impl Read for FileSource {
63    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
64        self.file.read(buf)
65    }
66}
67
68impl Source for FileSource {
69    fn caps(&self) -> SourceCaps {
70        SourceCaps {
71            seekable: self.seekable,
72            len: self.len,
73        }
74    }
75
76    fn as_seek(&mut self) -> Option<&mut dyn SeekRead> {
77        if self.seekable {
78            Some(&mut self.file)
79        } else {
80            None
81        }
82    }
83}