Skip to main content

shared_brotli_patch_decoder/
lib.rs

1#[cfg(feature = "c-brotli")]
2mod c_brotli;
3
4#[cfg(feature = "c-brotli")]
5mod sys;
6
7#[cfg(feature = "rust-brotli")]
8mod rust_brotli;
9
10pub mod decode_error;
11
12#[cfg(fuzzing)]
13use decode_error::DecodeError;
14
15/// A Shared Brotli Decoder.
16///
17/// Shared brotli (<https://datatracker.ietf.org/doc/draft-vandevenne-shared-brotli-format/>) is an
18/// extension of brotli to allow the decompression to include a shared dictionary.
19pub trait SharedBrotliDecoder {
20    /// Decodes shared brotli encoded data using the optional shared dictionary.
21    ///
22    /// The shared dictionary is a raw LZ77 style dictionary, see:
23    /// <https://datatracker.ietf.org/doc/html/draft-vandevenne-shared-brotli-format#section-3.2>
24    ///
25    /// Will fail if the decoded result will be greater than max_uncompressed_length. Any excess data
26    /// in encoded after the encoded stream finishes is also considered an error.
27    fn decode(
28        &self,
29        encoded: &[u8],
30        shared_dictionary: Option<&[u8]>,
31        max_uncompressed_length: usize,
32    ) -> Result<Vec<u8>, decode_error::DecodeError>;
33}
34
35/// The brotli decoder provided by this crate.
36///
37/// By default a rust wrapper around the c brotli decoder implementation is used.
38pub struct BuiltInBrotliDecoder;
39
40/// An implementation that just passes through the input data.
41///
42/// Useful in fuzzers and unit testing.
43pub struct NoopBrotliDecoder;
44
45impl SharedBrotliDecoder for Box<dyn SharedBrotliDecoder> {
46    fn decode(
47        &self,
48        encoded: &[u8],
49        shared_dictionary: Option<&[u8]>,
50        max_uncompressed_length: usize,
51    ) -> Result<Vec<u8>, decode_error::DecodeError> {
52        self.as_ref()
53            .decode(encoded, shared_dictionary, max_uncompressed_length)
54    }
55}
56
57impl SharedBrotliDecoder for BuiltInBrotliDecoder {
58    fn decode(
59        &self,
60        encoded: &[u8],
61        shared_dictionary: Option<&[u8]>,
62        max_uncompressed_length: usize,
63    ) -> Result<Vec<u8>, decode_error::DecodeError> {
64        cfg_if::cfg_if! {
65            if #[cfg(feature = "c-brotli")] {
66                #[allow(clippy::needless_return)]
67                return c_brotli::shared_brotli_decode_c(
68                    encoded,
69                    shared_dictionary,
70                    max_uncompressed_length,
71                );
72            } else if #[cfg(feature = "rust-brotli")] {
73                return rust_brotli::shared_brotli_decode_rust(encoded, shared_dictionary, max_uncompressed_length);
74            } else {
75                compile_error!("At least one of 'c-brotli' or 'rust-brotli' must be enabled.");
76            }
77        }
78    }
79}
80
81impl SharedBrotliDecoder for NoopBrotliDecoder {
82    fn decode(
83        &self,
84        encoded: &[u8],
85        _shared_dictionary: Option<&[u8]>,
86        max_uncompressed_length: usize,
87    ) -> Result<Vec<u8>, decode_error::DecodeError> {
88        if encoded.len() <= max_uncompressed_length {
89            Ok(encoded.to_vec())
90        } else {
91            Err(decode_error::DecodeError::MaxSizeExceeded)
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use decode_error::DecodeError;
100
101    const TARGET: &[u8] = "hijkabcdeflmnohijkabcdeflmno\n".as_bytes();
102    const BASE: &str = "abcdef\n";
103
104    // This patch was manually generated with a brotli encoder (https://github.com/google/brotli)
105    // uncompressed = TARGET
106    // dict = BASE
107    const SHARED_DICT_PATCH: [u8; 23] = [
108        0xa1, 0xe0, 0x00, 0xc0, 0x2f, 0x3a, 0x38, 0xf4, 0x01, 0xd1, 0xaf, 0x54, 0x84, 0x14, 0x71,
109        0x2a, 0x80, 0x04, 0xa2, 0x1c, 0xd3, 0xdd, 0x07,
110    ];
111
112    // This patch was manually generated with a brotli encoder (https://github.com/google/brotli)
113    // uncompressed = TARGET
114    const NO_DICT_PATCH: [u8; 26] = [
115        0xa1, 0xe0, 0x00, 0xc0, 0x2f, 0x96, 0x1c, 0xf3, 0x03, 0xb1, 0xcf, 0x45, 0x95, 0x22, 0x4a,
116        0xc5, 0x03, 0x21, 0xb2, 0x9a, 0x58, 0xd4, 0x7c, 0xf6, 0x1e, 0x00u8,
117    ];
118
119    #[test]
120    fn brotli_decode_with_shared_dict() {
121        assert_eq!(
122            Ok(TARGET.to_vec()),
123            BuiltInBrotliDecoder.decode(&SHARED_DICT_PATCH, Some(BASE.as_bytes()), TARGET.len(),)
124        );
125    }
126
127    #[test]
128    fn brotli_decode_rust_brotli_regression_case() {
129        // Tests a case that triggered a bug in the rust brotli decompressor library,
130        // see: https://github.com/dropbox/rust-brotli-decompressor/issues/36
131        let patch: &[u8] = &[
132            27, 103, 0, 96, 47, 14, 120, 211, 142, 228, 22, 15, 167, 193, 55, 28, 228, 226, 254,
133            54, 10, 36, 226, 192, 19, 76, 50, 8, 169, 92, 9, 197, 47, 12, 211, 114, 34, 175, 18,
134            241, 122, 134, 170, 32, 189, 4, 112, 153, 119, 12, 237, 23, 120, 130, 2,
135        ];
136
137        let dict: Vec<u8> = vec![
138            2, 0, 0, 0, 0, 213, 195, 31, 121, 231, 225, 250, 238, 34, 174, 158, 246, 208, 145, 187,
139            92, 2, 0, 0, 4, 0, 0, 0, 46, 0, 0, 0, 0, 0, 11, 123, 105, 100, 125, 46, 105, 102, 116,
140            95, 116, 107, 20, 0, 0, 52, 40, 103, 221, 215, 223, 255, 95, 54, 15, 13, 85, 53, 206,
141            115, 249, 165, 159, 159, 16, 29, 37, 17, 114, 1, 163, 2, 16, 33, 51, 4, 32, 0, 226, 29,
142            19, 88, 254, 195, 129, 23, 25, 22, 8, 19, 21, 41, 130, 136, 51, 8, 67, 209, 52, 204,
143            204, 70, 199, 130, 252, 47, 16, 40, 186, 251, 62, 63, 19, 236, 147, 240, 211, 215, 59,
144        ];
145
146        let decompressed = BuiltInBrotliDecoder.decode(patch, Some(&dict), 500);
147
148        assert_eq!(
149            decompressed,
150            Ok(vec![
151                0x02, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x16, 0xa6, 0x25, 0x18, 0xf8, 0x68, 0x63, 0x4e,
152                0xe4, 0x09, 0x2b, 0xa1, 0xe2, 0x4b, 0xba, 0x02, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
153                0x2e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x7b, 0x69, 0x64, 0x7d, 0x2e, 0x69, 0x66,
154                0x74, 0x5f, 0x74, 0x6b, 0x14, 0x00, 0x00, 0x38, 0x1d, 0x25, 0x11, 0x72, 0x01, 0xa3,
155                0x02, 0x10, 0x21, 0x33, 0x04, 0x20, 0x00, 0xe2, 0x1d, 0x13, 0x58, 0xfe, 0xc3, 0x81,
156                0x17, 0x19, 0x16, 0x08, 0x13, 0x15, 0x29, 0x82, 0x88, 0x33, 0x08, 0x43, 0xd1, 0x34,
157                0xcc, 0xcc, 0x46, 0xc7, 0x82, 0xfc, 0x2f, 0x10, 0x28, 0xba, 0xfb, 0x3e, 0x3f, 0x13,
158                0xec, 0x93, 0xf0, 0xd3, 0xd7, 0x3b,
159            ])
160        );
161    }
162
163    #[test]
164    fn brotli_decode_without_shared_dict() {
165        assert_eq!(
166            Ok(TARGET.to_vec()),
167            BuiltInBrotliDecoder.decode(&NO_DICT_PATCH, None, TARGET.len())
168        );
169    }
170
171    #[test]
172    fn brotli_decode_with_empty_shared_dict() {
173        // An empty shared dictionary used to behave similarly to a `None`, but it is now invalid as
174        // of https://github.com/google/brotli/pull/1479
175        assert_eq!(
176            Err(DecodeError::InvalidDictionary),
177            BuiltInBrotliDecoder.decode(&NO_DICT_PATCH, Some(b""), TARGET.len())
178        );
179    }
180
181    #[test]
182    fn brotli_decode_too_little_output() {
183        assert_eq!(
184            Err(DecodeError::MaxSizeExceeded),
185            BuiltInBrotliDecoder.decode(
186                &SHARED_DICT_PATCH,
187                Some(BASE.as_bytes()),
188                TARGET.len() - 1
189            )
190        );
191    }
192
193    #[test]
194    fn brotli_decode_excess_output() {
195        assert_eq!(
196            Ok(TARGET.to_vec()),
197            BuiltInBrotliDecoder.decode(
198                &SHARED_DICT_PATCH,
199                Some(BASE.as_bytes()),
200                TARGET.len() + 1,
201            )
202        );
203    }
204
205    // TODO(garretrieger): there doesn't seem to be an easy way to detect this condition with
206    // the rust brotli implementation. So disable for now. However, we need to make this behaviour
207    // consistent between the two possible implementations. Either don't check for this in the c
208    // version, or figure out how to have a similar check in rust.
209    #[cfg(feature = "c-brotli")]
210    #[test]
211    fn brotli_decode_too_much_input() {
212        let mut patch: Vec<u8> = NO_DICT_PATCH.to_vec();
213        patch.push(0u8);
214
215        assert_eq!(
216            Err(DecodeError::ExcessInputData),
217            BuiltInBrotliDecoder.decode(&patch, None, TARGET.len())
218        );
219    }
220
221    #[test]
222    fn brotli_decode_input_missing() {
223        // Check what happens if input stream is missing some trailing bytes
224        let patch: Vec<u8> = NO_DICT_PATCH[..NO_DICT_PATCH.len() - 1].to_vec();
225        assert!(matches!(
226            BuiltInBrotliDecoder.decode(&patch, None, TARGET.len()),
227            Err(DecodeError::InvalidStream)
228        ));
229    }
230
231    #[test]
232    fn brotli_decode_invalid() {
233        let patch = [0xFF, 0xFF, 0xFFu8];
234        assert!(matches!(
235            BuiltInBrotliDecoder.decode(&patch, None, 10),
236            Err(DecodeError::InvalidStream)
237        ));
238    }
239}