Skip to main content

rd_rds/
file.rs

1//! Bounded standalone RDS file reading.
2//!
3//! This module accepts an entire standalone RDS file and recognizes only the
4//! `X\n` XDR marker, gzip (`1f 8b`), xz (`fd 37 7a 58 5a 00`), bzip2
5//! (`BZh`), and zstd (`28 b5 2f fd`) envelopes. Compression support is
6//! controlled by the corresponding crate features. Input and decompressed data
7//! are capped at 256 MiB by default; [`ReadOptions`] can lower or raise those
8//! caps and configure
9//! decoder [`Limits`]. This is distinct from [`crate::parse`], which accepts
10//! only an already decompressed XDR stream.
11
12use std::{
13    fmt,
14    fs::File,
15    io::{self, Read},
16    path::{Path, PathBuf},
17};
18
19use crate::{Limits, RObject};
20
21const XDR_MARKER: &[u8; 2] = b"X\n";
22const ZSTD_MAGIC: &[u8; 4] = &[0x28, 0xb5, 0x2f, 0xfd];
23const DEFAULT_CAP: usize = 256 * 1024 * 1024;
24
25/// Options controlling standalone-file size limits and RDS decoding limits.
26#[derive(Debug, Clone, Copy)]
27pub struct ReadOptions {
28    limits: Limits,
29    max_compressed_bytes: usize,
30    max_decompressed_bytes: usize,
31}
32
33impl Default for ReadOptions {
34    fn default() -> Self {
35        Self {
36            limits: Limits::default(),
37            max_compressed_bytes: DEFAULT_CAP,
38            max_decompressed_bytes: DEFAULT_CAP,
39        }
40    }
41}
42
43impl ReadOptions {
44    pub fn limits(mut self, limits: Limits) -> Self {
45        self.limits = limits;
46        self
47    }
48
49    pub fn max_compressed_bytes(mut self, limit: usize) -> Self {
50        self.max_compressed_bytes = limit;
51        self
52    }
53
54    pub fn max_decompressed_bytes(mut self, limit: usize) -> Self {
55        self.max_decompressed_bytes = limit;
56        self
57    }
58}
59
60/// Compression envelopes recognized by standalone RDS reading.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum Compression {
64    Gzip,
65    Xz,
66    Bzip2,
67    Zstd,
68}
69
70impl fmt::Display for Compression {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        formatter.write_str(match self {
73            Self::Gzip => "gzip",
74            Self::Xz => "xz",
75            Self::Bzip2 => "bzip2",
76            Self::Zstd => "zstd",
77        })
78    }
79}
80
81/// Errors from bounded standalone RDS file reading.
82#[derive(Debug, thiserror::Error)]
83#[non_exhaustive]
84pub enum ReadError {
85    #[error("I/O error at {path}: {source}")]
86    Io {
87        path: PathBuf,
88        #[source]
89        source: io::Error,
90    },
91    #[error("unknown RDS envelope magic bytes {magic:02x?}")]
92    UnknownEnvelope { magic: Vec<u8> },
93    #[error("{format} support is disabled")]
94    CompressionDisabled { format: Compression },
95    #[error("{format} decompression failed: {source}")]
96    Decompression {
97        format: Compression,
98        #[source]
99        source: io::Error,
100    },
101    #[error("compressed input exceeds the {limit}-byte limit")]
102    CompressedSizeLimitExceeded { limit: usize },
103    #[error("decompressed input exceeds the {limit}-byte limit")]
104    DecompressedSizeLimitExceeded { limit: usize },
105    #[error(transparent)]
106    Decode(#[from] crate::Error),
107}
108
109/// Reads a standalone RDS file from disk.
110pub fn read(path: impl AsRef<Path>) -> Result<RObject, ReadError> {
111    read_with_options(path, &ReadOptions::default())
112}
113
114/// Reads a standalone RDS file from disk with explicit bounds.
115pub fn read_with_options(
116    path: impl AsRef<Path>,
117    options: &ReadOptions,
118) -> Result<RObject, ReadError> {
119    let path = path.as_ref();
120    let file = File::open(path).map_err(|source| ReadError::Io {
121        path: path.to_path_buf(),
122        source,
123    })?;
124    let mut bytes = Vec::new();
125    file.take(options.max_compressed_bytes.saturating_add(1) as u64)
126        .read_to_end(&mut bytes)
127        .map_err(|source| ReadError::Io {
128            path: path.to_path_buf(),
129            source,
130        })?;
131    if bytes.len() > options.max_compressed_bytes {
132        return Err(ReadError::CompressedSizeLimitExceeded {
133            limit: options.max_compressed_bytes,
134        });
135    }
136    from_bytes_with_options(&bytes, options)
137}
138
139/// Decodes a complete standalone RDS file held in memory.
140pub fn from_bytes(bytes: &[u8]) -> Result<RObject, ReadError> {
141    from_bytes_with_options(bytes, &ReadOptions::default())
142}
143
144/// Decodes a complete standalone RDS file held in memory with explicit bounds.
145pub fn from_bytes_with_options(bytes: &[u8], options: &ReadOptions) -> Result<RObject, ReadError> {
146    if bytes.len() > options.max_compressed_bytes {
147        return Err(ReadError::CompressedSizeLimitExceeded {
148            limit: options.max_compressed_bytes,
149        });
150    }
151
152    let format = if is_xdr_stream(bytes) {
153        None
154    } else if bytes.starts_with(&[0x1f, 0x8b]) {
155        Some(Compression::Gzip)
156    } else if bytes.starts_with(&[0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00]) {
157        Some(Compression::Xz)
158    } else if bytes.starts_with(b"BZh") {
159        Some(Compression::Bzip2)
160    } else if bytes.starts_with(ZSTD_MAGIC) {
161        Some(Compression::Zstd)
162    } else {
163        return Err(ReadError::UnknownEnvelope {
164            magic: bytes[..bytes.len().min(6)].to_vec(),
165        });
166    };
167
168    let decoded = match format {
169        None => {
170            if bytes.len() > options.max_decompressed_bytes {
171                return Err(ReadError::DecompressedSizeLimitExceeded {
172                    limit: options.max_decompressed_bytes,
173                });
174            }
175            bytes.to_vec()
176        }
177        Some(format) => decompress(bytes, format, options.max_decompressed_bytes)?,
178    };
179    Ok(crate::parse_with_limits(&decoded, options.limits)?)
180}
181
182/// Returns whether `bytes` begins with the decompressed XDR stream marker.
183pub fn is_xdr_stream(bytes: &[u8]) -> bool {
184    bytes.starts_with(XDR_MARKER)
185}
186
187fn decompress(bytes: &[u8], format: Compression, limit: usize) -> Result<Vec<u8>, ReadError> {
188    let output = match format {
189        Compression::Gzip => decompress_gzip(bytes, limit)?,
190        Compression::Xz => decompress_xz(bytes, limit)?,
191        Compression::Bzip2 => decompress_bzip2(bytes, limit)?,
192        Compression::Zstd => decompress_zstd(bytes, limit)?,
193    };
194    if output.len() > limit {
195        return Err(ReadError::DecompressedSizeLimitExceeded { limit });
196    }
197    Ok(output)
198}
199
200#[cfg(any(feature = "gzip", feature = "xz", feature = "bzip2", feature = "zstd"))]
201fn read_limited<R: std::io::Read>(
202    reader: R,
203    limit: usize,
204    format: Compression,
205) -> Result<Vec<u8>, ReadError> {
206    use std::io::Read as _;
207
208    let mut output = Vec::new();
209    reader
210        .take(limit.saturating_add(1) as u64)
211        .read_to_end(&mut output)
212        .map_err(|source| ReadError::Decompression { format, source })?;
213    Ok(output)
214}
215
216#[cfg(feature = "gzip")]
217fn decompress_gzip(bytes: &[u8], limit: usize) -> Result<Vec<u8>, ReadError> {
218    read_limited(
219        flate2::read::GzDecoder::new(bytes),
220        limit,
221        Compression::Gzip,
222    )
223}
224
225#[cfg(not(feature = "gzip"))]
226fn decompress_gzip(_: &[u8], _: usize) -> Result<Vec<u8>, ReadError> {
227    Err(ReadError::CompressionDisabled {
228        format: Compression::Gzip,
229    })
230}
231
232#[cfg(feature = "xz")]
233fn decompress_xz(bytes: &[u8], limit: usize) -> Result<Vec<u8>, ReadError> {
234    read_limited(xz2::read::XzDecoder::new(bytes), limit, Compression::Xz)
235}
236
237#[cfg(not(feature = "xz"))]
238fn decompress_xz(_: &[u8], _: usize) -> Result<Vec<u8>, ReadError> {
239    Err(ReadError::CompressionDisabled {
240        format: Compression::Xz,
241    })
242}
243
244#[cfg(feature = "bzip2")]
245fn decompress_bzip2(bytes: &[u8], limit: usize) -> Result<Vec<u8>, ReadError> {
246    read_limited(
247        bzip2::read::BzDecoder::new(bytes),
248        limit,
249        Compression::Bzip2,
250    )
251}
252
253#[cfg(not(feature = "bzip2"))]
254fn decompress_bzip2(_: &[u8], _: usize) -> Result<Vec<u8>, ReadError> {
255    Err(ReadError::CompressionDisabled {
256        format: Compression::Bzip2,
257    })
258}
259
260#[cfg(feature = "zstd")]
261fn decompress_zstd(bytes: &[u8], limit: usize) -> Result<Vec<u8>, ReadError> {
262    let mut source = bytes;
263    let decoder = ruzstd::decoding::StreamingDecoder::new(&mut source).map_err(|source| {
264        ReadError::Decompression {
265            format: Compression::Zstd,
266            source: io::Error::new(io::ErrorKind::InvalidData, source.to_string()),
267        }
268    })?;
269    read_limited(decoder, limit, Compression::Zstd)
270}
271
272#[cfg(not(feature = "zstd"))]
273fn decompress_zstd(_: &[u8], _: usize) -> Result<Vec<u8>, ReadError> {
274    Err(ReadError::CompressionDisabled {
275        format: Compression::Zstd,
276    })
277}