Skip to main content

orbis_pfs/
pfsc.rs

1use flate2::FlushDecompress;
2use std::cmp::min;
3use std::io::{self, ErrorKind};
4use zerocopy::{
5    FromBytes, Immutable, IntoBytes, KnownLayout,
6    little_endian::{U32, U64},
7};
8
9use crate::image::Image;
10use snafu::{Snafu, ensure};
11
12/// PFSC header (48 bytes).
13#[derive(Clone, Copy, FromBytes, KnownLayout, Immutable)]
14#[repr(C)]
15struct PfscHeader {
16    /// 0x00: Magic bytes "PFSC"
17    magic: [u8; 4],
18    /// 0x04: Unknown
19    _unknown_04: U32,
20    /// 0x08: Unknown
21    _unknown_08: U32,
22    /// 0x0C: Compressed block size
23    block_size: U32,
24    /// 0x10: Original block size
25    block_size2: U64,
26    /// 0x18: Offset to block mapping table
27    block_offsets: U64,
28    /// 0x20: Unknown
29    _unknown_20: U64,
30    /// 0x28: Original (uncompressed) data length
31    data_length: U64,
32}
33
34const PFSC_MAGIC: &[u8; 4] = b"PFSC";
35
36/// Errors when opening a PFSC compressed file.
37#[derive(Debug, Snafu)]
38#[non_exhaustive]
39pub enum OpenError {
40    #[snafu(display("i/o failed"))]
41    IoFailed { source: std::io::Error },
42
43    #[snafu(display("data too small"))]
44    TooSmall,
45
46    #[snafu(display("invalid magic"))]
47    InvalidMagic,
48
49    #[snafu(display("cannot read block mapping"))]
50    ReadBlockMappingFailed { source: std::io::Error },
51}
52
53/// A decompressing [`Image`] adapter for PFSC-compressed files.
54///
55/// Each PFSC block is independently compressed, so `read_at` at any offset
56/// only needs to decompress one block (or two if straddling a boundary).
57/// All state is local to each call — no shared mutable state, naturally
58/// thread-safe.
59///
60/// Created via [`PfscImage::open()`].
61///
62/// # Example
63///
64/// ```no_run
65/// use orbis_pfs::image::Image;
66/// use orbis_pfs::pfsc::PfscImage;
67///
68/// # fn example(source: impl Image) -> Result<(), Box<dyn std::error::Error>> {
69/// let pfsc = PfscImage::open(source)?;
70/// let pfs = orbis_pfs::open_image(pfsc)?;
71/// # Ok(())
72/// # }
73/// ```
74pub struct PfscImage<I: Image> {
75    source: I,
76    block_size: u32,
77    original_block_size: u64,
78    compressed_blocks: Vec<u64>,
79    original_size: u64,
80}
81
82impl<I: Image> std::fmt::Debug for PfscImage<I> {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.debug_struct("PfscImage")
85            .field("block_size", &self.block_size)
86            .field("original_block_size", &self.original_block_size)
87            .field("original_size", &self.original_size)
88            .finish_non_exhaustive()
89    }
90}
91
92impl<I: Image> PfscImage<I> {
93    /// Opens a PFSC-compressed image from an underlying [`Image`] source.
94    ///
95    /// Reads the PFSC header and block offset table at construction time.
96    pub fn open(source: I) -> Result<Self, OpenError> {
97        // Read header.
98        let mut header_buf = [0u8; size_of::<PfscHeader>()];
99
100        source.read_exact_at(0, &mut header_buf).map_err(|e| {
101            if e.kind() == ErrorKind::UnexpectedEof {
102                OpenError::TooSmall
103            } else {
104                OpenError::IoFailed { source: e }
105            }
106        })?;
107
108        let header =
109            PfscHeader::read_from_bytes(&header_buf).expect("header buffer is correctly sized");
110
111        ensure!(&header.magic == PFSC_MAGIC, InvalidMagicSnafu);
112
113        let block_size = header.block_size.get();
114        let original_block_size = header.block_size2.get();
115        let block_offsets_offset = header.block_offsets.get();
116        let original_size = header.data_length.get();
117
118        // Read block offsets.
119        let original_block_count = original_size / original_block_size + 1;
120        let mut compressed_blocks: Vec<u64> = vec![0; original_block_count as usize];
121
122        source
123            .read_exact_at(
124                block_offsets_offset,
125                compressed_blocks.as_mut_slice().as_mut_bytes(),
126            )
127            .map_err(|e| OpenError::ReadBlockMappingFailed { source: e })?;
128
129        Ok(Self {
130            source,
131            block_size,
132            original_block_size,
133            compressed_blocks,
134            original_size,
135        })
136    }
137
138    /// Returns the decompressed size of the file.
139    #[must_use]
140    pub fn decompressed_len(&self) -> u64 {
141        self.original_size
142    }
143
144    /// Decompresses a single PFSC block into `out`.
145    ///
146    /// `out` must be exactly `self.block_size` bytes.
147    fn decompress_block(&self, num: u64, out: &mut [u8]) -> io::Result<()> {
148        debug_assert_eq!(out.len(), self.block_size as usize);
149
150        // Get compressed block range.
151        let end = match self.compressed_blocks.get(num as usize + 1) {
152            Some(&v) => v,
153            None => return Err(io::Error::from(ErrorKind::InvalidInput)),
154        };
155
156        let offset = self.compressed_blocks[num as usize];
157        let size = end - offset;
158
159        match size.cmp(&self.original_block_size) {
160            std::cmp::Ordering::Less => {
161                // Read compressed data.
162                let mut compressed_buf = vec![0u8; size as usize];
163                self.source.read_exact_at(offset, &mut compressed_buf)?;
164
165                // Decompress.
166                let mut deflate = flate2::Decompress::new(true);
167
168                let status = match deflate.decompress(&compressed_buf, out, FlushDecompress::Finish)
169                {
170                    Ok(v) => v,
171                    Err(e) => return Err(io::Error::other(e)),
172                };
173
174                if status != flate2::Status::StreamEnd || deflate.total_out() as usize != out.len()
175                {
176                    return Err(io::Error::other(format!(
177                        "invalid data on PFSC block #{}",
178                        num
179                    )));
180                }
181            }
182
183            std::cmp::Ordering::Equal => {
184                // Uncompressed block — read directly.
185                self.source.read_exact_at(offset, out)?;
186            }
187
188            std::cmp::Ordering::Greater => {
189                // Sparse / zero block.
190                out.fill(0);
191            }
192        }
193
194        Ok(())
195    }
196}
197
198impl<I: Image> Image for PfscImage<I> {
199    fn read_at(&self, offset: u64, buf: &mut [u8]) -> io::Result<usize> {
200        if buf.is_empty() || offset >= self.original_size {
201            return Ok(0);
202        }
203
204        let block_size = self.block_size as u64;
205        let mut copied = 0usize;
206        let mut pos = offset;
207        let mut block_buf = vec![0u8; self.block_size as usize];
208
209        while copied < buf.len() && pos < self.original_size {
210            // Determine which PFSC block and offset within it.
211            let block_index = pos / block_size;
212            let offset_in_block = (pos % block_size) as usize;
213
214            // Decompress the block.
215            self.decompress_block(block_index, &mut block_buf)?;
216
217            // Trim the last block if it extends past the original size.
218            let block_end = (block_index + 1) * block_size;
219            let valid_in_block = if block_end > self.original_size {
220                (self.original_size - block_index * block_size) as usize
221            } else {
222                self.block_size as usize
223            };
224
225            // Copy the relevant portion to the output buffer.
226            let available = valid_in_block - offset_in_block;
227            let n = min(available, buf.len() - copied);
228
229            buf[copied..copied + n]
230                .copy_from_slice(&block_buf[offset_in_block..offset_in_block + n]);
231
232            copied += n;
233            pos += n as u64;
234        }
235
236        Ok(copied)
237    }
238
239    fn len(&self) -> u64 {
240        self.original_size
241    }
242}