Skip to main content

void_core/shard/
reader.rs

1//! Shard reading — decompression and manifest-driven file access.
2
3use super::writer::read_padding_info;
4use crate::metadata::ManifestEntry;
5use crate::{FileContent, Result, VoidError};
6
7/// Maximum allowed decompressed body size (1 GB).
8///
9/// This prevents decompression bomb attacks where a small compressed
10/// payload expands to consume all available memory.
11const MAX_DECOMPRESSED_SIZE: u64 = 1024 * 1024 * 1024;
12
13/// Decompressed shard body for manifest-driven file access.
14///
15/// Use with `ManifestEntry` offsets instead of parsing shard-internal metadata.
16/// Created via `DecryptedShard::decompress()`.
17pub struct ShardBody(Vec<u8>);
18
19impl ShardBody {
20    /// Read a file using its manifest entry.
21    ///
22    /// Extracts the file content at the offset and length specified by the entry.
23    pub fn read_file(&self, entry: &ManifestEntry) -> Result<FileContent> {
24        let start = entry.offset as usize;
25        let end = start
26            .checked_add(entry.length as usize)
27            .ok_or_else(|| VoidError::Shard("offset+length overflow".into()))?;
28        if end > self.0.len() {
29            return Err(VoidError::Shard(format!(
30                "file '{}' range {}..{} exceeds body size {}",
31                entry.path, start, end, self.0.len()
32            )));
33        }
34        Ok(self.0[start..end].to_vec())
35    }
36
37    /// Total decompressed body size in bytes.
38    pub fn len(&self) -> usize {
39        self.0.len()
40    }
41
42    /// Check if the body is empty.
43    pub fn is_empty(&self) -> bool {
44        self.0.is_empty()
45    }
46}
47
48/// Decompress a shard's body.
49///
50/// Implementation detail of `DecryptedShard::decompress()`.
51/// Strips padding (if present), then decompresses the zstd data.
52pub(crate) fn decompress_shard_body(data: Vec<u8>) -> Result<ShardBody> {
53    let data = if let Some(padding_size) = read_padding_info(&data) {
54        &data[..data.len().saturating_sub(16 + padding_size)]
55    } else {
56        &data[..]
57    };
58
59    if data.is_empty() {
60        return Ok(ShardBody(Vec::new()));
61    }
62
63    let decompressed = decompress_bounded(data, MAX_DECOMPRESSED_SIZE)?;
64    Ok(ShardBody(decompressed))
65}
66
67/// Decompresses zstd data with a maximum output size limit.
68///
69/// Uses streaming decompression to enforce the limit during decompression,
70/// preventing memory exhaustion attacks where a small compressed payload
71/// expands to consume all available memory (zstd bomb).
72///
73/// # Errors
74/// Returns `VoidError::Shard` if decompressed size exceeds the limit.
75/// Returns `VoidError::Compression` if decompression fails.
76fn decompress_bounded(compressed: &[u8], max_size: u64) -> Result<Vec<u8>> {
77    use std::io::Read;
78
79    let mut decoder = zstd::Decoder::new(compressed)
80        .map_err(|e| VoidError::Compression(e.to_string()))?;
81
82    let initial_capacity = std::cmp::min(compressed.len().saturating_mul(4), max_size as usize);
83    let mut output = Vec::with_capacity(initial_capacity);
84
85    const CHUNK_SIZE: usize = 64 * 1024;
86    let mut buf = [0u8; CHUNK_SIZE];
87
88    loop {
89        let bytes_read = decoder
90            .read(&mut buf)
91            .map_err(|e| VoidError::Compression(e.to_string()))?;
92
93        if bytes_read == 0 {
94            break;
95        }
96
97        if output.len() + bytes_read > max_size as usize {
98            return Err(VoidError::Shard(format!(
99                "decompressed size exceeds maximum {} bytes",
100                max_size
101            )));
102        }
103
104        output.extend_from_slice(&buf[..bytes_read]);
105    }
106
107    Ok(output)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn decompress_bounded_stops_at_limit() {
116        let data = vec![0u8; 1024 * 1024];
117        let compressed = zstd::encode_all(&data[..], 3).unwrap();
118        let small_limit = 512 * 1024;
119
120        let result = decompress_bounded(&compressed, small_limit);
121        assert!(result.is_err());
122        assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
123    }
124}