Skip to main content

shared_brotli_patch_decoder/
lib.rs

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