soil_client/maybe_compressed_blob/
mod.rs1use std::{
11 borrow::Cow,
12 io::{Read, Write},
13};
14
15const ZSTD_PREFIX: [u8; 8] = [82, 188, 83, 118, 70, 219, 142, 5];
20
21pub const CODE_BLOB_BOMB_LIMIT: usize = 50 * 1024 * 1024;
27
28#[derive(Debug, Clone, PartialEq, thiserror::Error)]
30pub enum Error {
31 #[error("Possible compression bomb encountered")]
33 PossibleBomb,
34 #[error("Blob had invalid format")]
36 Invalid,
37}
38
39fn read_from_decoder(
40 decoder: impl Read,
41 blob_len: usize,
42 bomb_limit: usize,
43) -> Result<Vec<u8>, Error> {
44 let mut decoder = decoder.take((bomb_limit + 1) as u64);
45
46 let mut buf = Vec::with_capacity(blob_len);
47 decoder.read_to_end(&mut buf).map_err(|_| Error::Invalid)?;
48
49 if buf.len() <= bomb_limit {
50 Ok(buf)
51 } else {
52 Err(Error::PossibleBomb)
53 }
54}
55
56fn decompress_zstd(blob: &[u8], bomb_limit: usize) -> Result<Vec<u8>, Error> {
57 let decoder = zstd::Decoder::new(blob).map_err(|_| Error::Invalid)?;
58
59 read_from_decoder(decoder, blob.len(), bomb_limit)
60}
61
62pub fn decompress(blob: &[u8], bomb_limit: usize) -> Result<Cow<'_, [u8]>, Error> {
65 if blob.starts_with(&ZSTD_PREFIX) {
66 decompress_zstd(&blob[ZSTD_PREFIX.len()..], bomb_limit).map(Into::into)
67 } else {
68 Ok(blob.into())
69 }
70}
71
72pub fn compress_weakly(blob: &[u8], bomb_limit: usize) -> Option<Vec<u8>> {
77 compress_with_level(blob, bomb_limit, 3)
78}
79
80pub fn compress_strongly(blob: &[u8], bomb_limit: usize) -> Option<Vec<u8>> {
85 compress_with_level(blob, bomb_limit, 22)
86}
87
88#[deprecated(
93 note = "Will be removed after June 2026. Use compress_strongly, compress_weakly or compress_with_level instead"
94)]
95pub fn compress(blob: &[u8], bomb_limit: usize) -> Option<Vec<u8>> {
96 compress_with_level(blob, bomb_limit, 3)
97}
98
99fn compress_with_level(blob: &[u8], bomb_limit: usize, level: i32) -> Option<Vec<u8>> {
106 if blob.len() > bomb_limit {
107 return None;
108 }
109
110 let mut buf = ZSTD_PREFIX.to_vec();
111
112 {
113 let mut v = zstd::Encoder::new(&mut buf, level).ok()?.auto_finish();
114 v.write_all(blob).ok()?;
115 }
116
117 Some(buf)
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 const BOMB_LIMIT: usize = 10;
125
126 #[test]
127 fn refuse_to_encode_over_limit() {
128 let mut v = vec![0; BOMB_LIMIT + 1];
129 assert!(compress_weakly(&v, BOMB_LIMIT).is_none());
130 assert!(compress_strongly(&v, BOMB_LIMIT).is_none());
131
132 let _ = v.pop();
133 assert!(compress_weakly(&v, BOMB_LIMIT).is_some());
134 assert!(compress_strongly(&v, BOMB_LIMIT).is_some());
135 }
136
137 #[test]
138 fn compress_and_decompress() {
139 let v = vec![0; BOMB_LIMIT];
140
141 let compressed_weakly = compress_weakly(&v, BOMB_LIMIT).unwrap();
142 let compressed_strongly = compress_strongly(&v, BOMB_LIMIT).unwrap();
143
144 assert!(compressed_weakly.starts_with(&ZSTD_PREFIX));
145 assert!(compressed_strongly.starts_with(&ZSTD_PREFIX));
146
147 assert_eq!(&decompress(&compressed_weakly, BOMB_LIMIT).unwrap()[..], &v[..]);
148 assert_eq!(&decompress(&compressed_strongly, BOMB_LIMIT).unwrap()[..], &v[..]);
149 }
150
151 #[test]
152 fn decompresses_only_when_magic() {
153 let v = vec![0; BOMB_LIMIT + 1];
154
155 assert_eq!(&decompress(&v, BOMB_LIMIT).unwrap()[..], &v[..]);
156 }
157
158 #[test]
159 fn possible_bomb_fails() {
160 let encoded_bigger_than_bomb = vec![0; BOMB_LIMIT + 1];
161 let mut buf = ZSTD_PREFIX.to_vec();
162
163 {
164 let mut v = zstd::Encoder::new(&mut buf, 3).unwrap().auto_finish();
165 v.write_all(&encoded_bigger_than_bomb[..]).unwrap();
166 }
167
168 assert_eq!(decompress(&buf[..], BOMB_LIMIT).err(), Some(Error::PossibleBomb));
169 }
170}