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        let base = "".as_bytes();
166
167        assert_eq!(
168            Ok(TARGET.to_vec()),
169            BuiltInBrotliDecoder.decode(&NO_DICT_PATCH, None, TARGET.len())
170        );
171
172        // Check that empty base is handled the same as no base.
173        assert_eq!(
174            Ok(TARGET.to_vec()),
175            BuiltInBrotliDecoder.decode(&NO_DICT_PATCH, Some(base), TARGET.len())
176        );
177    }
178
179    #[test]
180    fn brotli_decode_too_little_output() {
181        assert_eq!(
182            Err(DecodeError::MaxSizeExceeded),
183            BuiltInBrotliDecoder.decode(
184                &SHARED_DICT_PATCH,
185                Some(BASE.as_bytes()),
186                TARGET.len() - 1
187            )
188        );
189    }
190
191    #[test]
192    fn brotli_decode_excess_output() {
193        assert_eq!(
194            Ok(TARGET.to_vec()),
195            BuiltInBrotliDecoder.decode(
196                &SHARED_DICT_PATCH,
197                Some(BASE.as_bytes()),
198                TARGET.len() + 1,
199            )
200        );
201    }
202
203    // TODO(garretrieger): there doesn't seem to be an easy way to detect this condition with
204    // the rust brotli implementation. So disable for now. However, we need to make this behaviour
205    // consistent between the two possible implementations. Either don't check for this in the c
206    // version, or figure out how to have a similar check in rust.
207    #[cfg(feature = "c-brotli")]
208    #[test]
209    fn brotli_decode_too_much_input() {
210        let mut patch: Vec<u8> = NO_DICT_PATCH.to_vec();
211        patch.push(0u8);
212
213        assert_eq!(
214            Err(DecodeError::ExcessInputData),
215            BuiltInBrotliDecoder.decode(&patch, None, TARGET.len())
216        );
217    }
218
219    #[test]
220    fn brotli_decode_input_missing() {
221        // Check what happens if input stream is missing some trailing bytes
222        let patch: Vec<u8> = NO_DICT_PATCH[..NO_DICT_PATCH.len() - 1].to_vec();
223        assert!(matches!(
224            BuiltInBrotliDecoder.decode(&patch, None, TARGET.len()),
225            Err(DecodeError::InvalidStream)
226        ));
227    }
228
229    #[test]
230    fn brotli_decode_invalid() {
231        let patch = [0xFF, 0xFF, 0xFFu8];
232        assert!(matches!(
233            BuiltInBrotliDecoder.decode(&patch, None, 10),
234            Err(DecodeError::InvalidStream)
235        ));
236    }
237}