1use std::io::{self, Write};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22#[repr(u8)]
23pub enum Compression {
24 Unknown = 0,
25 None = 1,
26 #[default]
27 Gzip = 2,
28 Brotli = 3,
29 Zstd = 4,
30}
31
32impl Compression {
33 #[allow(clippy::should_implement_trait)]
37 pub fn from_str(s: &str) -> Option<Self> {
38 match s.to_lowercase().as_str() {
39 "none" => Some(Compression::None),
40 "gzip" => Some(Compression::Gzip),
41 "brotli" => Some(Compression::Brotli),
42 "zstd" => Some(Compression::Zstd),
43 _ => Option::None,
44 }
45 }
46
47 pub fn code(&self) -> u8 {
49 *self as u8
50 }
51
52 pub fn from_code(code: u8) -> Option<Self> {
57 match code {
58 0 => Some(Compression::Unknown),
59 1 => Some(Compression::None),
60 2 => Some(Compression::Gzip),
61 3 => Some(Compression::Brotli),
62 4 => Some(Compression::Zstd),
63 _ => Option::None,
64 }
65 }
66
67 pub fn name(&self) -> &'static str {
69 match self {
70 Compression::Unknown => "unknown",
71 Compression::None => "none",
72 Compression::Gzip => "gzip",
73 Compression::Brotli => "brotli",
74 Compression::Zstd => "zstd",
75 }
76 }
77}
78
79struct Codec {
86 verb: &'static str,
87 gzip: fn(&[u8]) -> io::Result<Vec<u8>>,
88 brotli: fn(&[u8]) -> io::Result<Vec<u8>>,
89 zstd: fn(&[u8]) -> io::Result<Vec<u8>>,
90}
91
92const COMPRESS: &Codec = &Codec {
93 verb: "compress",
94 gzip: compress_gzip,
95 brotli: compress_brotli,
96 zstd: compress_zstd,
97};
98
99const DECOMPRESS: &Codec = &Codec {
100 verb: "decompress",
101 gzip: decompress_gzip,
102 brotli: decompress_brotli,
103 zstd: decompress_zstd,
104};
105
106fn dispatch(data: &[u8], compression: Compression, codec: &Codec) -> io::Result<Vec<u8>> {
107 match compression {
108 Compression::Unknown => Err(io::Error::new(
109 io::ErrorKind::InvalidInput,
110 format!("Cannot {} with unknown compression type", codec.verb),
111 )),
112 Compression::None => Ok(data.to_vec()),
113 Compression::Gzip => (codec.gzip)(data),
114 Compression::Brotli => (codec.brotli)(data),
115 Compression::Zstd => (codec.zstd)(data),
116 }
117}
118
119pub fn compress(data: &[u8], compression: Compression) -> io::Result<Vec<u8>> {
128 dispatch(data, compression, COMPRESS)
129}
130
131pub fn decompress(data: &[u8], compression: Compression) -> io::Result<Vec<u8>> {
144 dispatch(data, compression, DECOMPRESS)
145}
146
147fn decompress_gzip(data: &[u8]) -> io::Result<Vec<u8>> {
149 use std::io::Read;
150 let mut out = Vec::new();
151 flate2::read::GzDecoder::new(data).read_to_end(&mut out)?;
152 Ok(out)
153}
154
155fn decompress_brotli(data: &[u8]) -> io::Result<Vec<u8>> {
157 use std::io::Read;
158 let mut out = Vec::new();
159 brotli::Decompressor::new(data, 4096).read_to_end(&mut out)?;
160 Ok(out)
161}
162
163fn decompress_zstd(data: &[u8]) -> io::Result<Vec<u8>> {
165 zstd::decode_all(data)
166}
167
168fn compress_gzip(data: &[u8]) -> io::Result<Vec<u8>> {
170 use flate2::write::GzEncoder;
171 use flate2::Compression as GzCompression;
172
173 let mut encoder = GzEncoder::new(Vec::new(), GzCompression::default());
174 encoder.write_all(data)?;
175 encoder.finish()
176}
177
178fn compress_brotli(data: &[u8]) -> io::Result<Vec<u8>> {
180 use brotli::enc::BrotliEncoderParams;
181 use brotli::CompressorWriter;
182
183 let params = BrotliEncoderParams {
186 quality: 4,
187 ..Default::default()
188 };
189
190 let mut output = Vec::new();
191 {
192 let mut writer = CompressorWriter::with_params(&mut output, 4096, ¶ms);
193 writer.write_all(data)?;
194 }
195 Ok(output)
196}
197
198fn compress_zstd(data: &[u8]) -> io::Result<Vec<u8>> {
200 zstd::encode_all(data, 3)
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
213 fn test_compression_codes_match_pmtiles_spec() {
214 assert_eq!(Compression::Unknown.code(), 0);
216 assert_eq!(Compression::None.code(), 1);
217 assert_eq!(Compression::Gzip.code(), 2);
218 assert_eq!(Compression::Brotli.code(), 3);
219 assert_eq!(Compression::Zstd.code(), 4);
220 }
221
222 #[test]
223 fn test_compression_default_is_gzip() {
224 assert_eq!(Compression::default(), Compression::Gzip);
229 }
230
231 #[test]
232 fn test_compression_from_str() {
233 assert_eq!(Compression::from_str("none"), Some(Compression::None));
234 assert_eq!(Compression::from_str("gzip"), Some(Compression::Gzip));
235 assert_eq!(Compression::from_str("brotli"), Some(Compression::Brotli));
236 assert_eq!(Compression::from_str("zstd"), Some(Compression::Zstd));
237 assert_eq!(Compression::from_str("GZIP"), Some(Compression::Gzip)); assert_eq!(Compression::from_str("invalid"), Option::None);
239 }
240
241 #[test]
242 fn test_compression_names() {
243 assert_eq!(Compression::None.name(), "none");
244 assert_eq!(Compression::Gzip.name(), "gzip");
245 assert_eq!(Compression::Brotli.name(), "brotli");
246 assert_eq!(Compression::Zstd.name(), "zstd");
247 }
248
249 #[test]
254 fn test_compress_none_returns_original() {
255 let data = b"Hello, PMTiles!";
256 let compressed = compress(data, Compression::None).unwrap();
257 assert_eq!(compressed, data);
258 }
259
260 #[test]
261 fn test_compress_unknown_returns_error() {
262 let data = b"Hello, PMTiles!";
263 let result = compress(data, Compression::Unknown);
264 assert!(result.is_err());
265 }
266
267 #[test]
268 fn test_compress_gzip_produces_smaller_output() {
269 let data = "Hello, PMTiles! ".repeat(100);
271 let compressed = compress(data.as_bytes(), Compression::Gzip).unwrap();
272 assert!(
273 compressed.len() < data.len(),
274 "Gzip should compress repetitive data: {} < {}",
275 compressed.len(),
276 data.len()
277 );
278 }
279
280 #[test]
281 fn test_compress_brotli_produces_smaller_output() {
282 let data = "Hello, PMTiles! ".repeat(100);
283 let compressed = compress(data.as_bytes(), Compression::Brotli).unwrap();
284 assert!(
285 compressed.len() < data.len(),
286 "Brotli should compress repetitive data: {} < {}",
287 compressed.len(),
288 data.len()
289 );
290 }
291
292 #[test]
293 fn test_compress_zstd_produces_smaller_output() {
294 let data = "Hello, PMTiles! ".repeat(100);
295 let compressed = compress(data.as_bytes(), Compression::Zstd).unwrap();
296 assert!(
297 compressed.len() < data.len(),
298 "Zstd should compress repetitive data: {} < {}",
299 compressed.len(),
300 data.len()
301 );
302 }
303
304 #[test]
309 fn test_gzip_roundtrip() {
310 use flate2::read::GzDecoder;
311 use std::io::Read;
312
313 let original = b"Hello, PMTiles! This is test data for compression roundtrip.";
314 let compressed = compress(original, Compression::Gzip).unwrap();
315
316 let mut decoder = GzDecoder::new(&compressed[..]);
317 let mut decompressed = Vec::new();
318 decoder.read_to_end(&mut decompressed).unwrap();
319
320 assert_eq!(decompressed, original);
321 }
322
323 #[test]
324 fn test_brotli_roundtrip() {
325 use brotli::Decompressor;
326 use std::io::Read;
327
328 let original = b"Hello, PMTiles! This is test data for compression roundtrip.";
329 let compressed = compress(original, Compression::Brotli).unwrap();
330
331 let mut decompressor = Decompressor::new(&compressed[..], 4096);
332 let mut decompressed = Vec::new();
333 decompressor.read_to_end(&mut decompressed).unwrap();
334
335 assert_eq!(decompressed, original);
336 }
337
338 #[test]
339 fn test_zstd_roundtrip() {
340 let original = b"Hello, PMTiles! This is test data for compression roundtrip.";
341 let compressed = compress(original, Compression::Zstd).unwrap();
342
343 let decompressed = zstd::decode_all(&compressed[..]).unwrap();
344
345 assert_eq!(decompressed, original);
346 }
347
348 #[test]
353 fn test_compress_empty_data() {
354 for compression in [
356 Compression::None,
357 Compression::Gzip,
358 Compression::Brotli,
359 Compression::Zstd,
360 ] {
361 let result = compress(&[], compression);
362 assert!(
363 result.is_ok(),
364 "{} should handle empty data",
365 compression.name()
366 );
367 }
368 }
369
370 #[test]
371 fn test_compress_large_data() {
372 let data = vec![0x42u8; 1_000_000];
374
375 for compression in [Compression::Gzip, Compression::Brotli, Compression::Zstd] {
376 let result = compress(&data, compression);
377 assert!(
378 result.is_ok(),
379 "{} should handle large data",
380 compression.name()
381 );
382
383 let compressed = result.unwrap();
384 assert!(
386 compressed.len() < data.len() / 10,
387 "{} should achieve >10x compression on uniform data",
388 compression.name()
389 );
390 }
391 }
392
393 #[test]
398 fn test_from_code_inverts_code() {
399 for compression in [
400 Compression::Unknown,
401 Compression::None,
402 Compression::Gzip,
403 Compression::Brotli,
404 Compression::Zstd,
405 ] {
406 assert_eq!(
407 Compression::from_code(compression.code()),
408 Some(compression)
409 );
410 }
411 assert_eq!(Compression::from_code(5), None);
412 assert_eq!(Compression::from_code(255), None);
413 }
414
415 #[test]
416 fn test_decompress_roundtrips_every_codec() {
417 let original = b"PMTiles round-trip payload \x00\x01\x02 with some repetition repetition";
418 for compression in [
419 Compression::None,
420 Compression::Gzip,
421 Compression::Brotli,
422 Compression::Zstd,
423 ] {
424 let compressed = compress(original, compression).unwrap();
425 let decompressed = decompress(&compressed, compression).unwrap();
426 assert_eq!(
427 decompressed,
428 original.to_vec(),
429 "{} round-trip",
430 compression.name()
431 );
432 }
433 }
434
435 #[test]
436 fn test_decompress_empty_payload() {
437 for compression in [
438 Compression::None,
439 Compression::Gzip,
440 Compression::Brotli,
441 Compression::Zstd,
442 ] {
443 let compressed = compress(&[], compression).unwrap();
444 let decompressed = decompress(&compressed, compression).unwrap();
445 assert!(decompressed.is_empty(), "{}", compression.name());
446 }
447 }
448
449 #[test]
450 fn test_decompress_unknown_is_error() {
451 assert!(decompress(b"anything", Compression::Unknown).is_err());
452 }
453
454 #[test]
455 fn test_decompress_corrupt_gzip_is_error() {
456 assert!(decompress(b"not gzip at all", Compression::Gzip).is_err());
457 }
458}