camel_processor/data_format/
gzip.rs1use bytes::Bytes;
2use camel_api::body::Body;
3use camel_api::data_format::DataFormat;
4use camel_api::error::CamelError;
5use flate2::Compression;
6use flate2::write::GzEncoder;
7use serde::Deserialize;
8use std::io::{Read, Write};
9
10const DEFAULT_MAX_DECOMPRESSED_SIZE: u64 = 1_073_741_824;
11const DEFAULT_MAX_INPUT_SIZE: u64 = 64 * 1024 * 1024; pub(super) fn deserialize_compression_level<'de, D>(deserializer: D) -> Result<Option<u8>, D::Error>
22where
23 D: serde::Deserializer<'de>,
24{
25 let raw: Option<u32> = Option::deserialize(deserializer)?;
26 match raw {
27 None => Ok(None),
28 Some(level) if level <= 9 => Ok(Some(level as u8)),
29 Some(level) => Err(serde::de::Error::custom(format!(
30 "compression_level must be between 0 and 9, got {level}"
31 ))),
32 }
33}
34
35fn to_flate2_compression(level: Option<u8>) -> Result<Compression, CamelError> {
39 match level {
40 None => Ok(Compression::default()),
41 Some(level) if level <= 9 => Ok(Compression::new(u32::from(level))),
42 Some(level) => Err(CamelError::TypeConversionFailed(format!(
43 "GzipDataFormat::marshal compression_level must be 0-9, got {level}"
44 ))),
45 }
46}
47
48#[derive(Debug, Clone, Deserialize)]
49#[serde(default, deny_unknown_fields)]
50pub struct GzipConfig {
51 pub max_decompressed_size: u64,
52 pub max_input_size: u64,
54 #[serde(deserialize_with = "deserialize_compression_level")]
56 pub compression_level: Option<u8>,
57}
58
59impl Default for GzipConfig {
60 fn default() -> Self {
61 Self {
62 max_decompressed_size: DEFAULT_MAX_DECOMPRESSED_SIZE,
63 max_input_size: DEFAULT_MAX_INPUT_SIZE,
64 compression_level: None,
65 }
66 }
67}
68
69#[derive(Debug, Clone, Default)]
70pub struct GzipDataFormat {
71 config: GzipConfig,
72}
73
74impl GzipDataFormat {
75 pub fn new(config: GzipConfig) -> Self {
76 Self { config }
77 }
78}
79
80pub(crate) struct FirstGzipMember {
82 pub data: Vec<u8>,
85 pub has_trailing_input: bool,
88}
89
90pub(crate) fn decode_first_member(
104 raw: &[u8],
105 take_limit: u64,
106) -> Result<FirstGzipMember, std::io::Error> {
107 let decoder =
108 flate2::bufread::GzDecoder::new(std::io::BufReader::new(std::io::Cursor::new(raw)));
109 let mut data = Vec::new();
110 let mut limited = decoder.take(take_limit);
111 limited.read_to_end(&mut data)?;
112
113 let buf_reader = limited.into_inner().into_inner();
118 let buffered = buf_reader.buffer().len() as u64;
119 let position = buf_reader.into_inner().position();
120 let consumed = position.saturating_sub(buffered);
121
122 Ok(FirstGzipMember {
123 data,
124 has_trailing_input: consumed < raw.len() as u64,
125 })
126}
127
128impl DataFormat for GzipDataFormat {
129 fn name(&self) -> &str {
130 "gzip"
131 }
132
133 fn marshal(&self, body: Body) -> Result<Body, CamelError> {
134 let content =
135 super::materialize_marshal_input("GzipDataFormat", &body, self.config.max_input_size)?;
136
137 let level = to_flate2_compression(self.config.compression_level)?;
138 let mut encoder = GzEncoder::new(Vec::new(), level);
139 encoder.write_all(&content).map_err(|e| {
140 CamelError::TypeConversionFailed(format!(
141 "GzipDataFormat::marshal failed to compress: {e}"
142 ))
143 })?;
144 let compressed = encoder.finish().map_err(|e| {
145 CamelError::TypeConversionFailed(format!(
146 "GzipDataFormat::marshal failed to finalize gzip stream: {e}"
147 ))
148 })?;
149
150 Ok(Body::Bytes(Bytes::from(compressed)))
151 }
152
153 fn unmarshal(&self, body: Body) -> Result<Body, CamelError> {
154 let raw = super::raw_unmarshal_body("GzipDataFormat", "GZIP data", &body)?;
155
156 let take_limit = self.config.max_decompressed_size.saturating_add(1);
162 let first = decode_first_member(&raw, take_limit).map_err(|e| {
163 CamelError::TypeConversionFailed(format!(
164 "GzipDataFormat::unmarshal invalid GZIP stream: {e}"
165 ))
166 })?;
167
168 if first.data.len() as u64 > self.config.max_decompressed_size {
169 return Err(CamelError::TypeConversionFailed(format!(
170 "GzipDataFormat::unmarshal decompressed size exceeds max_decompressed_size {}",
171 self.config.max_decompressed_size
172 )));
173 }
174
175 Ok(Body::Bytes(Bytes::from(first.data)))
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use bytes::Bytes;
183
184 use super::super::test_util::{assert_bytes, stream_body_pair};
185
186 #[test]
187 fn test_name() {
188 let df = GzipDataFormat::default();
189 assert_eq!(df.name(), "gzip");
190 }
191
192 #[test]
193 fn test_gzip_config_deserialize_from_json() {
194 let json = serde_json::json!({
195 "max_decompressed_size": 2147483648u64,
196 "max_input_size": 134217728u64,
197 "compression_level": 9
198 });
199 let cfg: GzipConfig = serde_json::from_value(json).unwrap();
200 assert_eq!(cfg.max_decompressed_size, 2147483648);
201 assert_eq!(cfg.max_input_size, 134217728);
202 assert_eq!(cfg.compression_level, Some(9));
203 }
204
205 #[test]
206 fn test_gzip_config_compression_level_boundaries() {
207 for level in [0u8, 9] {
208 let json = serde_json::json!({ "compression_level": level });
209 let cfg: GzipConfig = serde_json::from_value(json).unwrap();
210 assert_eq!(cfg.compression_level, Some(level));
211 }
212 }
213
214 #[test]
215 fn test_gzip_programmatic_out_of_range_level_fails_closed() {
216 let df = GzipDataFormat::new(GzipConfig {
217 compression_level: Some(10),
218 ..Default::default()
219 });
220 let result = df.marshal(Body::Bytes(Bytes::from_static(b"payload")));
221 let msg = format!("{}", result.unwrap_err());
222 assert!(
223 msg.contains("compression_level must be 0-9"),
224 "error should mention the level bound: {msg}"
225 );
226 }
227
228 #[test]
229 fn gzip_round_trip_with_explicit_compression_levels() {
230 for level in [0u8, 9] {
231 let df = GzipDataFormat::new(GzipConfig {
232 compression_level: Some(level),
233 ..Default::default()
234 });
235 let original = Body::Bytes(Bytes::from_static(b"explicit level payload"));
236 let restored = df.unmarshal(df.marshal(original.clone()).unwrap()).unwrap();
237 assert_eq!(restored, original, "round trip failed for level {level}");
238 }
239 }
240
241 #[test]
242 fn test_gzip_config_deny_unknown_fields() {
243 let json = serde_json::json!({"unknown_key": 42});
244 let result: Result<GzipConfig, _> = serde_json::from_value(json);
245 assert!(result.is_err());
246 }
247
248 #[test]
249 fn test_gzip_config_invalid_compression_level_fails_closed() {
250 let json = serde_json::json!({"compression_level": 10});
251 let result: Result<GzipConfig, _> = serde_json::from_value(json);
252 assert!(result.is_err());
253 }
254
255 #[test]
256 fn gzip_round_trip_bytes() {
257 let df = GzipDataFormat::default();
258 let original = Body::Bytes(Bytes::from_static(b"\x00\x01\x02\xffbinary payload"));
259 let compressed = df.marshal(original.clone()).unwrap();
260 let compressed_bytes = match &compressed {
261 Body::Bytes(b) => b.clone(),
262 _ => panic!("expected Body::Bytes"),
263 };
264 assert_eq!(
265 &compressed_bytes[..2],
266 &[0x1f, 0x8b],
267 "marshal must emit standalone gzip bytes"
268 );
269 let restored = df.unmarshal(compressed).unwrap();
270 assert_eq!(restored, original);
271 }
272
273 #[test]
274 fn gzip_tar_cross_format_round_trip() {
275 let tar_df = super::super::tar::TarDataFormat::default();
278 let gzip_df = GzipDataFormat::default();
279 let original = Body::Bytes(Bytes::from_static(b"\x00\x01\x02\xfftar.gz payload"));
280
281 let archived = tar_df.marshal(original.clone()).unwrap();
282 let compressed = gzip_df.marshal(archived.clone()).unwrap();
283 let decompressed = gzip_df.unmarshal(compressed).unwrap();
284 assert_eq!(
285 decompressed, archived,
286 "gzip layer must preserve the TAR archive exactly"
287 );
288 let restored = tar_df.unmarshal(decompressed).unwrap();
289 assert_eq!(restored, original);
290 }
291
292 #[test]
293 fn gzip_data_format_first_member_behavior_is_unchanged() {
294 let df = GzipDataFormat::default();
295 let first = match df
296 .marshal(Body::Bytes(Bytes::from_static(b"first member payload")))
297 .unwrap()
298 {
299 Body::Bytes(b) => b.to_vec(),
300 other => panic!("expected Body::Bytes: {other:?}"),
301 };
302 let second = match df
303 .marshal(Body::Bytes(Bytes::from_static(b"second member payload")))
304 .unwrap()
305 {
306 Body::Bytes(b) => b.to_vec(),
307 other => panic!("expected Body::Bytes: {other:?}"),
308 };
309 let mut concatenated = first;
310 concatenated.extend_from_slice(&second);
311
312 let restored = df
315 .unmarshal(Body::Bytes(Bytes::from(concatenated)))
316 .unwrap();
317 assert_bytes(restored, b"first member payload");
318 }
319
320 #[test]
321 fn gzip_decompression_limit_covers_full_stream() {
322 let config = GzipConfig {
323 max_decompressed_size: 16,
324 ..Default::default()
325 };
326 let df = GzipDataFormat::new(config);
327 let compressed = GzipDataFormat::default()
330 .marshal(Body::Bytes(Bytes::from(vec![b'A'; 4096])))
331 .unwrap();
332 let result = df.unmarshal(compressed);
333 match result {
334 Err(CamelError::TypeConversionFailed(msg)) => {
335 assert!(
336 msg.contains("max_decompressed_size"),
337 "error should mention max_decompressed_size: {msg}"
338 );
339 }
340 _ => panic!("decompressed stream beyond the cap must be rejected"),
341 }
342 }
343
344 #[test]
345 fn malformed_gzip_input_rejected() {
346 let df = GzipDataFormat::default();
347
348 let garbage = vec![b'G'; 64];
350 let result = df.unmarshal(Body::Bytes(Bytes::from(garbage)));
351 match result {
352 Err(CamelError::TypeConversionFailed(_)) => {}
353 _ => panic!("garbage input must yield TypeConversionFailed"),
354 }
355
356 let full = match GzipDataFormat::default()
358 .marshal(Body::Bytes(Bytes::from_static(b"hello world")))
359 {
360 Ok(Body::Bytes(b)) => b.to_vec(),
361 other => panic!("marshal must yield compressed bytes: {other:?}"),
362 };
363 let truncated = full[..full.len() - 4].to_vec();
364 let result = df.unmarshal(Body::Bytes(Bytes::from(truncated)));
365 match result {
366 Err(CamelError::TypeConversionFailed(_)) => {}
367 _ => panic!("truncated gzip input must yield TypeConversionFailed"),
368 }
369 }
370
371 #[test]
372 fn gzip_materialized_empty_and_stream_bodies() {
373 let df = GzipDataFormat::default();
374
375 for body in [Body::Bytes(Bytes::new()), Body::Text(String::new())] {
378 let compressed = df.marshal(body).unwrap();
379 let restored = df.unmarshal(compressed).unwrap();
380 assert_bytes(restored, b"");
381 }
382
383 assert!(df.marshal(Body::Empty).is_err());
385
386 let (body, slot) = stream_body_pair();
389 assert!(df.marshal(body).is_err());
390 assert!(
391 slot.blocking_lock().is_some(),
392 "marshal must not consume the stream"
393 );
394
395 let (body, slot) = stream_body_pair();
396 assert!(df.unmarshal(body).is_err());
397 assert!(
398 slot.blocking_lock().is_some(),
399 "unmarshal must not consume the stream"
400 );
401 }
402
403 #[test]
404 fn test_marshal_input_size_cap() {
405 let config = GzipConfig {
406 max_input_size: 16,
407 ..Default::default()
408 };
409 let df = GzipDataFormat::new(config);
410 let result = df.marshal(Body::Text("x".repeat(64)));
411 assert!(result.is_err());
412 let msg = format!("{}", result.unwrap_err());
413 assert!(
414 msg.contains("max_input_size"),
415 "error should mention max_input_size: {msg}"
416 );
417 }
418}