Skip to main content

orbis_pfs/
file.rs

1use crate::Pfs;
2use crate::image::Image;
3use crate::inode::Inode;
4use std::cmp::min;
5use std::io::{self, Error, Read, Seek, SeekFrom};
6use std::sync::Arc;
7
8/// Represents a file in the PFS.
9///
10/// Use [`read_at()`](Self::read_at) for positional reads (thread-safe, `&self`),
11/// or [`as_slice()`](Self::as_slice) for zero-copy access when available.
12///
13/// Files may be compressed, in which case you should use
14/// [`pfsc::PfscImage`][crate::pfsc::PfscImage] as an [`Image`] adapter.
15///
16/// # Example
17///
18/// ```no_run
19/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
20/// # let data = vec![];
21/// let pfs = orbis_pfs::open_slice(&data, None)?;
22/// let root = pfs.root().open()?;
23///
24/// if let Some(orbis_pfs::directory::DirEntry::File(file)) = root.get(b"example.txt") {
25///     let mut contents = vec![0u8; file.len() as usize];
26///     file.read_at(0, &mut contents)?;
27///     println!("File contents: {}", String::from_utf8_lossy(&contents));
28/// }
29/// # Ok(())
30/// # }
31/// ```
32#[must_use]
33pub struct File<'a> {
34    pfs: Arc<Pfs<'a>>,
35    inode: usize,
36}
37
38impl<'a> std::fmt::Debug for File<'a> {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        f.debug_struct("File")
41            .field("inode", &self.inode)
42            .field("len", &self.len())
43            .field("mode", &self.mode())
44            .finish_non_exhaustive()
45    }
46}
47
48impl<'a> File<'a> {
49    pub(crate) fn new(pfs: Arc<Pfs<'a>>, inode: usize) -> Self {
50        Self { pfs, inode }
51    }
52
53    #[must_use]
54    pub fn mode(&self) -> u16 {
55        self.inode_ref().mode()
56    }
57
58    #[must_use]
59    pub fn flags(&self) -> u32 {
60        self.inode_ref().flags().value()
61    }
62
63    #[must_use]
64    pub fn len(&self) -> u64 {
65        self.inode_ref().size()
66    }
67
68    #[must_use]
69    pub fn compressed_len(&self) -> u64 {
70        self.inode_ref().compressed_len()
71    }
72
73    /// Returns the last access time as seconds since the Unix epoch.
74    #[must_use]
75    pub fn atime(&self) -> u64 {
76        self.inode_ref().atime()
77    }
78
79    /// Returns the last modification time as seconds since the Unix epoch.
80    #[must_use]
81    pub fn mtime(&self) -> u64 {
82        self.inode_ref().mtime()
83    }
84
85    /// Returns the last metadata change time as seconds since the Unix epoch.
86    #[must_use]
87    pub fn ctime(&self) -> u64 {
88        self.inode_ref().ctime()
89    }
90
91    /// Returns the creation time as seconds since the Unix epoch.
92    #[must_use]
93    pub fn birthtime(&self) -> u64 {
94        self.inode_ref().birthtime()
95    }
96
97    /// Returns the sub-second nanosecond component of [`mtime()`](Self::mtime).
98    #[must_use]
99    pub fn mtimensec(&self) -> u32 {
100        self.inode_ref().mtimensec()
101    }
102
103    /// Returns the sub-second nanosecond component of [`atime()`](Self::atime).
104    #[must_use]
105    pub fn atimensec(&self) -> u32 {
106        self.inode_ref().atimensec()
107    }
108
109    /// Returns the sub-second nanosecond component of [`ctime()`](Self::ctime).
110    #[must_use]
111    pub fn ctimensec(&self) -> u32 {
112        self.inode_ref().ctimensec()
113    }
114
115    /// Returns the sub-second nanosecond component of [`birthtime()`](Self::birthtime).
116    #[must_use]
117    pub fn birthnsec(&self) -> u32 {
118        self.inode_ref().birthnsec()
119    }
120
121    #[must_use]
122    pub fn uid(&self) -> u32 {
123        self.inode_ref().uid()
124    }
125
126    #[must_use]
127    pub fn gid(&self) -> u32 {
128        self.inode_ref().gid()
129    }
130
131    #[must_use]
132    pub fn is_compressed(&self) -> bool {
133        self.inode_ref().flags().is_compressed()
134    }
135
136    #[must_use]
137    pub fn is_empty(&self) -> bool {
138        self.len() == 0
139    }
140
141    /// Returns the file contents as a borrowed slice without a copy.
142    ///
143    /// This returns `Some` only when **all** of the following are true:
144    /// - The PFS was opened via [`open_slice()`][crate::open_slice] with an unencrypted image
145    /// - The file is not compressed
146    /// - The file's blocks are laid out contiguously in the image
147    ///
148    /// For compressed files, use [`pfsc::PfscImage`][crate::pfsc::PfscImage] instead.
149    /// When this returns `None`, use [`read_at()`](Self::read_at) as a fallback.
150    #[must_use]
151    pub fn as_slice(&self) -> Option<&'a [u8]> {
152        let data = self.pfs.data?;
153
154        if self.is_compressed() {
155            return None;
156        }
157
158        let inode = self.inode_ref();
159
160        if inode.size() == 0 {
161            return Some(&[]);
162        }
163
164        let (start_block, _) = inode.contiguous_blocks()?;
165        let block_size = self.pfs.block_size as u64;
166
167        let start = (start_block as u64) * block_size;
168        let end = start + inode.size();
169
170        data.get(start as usize..end as usize)
171    }
172
173    /// Reads file data at the given offset without modifying any cursor.
174    ///
175    /// This is the primary read method. It takes `&self` (not `&mut self`)
176    /// and requires no synchronization, making it safe to call from multiple
177    /// threads concurrently.
178    pub fn read_at(&self, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
179        pfs_read_at(&self.pfs, self.inode, offset, buf)
180    }
181
182    /// Creates a [`FileReader`] that implements [`Read`] and [`Seek`].
183    ///
184    /// This is useful when you need to pass a PFS file to APIs that expect
185    /// standard I/O traits (e.g. `io::copy`, decompressors, parsers).
186    ///
187    /// Each reader maintains its own cursor position. Multiple readers can
188    /// exist concurrently for the same file.
189    ///
190    /// # Example
191    ///
192    /// ```no_run
193    /// use std::io::Read;
194    ///
195    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
196    /// # let data = vec![];
197    /// let pfs = orbis_pfs::open_slice(&data, None)?;
198    /// let root = pfs.root().open()?;
199    ///
200    /// if let Some(orbis_pfs::directory::DirEntry::File(file)) = root.get(b"example.txt") {
201    ///     let mut reader = file.reader();
202    ///     let mut contents = String::new();
203    ///     reader.read_to_string(&mut contents)?;
204    /// }
205    /// # Ok(())
206    /// # }
207    /// ```
208    #[must_use]
209    pub fn reader(&self) -> FileReader<'a> {
210        FileReader {
211            file: self.clone(),
212            pos: 0,
213        }
214    }
215
216    /// Converts this file handle into a [`PfsFileImage`] for use as an
217    /// [`Image`] source (e.g. to open a nested PFS or wrap in
218    /// [`PfscImage`][crate::pfsc::PfscImage]).
219    pub fn into_image(self) -> PfsFileImage<'a> {
220        PfsFileImage {
221            pfs: self.pfs,
222            inode: self.inode,
223        }
224    }
225
226    fn inode_ref(&self) -> &Inode {
227        self.pfs.inode(self.inode)
228    }
229}
230
231impl<'a> Clone for File<'a> {
232    fn clone(&self) -> Self {
233        Self {
234            pfs: self.pfs.clone(),
235            inode: self.inode,
236        }
237    }
238}
239
240/// A cursor-based reader for a PFS [`File`], implementing [`Read`] and [`Seek`].
241///
242/// Created via [`File::reader()`].
243pub struct FileReader<'a> {
244    file: File<'a>,
245    pos: u64,
246}
247
248impl<'a> std::fmt::Debug for FileReader<'a> {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        f.debug_struct("FileReader")
251            .field("file", &self.file)
252            .field("pos", &self.pos)
253            .finish()
254    }
255}
256
257impl Read for FileReader<'_> {
258    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
259        let n = self.file.read_at(self.pos, buf)?;
260        self.pos += n as u64;
261        Ok(n)
262    }
263}
264
265impl Seek for FileReader<'_> {
266    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
267        let file_len = self.file.len();
268
269        let new_pos = match pos {
270            SeekFrom::Start(offset) => offset as i64,
271            SeekFrom::End(offset) => file_len as i64 + offset,
272            SeekFrom::Current(offset) => self.pos as i64 + offset,
273        };
274
275        if new_pos < 0 {
276            return Err(io::Error::new(
277                io::ErrorKind::InvalidInput,
278                "seek to a negative position",
279            ));
280        }
281
282        self.pos = new_pos as u64;
283        Ok(self.pos)
284    }
285}
286
287/// A file within a PFS, exposed as an [`Image`] for chaining.
288///
289/// This adapter maps logical file offsets through the inode's precomputed block
290/// map to physical offsets in the underlying PFS image. It is used to open
291/// nested PFS images (e.g. `pfs_image.dat` inside an outer PFS), optionally
292/// wrapped in [`PfscImage`][crate::pfsc::PfscImage] for decompression.
293///
294/// Created via [`File::into_image()`].
295#[derive(Clone)]
296pub struct PfsFileImage<'a> {
297    pfs: Arc<Pfs<'a>>,
298    inode: usize,
299}
300
301impl<'a> std::fmt::Debug for PfsFileImage<'a> {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        f.debug_struct("PfsFileImage")
304            .field("inode", &self.inode)
305            .field("len", &self.len())
306            .finish_non_exhaustive()
307    }
308}
309
310impl Image for PfsFileImage<'_> {
311    fn read_at(&self, offset: u64, output_buf: &mut [u8]) -> std::io::Result<usize> {
312        pfs_read_at(&self.pfs, self.inode, offset, output_buf)
313    }
314
315    fn len(&self) -> u64 {
316        self.pfs.inode(self.inode).size()
317    }
318}
319
320fn pfs_read_at(pfs: &Pfs<'_>, inode: usize, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
321    let file_size = pfs.inode(inode).size();
322
323    if buf.is_empty() || offset >= file_size {
324        return Ok(0);
325    }
326
327    let block_map = pfs.block_map(inode);
328    let block_size = pfs.block_size as u64;
329    let image = pfs.image();
330    let mut copied = 0usize;
331    let mut pos = offset;
332
333    loop {
334        let block_index = pos / block_size;
335        let offset_in_block = pos % block_size;
336
337        let block_num = match block_map.get(block_index as usize) {
338            Some(&v) => v,
339            None => {
340                return Err(Error::other(format!(
341                    "block #{} is not available",
342                    block_index
343                )));
344            }
345        };
346
347        let block_end = (block_index + 1) * block_size;
348        let remaining_in_block = (min(block_end, file_size) - pos) as usize;
349        let to_read = min(remaining_in_block, buf.len() - copied);
350
351        let phys_offset = (block_num as u64) * block_size + offset_in_block;
352
353        image.read_exact_at(phys_offset, &mut buf[copied..copied + to_read])?;
354
355        copied += to_read;
356        pos += to_read as u64;
357
358        if copied == buf.len() || pos >= file_size {
359            break Ok(copied);
360        }
361    }
362}