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}