Skip to main content

rd_helpdb/
rds.rs

1//! Standalone `.rds` file reading and `.rdb` record decoding.
2//!
3//! Both entry points ultimately hand a decompressed byte buffer to
4//! `rd_rds::parse`; the difference is the compression envelope:
5//!
6//! - A standalone `.rds` file (`aliases.rds`, `Meta/hsearch.rds`, `<pkg>.rdx`)
7//!   is decoded by [`rd_rds::file`], which handles the bounded `"X\n"`, gzip,
8//!   xz, bzip2, and zstd envelope layer.
9//! - A `.rdb` record is framed as a 4-byte big-endian uncompressed-size
10//!   prefix followed by a raw zlib deflate stream (no gzip wrapper).
11
12use std::{io::Read, path::Path};
13
14use flate2::read::ZlibDecoder;
15
16use crate::Error;
17
18/// Reads a standalone (possibly compressed) `.rds` file into an
19/// [`rd_rds::RObject`].
20pub fn read_rds_file(path: impl AsRef<Path>) -> Result<rd_rds::RObject, Error> {
21    let path = path.as_ref();
22    rd_rds::file::read(path).map_err(|error| match error {
23        rd_rds::file::ReadError::Io { path, source } => Error::io(path, source),
24        rd_rds::file::ReadError::UnknownEnvelope { magic } => Error::UnsupportedCompression {
25            path: path.to_path_buf(),
26            magic,
27        },
28        rd_rds::file::ReadError::Decode(error) => Error::Rds(error),
29        error => Error::RdsFile(error),
30    })
31}
32
33/// Decodes a single `.rdb` record: `bytes` is the exact `(offset, length)`
34/// slice a `.rdx` index entry points at, i.e. a 4-byte big-endian
35/// uncompressed-size prefix followed by a raw zlib deflate stream. The
36/// decompressed size is checked against the prefix before parsing.
37pub fn decode_rdb_record(bytes: &[u8]) -> Result<rd_rds::RObject, Error> {
38    if bytes.len() < 4 {
39        // Too short to even hold the 4-byte length prefix.
40        return Err(Error::RecordSizeMismatch {
41            expected: 4,
42            actual: bytes.len(),
43        });
44    }
45    let (prefix, payload) = bytes.split_at(4);
46    let declared_len =
47        u32::from_be_bytes(prefix.try_into().expect("split_at(4) yields 4 bytes")) as usize;
48
49    let mut decoder = ZlibDecoder::new(payload);
50    let mut decompressed = Vec::new();
51    decoder.read_to_end(&mut decompressed).map_err(|err| {
52        Error::MalformedIndex(format!("zlib decompression of .rdb record failed: {err}"))
53    })?;
54    if decompressed.len() != declared_len {
55        return Err(Error::RecordSizeMismatch {
56            expected: declared_len,
57            actual: decompressed.len(),
58        });
59    }
60
61    Ok(rd_rds::parse(&decompressed)?)
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn maps_unknown_envelope_to_existing_error() {
70        let path = std::env::temp_dir().join(format!(
71            "rd-helpdb-unknown-envelope-{}.rds",
72            std::process::id()
73        ));
74        std::fs::write(&path, b"A\nunknown").expect("write unknown envelope");
75        let err = read_rds_file(&path).unwrap_err();
76        let _ = std::fs::remove_file(&path);
77        assert!(matches!(
78            err,
79            Error::UnsupportedCompression { magic, .. } if magic == b"A\nunkn"
80        ));
81    }
82
83    #[test]
84    fn rejects_short_records() {
85        let err = decode_rdb_record(&[0, 1, 2]).unwrap_err();
86        assert!(matches!(
87            err,
88            Error::RecordSizeMismatch {
89                expected: 4,
90                actual: 3
91            }
92        ));
93    }
94}