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    /// Access the raw decompressed bytes.
48    pub fn as_bytes(&self) -> &[u8] {
49        &self.0
50    }
51}
52
53/// Decompress a shard's body.
54///
55/// Implementation detail of `DecryptedShard::decompress()`.
56/// Strips padding (if present), then decompresses the zstd data.
57pub(crate) fn decompress_shard_body(data: Vec<u8>) -> Result<ShardBody> {
58    let data = if let Some(padding_size) = read_padding_info(&data) {
59        &data[..data.len().saturating_sub(16 + padding_size)]
60    } else {
61        &data[..]
62    };
63
64    if data.is_empty() {
65        return Ok(ShardBody(Vec::new()));
66    }
67
68    let decompressed = decompress_bounded(data, MAX_DECOMPRESSED_SIZE)?;
69    Ok(ShardBody(decompressed))
70}
71
72/// Decompresses zstd data with a maximum output size limit.
73///
74/// Uses streaming decompression to enforce the limit during decompression,
75/// preventing memory exhaustion attacks where a small compressed payload
76/// expands to consume all available memory (zstd bomb).
77///
78/// # Errors
79/// Returns `VoidError::Shard` if decompressed size exceeds the limit.
80/// Returns `VoidError::Compression` if decompression fails.
81fn decompress_bounded(compressed: &[u8], max_size: u64) -> Result<Vec<u8>> {
82    use std::io::Read;
83
84    let mut decoder = zstd::Decoder::new(compressed)
85        .map_err(|e| VoidError::Compression(e.to_string()))?;
86
87    let initial_capacity = std::cmp::min(compressed.len().saturating_mul(4), max_size as usize);
88    let mut output = Vec::with_capacity(initial_capacity);
89
90    const CHUNK_SIZE: usize = 64 * 1024;
91    let mut buf = [0u8; CHUNK_SIZE];
92
93    loop {
94        let bytes_read = decoder
95            .read(&mut buf)
96            .map_err(|e| VoidError::Compression(e.to_string()))?;
97
98        if bytes_read == 0 {
99            break;
100        }
101
102        if output.len() + bytes_read > max_size as usize {
103            return Err(VoidError::Shard(format!(
104                "decompressed size exceeds maximum {} bytes",
105                max_size
106            )));
107        }
108
109        output.extend_from_slice(&buf[..bytes_read]);
110    }
111
112    Ok(output)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn decompress_bounded_stops_at_limit() {
121        let data = vec![0u8; 1024 * 1024];
122        let compressed = zstd::encode_all(&data[..], 3).unwrap();
123        let small_limit = 512 * 1024;
124
125        let result = decompress_bounded(&compressed, small_limit);
126        assert!(result.is_err());
127        assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
128    }
129}