Skip to main content

orbis_pfs/
lib.rs

1//! A library for reading PlayStation 4 PFS (PlayStation File System) images.
2//!
3//! This crate provides functionality to parse and read files from PFS images,
4//! which are used by the PlayStation 4 to store game data and other content.
5//!
6//! # Features
7//!
8//! - Parse PFS image headers and metadata
9//! - Read files and directories from PFS images
10//! - Support for both encrypted and unencrypted PFS images
11//! - Support for compressed files (PFSC format)
12//! - Thread-safe: all read operations use positional I/O (`read_at`) —
13//!   no shared mutable cursor, no locks in the read path
14//!
15//! # Example
16//!
17//! ```no_run
18//! // Open a PFS image from a byte slice (e.g. memory-mapped file)
19//! let data = std::fs::read("image.pfs").unwrap();
20//! let pfs = orbis_pfs::open_slice(&data, None).unwrap();
21//!
22//! // Access the root directory
23//! let root = pfs.root();
24//! ```
25//!
26//! # References
27//!
28//! - [PS4 Developer Wiki - PFS](https://www.psdevwiki.com/ps4/PFS)
29
30use crate::header::Mode;
31
32use self::directory::Directory;
33use self::header::PfsHeader;
34use self::inode::Inode;
35use aes::Aes128;
36use aes::cipher::KeyInit;
37use snafu::{OptionExt, ResultExt, Snafu, ensure};
38use std::sync::Arc;
39use xts_mode::Xts128;
40
41pub mod directory;
42pub mod file;
43pub mod header;
44pub mod image;
45pub mod inode;
46pub mod pfsc;
47
48/// Shared errors for PFS open operations.
49///
50/// These errors can occur in both [`open_slice()`] and [`open_image()`] during
51/// the common phase: validating the header, reading inodes, and precomputing
52/// block maps.
53#[derive(Debug, Snafu)]
54#[non_exhaustive]
55pub enum OpenError {
56    #[snafu(display("invalid block size"))]
57    InvalidBlockSize,
58
59    #[snafu(display("cannot parse inode"))]
60    ParseInodeFailed { source: inode::FromRawError },
61
62    #[snafu(display("cannot read block #{block}"))]
63    ReadBlockFailed { block: u32, source: std::io::Error },
64
65    #[snafu(display("invalid super-root"))]
66    InvalidSuperRoot,
67
68    #[snafu(display("cannot load block map for inode #{inode}"))]
69    LoadBlockMapFailed {
70        inode: usize,
71        source: inode::LoadBlocksError,
72    },
73}
74
75/// Errors for [`open_slice()`].
76#[derive(Debug, Snafu)]
77#[snafu(module)]
78#[non_exhaustive]
79pub enum OpenSliceError {
80    #[snafu(display("cannot parse header"))]
81    ReadHeaderFailed { source: header::ReadError },
82
83    #[snafu(display("block size too small for encryption"))]
84    EncryptionBlockSizeTooSmall,
85
86    #[snafu(display("encryption required but no EKPFS is provided"))]
87    EmptyEkpfs,
88
89    #[snafu(transparent)]
90    Open { source: OpenError },
91}
92
93/// Errors for [`open_image()`].
94#[derive(Debug, Snafu)]
95#[snafu(module)]
96#[non_exhaustive]
97pub enum OpenImageError {
98    #[snafu(display("cannot read header from image"))]
99    ReadHeaderIoFailed { source: std::io::Error },
100
101    #[snafu(display("cannot parse header"))]
102    ReadHeaderFailed { source: header::ReadError },
103
104    #[snafu(display("unsupported mode: {mode}"))]
105    UnsupportedMode { mode: Mode },
106
107    #[snafu(transparent)]
108    Open { source: OpenError },
109}
110
111/// Represents a loaded PFS.
112///
113/// This type is `Send + Sync` and can be shared across threads via [`Arc`].
114/// All read operations use positional I/O, so concurrent reads from multiple
115/// threads do not require synchronization.
116#[must_use]
117pub struct Pfs<'a> {
118    image: Box<dyn image::Image + 'a>,
119    inodes: Vec<Inode>,
120    /// Precomputed block maps: `block_maps[inode_index]` gives the
121    /// logical-block -> physical-block mapping for that inode.
122    block_maps: Vec<Vec<u32>>,
123    root: usize,
124    block_size: u32,
125    /// Backing data for unencrypted, slice-backed images (from [`open_slice()`]).
126    /// Enables zero-copy file access via [`file::File::as_slice()`].
127    data: Option<&'a [u8]>,
128}
129
130// SAFETY: All fields are Send + Sync:
131// - Box<dyn Image + 'a>: Image requires Send + Sync
132// - Vec<Inode>: Inode contains only Copy/primitive types
133// - Vec<Vec<u32>>, usize, u32: trivially Send + Sync
134// - Option<&'a [u8]>: &[u8] is Send + Sync
135unsafe impl Send for Pfs<'_> {}
136unsafe impl Sync for Pfs<'_> {}
137
138impl<'a> std::fmt::Debug for Pfs<'a> {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct("Pfs")
141            .field("inode_count", &self.inodes.len())
142            .field("root", &self.root)
143            .field("block_size", &self.block_size)
144            .field("slice_backed", &self.data.is_some())
145            .finish_non_exhaustive()
146    }
147}
148
149impl<'a> Pfs<'a> {
150    /// Returns the number of inodes in this PFS.
151    ///
152    /// This represents the total number of files and directories in the filesystem.
153    #[must_use]
154    pub fn inode_count(&self) -> usize {
155        self.inodes.len()
156    }
157
158    /// Returns the root directory of this PFS.
159    ///
160    /// # Example
161    ///
162    /// ```no_run
163    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
164    /// let data = std::fs::read("image.pfs")?;
165    /// let pfs = orbis_pfs::open_slice(&data, None)?;
166    ///
167    /// // Get the root directory
168    /// let root = pfs.root();
169    ///
170    /// // Open and iterate over entries
171    /// for (name, entry) in root.open()? {
172    ///     println!("Entry: {:?}", String::from_utf8_lossy(&name));
173    /// }
174    /// # Ok(())
175    /// # }
176    /// ```
177    pub fn root(self: &Arc<Self>) -> Directory<'a> {
178        Directory::new(self.clone(), self.root)
179    }
180
181    /// Returns the block size used by this PFS.
182    #[must_use]
183    pub fn block_size(&self) -> u32 {
184        self.block_size
185    }
186
187    // --- Internal accessors for File / Directory / PfsFileImage ---
188
189    pub(crate) fn image(&self) -> &dyn image::Image {
190        &*self.image
191    }
192
193    pub(crate) fn inode(&self, index: usize) -> &Inode {
194        &self.inodes[index]
195    }
196
197    pub(crate) fn block_map(&self, inode: usize) -> &[u32] {
198        &self.block_maps[inode]
199    }
200}
201
202/// Opens a PFS image for reading from a byte slice.
203///
204/// This is the primary entry point when the image data is already in memory.
205///
206/// For unencrypted images, this avoids intermediate buffer allocations during
207/// header and inode parsing by reading directly from the slice, and enables
208/// zero-copy file access via [`file::File::as_slice()`].
209///
210/// # Arguments
211///
212/// * `data` - The PFS image data as a byte slice
213/// * `ekpfs` - The EKPFS key for encrypted images, or `None` for unencrypted images
214///
215/// # Returns
216///
217/// Returns a thread-safe, reference-counted [`Pfs`] handle on success.
218///
219/// # Errors
220///
221/// Returns an [`OpenSliceError`] if:
222/// - The image header is invalid
223/// - The image is encrypted but no key is provided
224/// - The block structure is invalid
225///
226/// # Example
227///
228/// ```no_run
229/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
230/// let data = std::fs::read("image.pfs")?;
231/// let pfs = orbis_pfs::open_slice(&data, None)?;
232/// println!("Opened PFS with {} inodes", pfs.inode_count());
233/// # Ok(())
234/// # }
235/// ```
236pub fn open_slice<'a>(
237    data: &'a [u8],
238    ekpfs: Option<&[u8]>,
239) -> Result<Arc<Pfs<'a>>, OpenSliceError> {
240    // Parse header directly from the slice.
241    let header = PfsHeader::from_bytes(data).context(open_slice_error::ReadHeaderFailedSnafu)?;
242
243    // Build the appropriate Image backend and determine zero-copy backing data.
244    let (image, backing_data): (Box<dyn image::Image + 'a>, Option<&'a [u8]>) =
245        if header.mode().is_encrypted() {
246            ensure!(
247                (header.block_size() as usize) >= image::XTS_BLOCK_SIZE,
248                open_slice_error::EncryptionBlockSizeTooSmallSnafu
249            );
250
251            let ekpfs_bytes = ekpfs.context(open_slice_error::EmptyEkpfsSnafu)?;
252
253            let key_seed = header.key_seed();
254            let (data_key, tweak_key) = image::get_xts_keys(ekpfs_bytes, key_seed);
255            let cipher_1 = Aes128::new((&data_key).into());
256            let cipher_2 = Aes128::new((&tweak_key).into());
257
258            let enc = image::EncryptedSlice::new(
259                data,
260                Xts128::<Aes128>::new(cipher_1, cipher_2),
261                (header.block_size() as usize) / image::XTS_BLOCK_SIZE,
262            );
263
264            (Box::new(enc), None)
265        } else {
266            (Box::new(image::UnencryptedSlice::new(data)), Some(data))
267        };
268
269    Ok(open_inner(image, &header, backing_data)?)
270}
271
272/// Opens a PFS image for reading from any [`Image`](image::Image) implementation.
273///
274/// This is used when the PFS image is behind a transformation layer (e.g.
275/// a file within another PFS, optionally PFSC-compressed). The image is read
276/// entirely through [`Image::read_at()`](image::Image::read_at).
277///
278/// # Arguments
279///
280/// * `image` - An [`Image`](image::Image) providing positional read access to the PFS data
281///
282/// # Returns
283///
284/// Returns a thread-safe, reference-counted [`Pfs`] handle on success.
285///
286/// # Errors
287///
288/// Returns an [`OpenImageError`] if the image header or block structure is invalid.
289///
290/// # Example
291///
292/// ```no_run
293/// use orbis_pfs::image::Image;
294///
295/// # fn open_inner(image: impl Image) -> Result<(), Box<dyn std::error::Error>> {
296/// let pfs = orbis_pfs::open_image(image)?;
297/// println!("Opened PFS with {} inodes", pfs.inode_count());
298/// # Ok(())
299/// # }
300/// ```
301pub fn open_image<'a>(image: impl image::Image + 'a) -> Result<Arc<Pfs<'a>>, OpenImageError> {
302    // Read header via positional read.
303    let mut header_buf = [0u8; header::HEADER_SIZE];
304
305    image
306        .read_exact_at(0, &mut header_buf)
307        .context(open_image_error::ReadHeaderIoFailedSnafu)?;
308
309    let header =
310        PfsHeader::from_bytes(&header_buf).context(open_image_error::ReadHeaderFailedSnafu)?;
311
312    ensure!(
313        !header.mode().is_encrypted(),
314        open_image_error::UnsupportedModeSnafu {
315            mode: header.mode()
316        }
317    );
318
319    Ok(open_inner(Box::new(image), &header, None)?)
320}
321
322/// Shared implementation for [`open_slice()`] and [`open_image()`].
323///
324/// Validates the header fields, reads inodes, precomputes block maps, and
325/// constructs the [`Pfs`].
326fn open_inner<'a>(
327    image: Box<dyn image::Image + 'a>,
328    header: &PfsHeader,
329    data: Option<&'a [u8]>,
330) -> Result<Arc<Pfs<'a>>, OpenError> {
331    let mode = header.mode();
332    let block_size = header.block_size();
333    let inode_count = header.inode_count();
334    let inode_block_count = header.inode_block_count();
335    let super_root = header.super_root_inode();
336
337    ensure!(
338        block_size > 0 && block_size.is_power_of_two(),
339        InvalidBlockSizeSnafu
340    );
341
342    // Read and parse all inodes.
343    let mut inodes: Vec<Inode> = Vec::with_capacity(inode_count);
344    let mut block_buf = vec![0; block_size as usize];
345
346    for block_num in 0..inode_block_count {
347        let offset = (block_size as u64) + (block_num as u64) * (block_size as u64);
348
349        image
350            .read_exact_at(offset, &mut block_buf)
351            .context(ReadBlockFailedSnafu { block: block_num })?;
352
353        if parse_inodes_from_block(&block_buf, mode, &mut inodes, inode_count)? {
354            break;
355        }
356    }
357
358    ensure!(super_root < inodes.len(), InvalidSuperRootSnafu);
359
360    // Precompute block maps for all inodes.
361    let block_maps = precompute_block_maps(&inodes, image.as_ref(), block_size)?;
362
363    Ok(Arc::new(Pfs {
364        image,
365        inodes,
366        block_maps,
367        root: super_root,
368        block_size,
369        data,
370    }))
371}
372
373/// Precomputes block maps for all inodes.
374fn precompute_block_maps(
375    inodes: &[Inode],
376    image: &dyn image::Image,
377    block_size: u32,
378) -> Result<Vec<Vec<u32>>, OpenError> {
379    let mut maps = Vec::with_capacity(inodes.len());
380
381    for (i, inode) in inodes.iter().enumerate() {
382        let block_map = inode
383            .load_block_map(image, block_size)
384            .context(LoadBlockMapFailedSnafu { inode: i })?;
385        maps.push(block_map);
386    }
387
388    Ok(maps)
389}
390
391/// Parses inodes from a single block of data.
392///
393/// Returns `true` if all expected inodes have been parsed, `false` if more blocks are
394/// needed (the current block was exhausted before reaching `inode_count`).
395fn parse_inodes_from_block(
396    block_data: &[u8],
397    mode: Mode,
398    inodes: &mut Vec<Inode>,
399    inode_count: usize,
400) -> Result<bool, OpenError> {
401    let reader = if mode.is_signed() {
402        Inode::from_raw32_signed
403    } else {
404        Inode::from_raw32_unsigned
405    };
406
407    let mut src = block_data;
408
409    while inodes.len() < inode_count {
410        let inode = match reader(inodes.len(), &mut src) {
411            Ok(v) => v,
412            Err(inode::FromRawError::TooSmall) => {
413                return Ok(false);
414            }
415            err => err.context(ParseInodeFailedSnafu)?,
416        };
417
418        inodes.push(inode);
419    }
420
421    Ok(true)
422}