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, NativeEncodingPolicy, ParseOptions, 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    parse_options: ParseOptions,
29    max_compressed_bytes: usize,
30    max_decompressed_bytes: usize,
31}
32
33impl Default for ReadOptions {
34    fn default() -> Self {
35        Self {
36            parse_options: ParseOptions::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.parse_options = self.parse_options.limits(limits);
46        self
47    }
48
49    /// Replaces the complete decompressed-stream parsing configuration.
50    pub fn parse_options(mut self, options: ParseOptions) -> Self {
51        self.parse_options = options;
52        self
53    }
54
55    pub fn max_compressed_bytes(mut self, limit: usize) -> Self {
56        self.max_compressed_bytes = limit;
57        self
58    }
59
60    pub fn max_decompressed_bytes(mut self, limit: usize) -> Self {
61        self.max_decompressed_bytes = limit;
62        self
63    }
64
65    /// Selects the native-string policy used when the RDS header has no
66    /// encoding, which means format 2; retained `RStr` values are validated
67    /// when converted, while `SYMSXP` print names are converted during parsing.
68    pub fn native_encoding_policy(mut self, policy: NativeEncodingPolicy) -> Self {
69        self.parse_options = self.parse_options.native_encoding_policy(policy);
70        self
71    }
72}
73
74/// Compression envelopes recognized by standalone RDS reading.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum Compression {
78    Gzip,
79    Xz,
80    Bzip2,
81    Zstd,
82}
83
84impl fmt::Display for Compression {
85    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86        formatter.write_str(match self {
87            Self::Gzip => "gzip",
88            Self::Xz => "xz",
89            Self::Bzip2 => "bzip2",
90            Self::Zstd => "zstd",
91        })
92    }
93}
94
95/// Errors from bounded standalone RDS file reading.
96#[derive(Debug, thiserror::Error)]
97#[non_exhaustive]
98pub enum ReadError {
99    #[error("I/O error at {path}: {source}")]
100    Io {
101        path: PathBuf,
102        #[source]
103        source: io::Error,
104    },
105    #[error("unknown RDS envelope magic bytes {magic:02x?}")]
106    UnknownEnvelope { magic: Vec<u8> },
107    #[error("{format} support is disabled")]
108    CompressionDisabled { format: Compression },
109    #[error("{format} decompression failed: {source}")]
110    Decompression {
111        format: Compression,
112        #[source]
113        source: io::Error,
114    },
115    #[error("compressed input exceeds the {limit}-byte limit")]
116    CompressedSizeLimitExceeded { limit: usize },
117    #[error("decompressed input exceeds the {limit}-byte limit")]
118    DecompressedSizeLimitExceeded { limit: usize },
119    #[error(transparent)]
120    Decode(#[from] crate::Error),
121}
122
123/// Reads a standalone RDS file from disk.
124pub fn read(path: impl AsRef<Path>) -> Result<RObject, ReadError> {
125    read_with_options(path, &ReadOptions::default())
126}
127
128/// Reads a standalone RDS file from disk with explicit bounds.
129pub fn read_with_options(
130    path: impl AsRef<Path>,
131    options: &ReadOptions,
132) -> Result<RObject, ReadError> {
133    let path = path.as_ref();
134    let file = File::open(path).map_err(|source| ReadError::Io {
135        path: path.to_path_buf(),
136        source,
137    })?;
138    let mut bytes = Vec::new();
139    file.take(options.max_compressed_bytes.saturating_add(1) as u64)
140        .read_to_end(&mut bytes)
141        .map_err(|source| ReadError::Io {
142            path: path.to_path_buf(),
143            source,
144        })?;
145    if bytes.len() > options.max_compressed_bytes {
146        return Err(ReadError::CompressedSizeLimitExceeded {
147            limit: options.max_compressed_bytes,
148        });
149    }
150    from_bytes_with_options(&bytes, options)
151}
152
153/// Decodes a complete standalone RDS file held in memory.
154pub fn from_bytes(bytes: &[u8]) -> Result<RObject, ReadError> {
155    from_bytes_with_options(bytes, &ReadOptions::default())
156}
157
158/// Decodes a complete standalone RDS file held in memory with explicit bounds.
159pub fn from_bytes_with_options(bytes: &[u8], options: &ReadOptions) -> Result<RObject, ReadError> {
160    if bytes.len() > options.max_compressed_bytes {
161        return Err(ReadError::CompressedSizeLimitExceeded {
162            limit: options.max_compressed_bytes,
163        });
164    }
165
166    let format = if is_xdr_stream(bytes) {
167        None
168    } else if bytes.starts_with(&[0x1f, 0x8b]) {
169        Some(Compression::Gzip)
170    } else if bytes.starts_with(&[0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00]) {
171        Some(Compression::Xz)
172    } else if bytes.starts_with(b"BZh") {
173        Some(Compression::Bzip2)
174    } else if bytes.starts_with(ZSTD_MAGIC) {
175        Some(Compression::Zstd)
176    } else {
177        return Err(ReadError::UnknownEnvelope {
178            magic: bytes[..bytes.len().min(6)].to_vec(),
179        });
180    };
181
182    let decoded = match format {
183        None => {
184            if bytes.len() > options.max_decompressed_bytes {
185                return Err(ReadError::DecompressedSizeLimitExceeded {
186                    limit: options.max_decompressed_bytes,
187                });
188            }
189            bytes.to_vec()
190        }
191        Some(format) => decompress(bytes, format, options.max_decompressed_bytes)?,
192    };
193    Ok(crate::parse_with_options(&decoded, options.parse_options)?)
194}
195
196/// Returns whether `bytes` begins with the decompressed XDR stream marker.
197pub fn is_xdr_stream(bytes: &[u8]) -> bool {
198    bytes.starts_with(XDR_MARKER)
199}
200
201fn decompress(bytes: &[u8], format: Compression, limit: usize) -> Result<Vec<u8>, ReadError> {
202    let output = match format {
203        Compression::Gzip => decompress_gzip(bytes, limit)?,
204        Compression::Xz => decompress_xz(bytes, limit)?,
205        Compression::Bzip2 => decompress_bzip2(bytes, limit)?,
206        Compression::Zstd => decompress_zstd(bytes, limit)?,
207    };
208    if output.len() > limit {
209        return Err(ReadError::DecompressedSizeLimitExceeded { limit });
210    }
211    Ok(output)
212}
213
214#[cfg(any(feature = "gzip", feature = "xz", feature = "bzip2", feature = "zstd"))]
215fn read_limited<R: std::io::Read>(
216    reader: R,
217    limit: usize,
218    format: Compression,
219) -> Result<Vec<u8>, ReadError> {
220    use std::io::Read as _;
221
222    let mut output = Vec::new();
223    reader
224        .take(limit.saturating_add(1) as u64)
225        .read_to_end(&mut output)
226        .map_err(|source| ReadError::Decompression { format, source })?;
227    Ok(output)
228}
229
230#[cfg(feature = "gzip")]
231fn decompress_gzip(bytes: &[u8], limit: usize) -> Result<Vec<u8>, ReadError> {
232    read_limited(
233        flate2::read::GzDecoder::new(bytes),
234        limit,
235        Compression::Gzip,
236    )
237}
238
239#[cfg(not(feature = "gzip"))]
240fn decompress_gzip(_: &[u8], _: usize) -> Result<Vec<u8>, ReadError> {
241    Err(ReadError::CompressionDisabled {
242        format: Compression::Gzip,
243    })
244}
245
246#[cfg(feature = "xz")]
247fn decompress_xz(bytes: &[u8], limit: usize) -> Result<Vec<u8>, ReadError> {
248    read_limited(
249        lzma_rust2::XzReader::new(bytes, false),
250        limit,
251        Compression::Xz,
252    )
253}
254
255#[cfg(not(feature = "xz"))]
256fn decompress_xz(_: &[u8], _: usize) -> Result<Vec<u8>, ReadError> {
257    Err(ReadError::CompressionDisabled {
258        format: Compression::Xz,
259    })
260}
261
262#[cfg(feature = "bzip2")]
263fn decompress_bzip2(bytes: &[u8], limit: usize) -> Result<Vec<u8>, ReadError> {
264    read_limited(
265        bzip2::read::BzDecoder::new(bytes),
266        limit,
267        Compression::Bzip2,
268    )
269}
270
271#[cfg(not(feature = "bzip2"))]
272fn decompress_bzip2(_: &[u8], _: usize) -> Result<Vec<u8>, ReadError> {
273    Err(ReadError::CompressionDisabled {
274        format: Compression::Bzip2,
275    })
276}
277
278#[cfg(feature = "zstd")]
279fn decompress_zstd(bytes: &[u8], limit: usize) -> Result<Vec<u8>, ReadError> {
280    let mut source = bytes;
281    let decoder = ruzstd::decoding::StreamingDecoder::new(&mut source).map_err(|source| {
282        ReadError::Decompression {
283            format: Compression::Zstd,
284            source: io::Error::new(io::ErrorKind::InvalidData, source.to_string()),
285        }
286    })?;
287    read_limited(decoder, limit, Compression::Zstd)
288}
289
290#[cfg(not(feature = "zstd"))]
291fn decompress_zstd(_: &[u8], _: usize) -> Result<Vec<u8>, ReadError> {
292    Err(ReadError::CompressionDisabled {
293        format: Compression::Zstd,
294    })
295}