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::path::Path;
13
14use crate::Error;
15
16/// Reads a standalone (possibly compressed) `.rds` file into an
17/// [`rd_rds::RObject`].
18pub fn read_rds_file(path: impl AsRef<Path>) -> Result<rd_rds::RObject, Error> {
19    let path = path.as_ref();
20    rd_rds::file::read(path).map_err(|error| map_file_error(path, error))
21}
22
23pub(crate) fn map_file_error(path: &Path, error: rd_rds::file::ReadError) -> Error {
24    match error {
25        rd_rds::file::ReadError::Io { path, source } => Error::io(path, source),
26        rd_rds::file::ReadError::UnknownEnvelope { magic } => Error::UnsupportedCompression {
27            path: path.to_path_buf(),
28            magic,
29        },
30        rd_rds::file::ReadError::Decode(error) => Error::Rds(error),
31        error => Error::RdsFile(error),
32    }
33}
34
35/// Decodes a single `.rdb` record: `bytes` is the exact `(offset, length)`
36/// slice a `.rdx` index entry points at, i.e. a 4-byte big-endian
37/// uncompressed-size prefix followed by a raw zlib deflate stream. The
38/// bounded `rd-rds::lazyload` record decoder checks the decompressed size,
39/// input size, stream completeness, and trailing bytes before parsing.
40pub fn decode_rdb_record(bytes: &[u8]) -> Result<rd_rds::RObject, Error> {
41    let decompressed = rd_rds::lazyload::decode_stored_record(
42        bytes,
43        rd_rds::lazyload::Compression::Zlib,
44        rd_rds::lazyload::Options::default(),
45    )
46    .map_err(|error| map_record_decode_error(error, bytes.len()))?;
47    Ok(rd_rds::parse(&decompressed)?)
48}
49
50fn map_record_decode_error(error: rd_rds::lazyload::Error, stored_len: usize) -> Error {
51    match error {
52        rd_rds::lazyload::Error::StoredRecordSizeLimitExceeded { limit } => {
53            Error::StoredRecordSizeLimitExceeded { limit }
54        }
55        rd_rds::lazyload::Error::DecompressedRecordSizeLimitExceeded { limit } => {
56            Error::DecompressedRecordSizeLimitExceeded { limit }
57        }
58        rd_rds::lazyload::Error::RecordLengthPrefixMissing => Error::RecordSizeMismatch {
59            expected: 4,
60            actual: stored_len,
61        },
62        rd_rds::lazyload::Error::RecordSizeMismatch { declared, actual } => {
63            Error::RecordSizeMismatch {
64                expected: declared,
65                actual,
66            }
67        }
68        rd_rds::lazyload::Error::CompressionUnsupported { compression } => {
69            Error::UnsupportedRecordCompression { compression }
70        }
71        other => {
72            Error::MalformedIndex(format!("zlib decompression of .rdb record failed: {other}"))
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn maps_unknown_envelope_to_existing_error() {
83        let path = std::env::temp_dir().join(format!(
84            "rd-helpdb-unknown-envelope-{}.rds",
85            std::process::id()
86        ));
87        std::fs::write(&path, b"A\nunknown").expect("write unknown envelope");
88        let err = read_rds_file(&path).unwrap_err();
89        let _ = std::fs::remove_file(&path);
90        assert!(matches!(
91            err,
92            Error::UnsupportedCompression { magic, .. } if magic == b"A\nunkn"
93        ));
94    }
95
96    #[test]
97    fn rejects_short_records() {
98        let err = decode_rdb_record(&[0, 1, 2]).unwrap_err();
99        assert!(matches!(
100            err,
101            Error::RecordSizeMismatch {
102                expected: 4,
103                actual: 3
104            }
105        ));
106    }
107
108    #[test]
109    fn compatibility_record_decoder_rejects_trailing_bytes() {
110        let mut bytes = include_bytes!("../tests/fixtures/data/rd_minimal_v3.rdbentry").to_vec();
111        bytes.push(0);
112        let err = decode_rdb_record(&bytes).unwrap_err();
113        assert!(matches!(err, Error::MalformedIndex(message) if message.contains("trailing")));
114    }
115
116    #[test]
117    fn compatibility_record_decoder_maps_declared_size_mismatch() {
118        let mut bytes = include_bytes!("../tests/fixtures/data/rd_minimal_v3.rdbentry").to_vec();
119        bytes[3] = bytes[3].wrapping_add(1);
120        let err = decode_rdb_record(&bytes).unwrap_err();
121        assert!(matches!(err, Error::RecordSizeMismatch { .. }));
122    }
123}