camel_processor/data_format/
zip.rs1use bytes::Bytes;
2use camel_api::body::Body;
3use camel_api::data_format::DataFormat;
4use camel_api::error::CamelError;
5use serde::Deserialize;
6use std::io::Read;
7use std::io::Write;
8use zip::ZipArchive;
9
10const DEFAULT_MAX_DECOMPRESSED_SIZE: u64 = 1_073_741_824;
11const DEFAULT_MAX_INPUT_SIZE: u64 = 64 * 1024 * 1024; const ENTRY_NAME: &str = "payload";
16
17#[derive(Debug, Clone, Deserialize)]
18#[serde(default, deny_unknown_fields)]
19pub struct ZipConfig {
20 pub max_decompressed_size: u64,
21 pub max_input_size: u64,
23 pub compression_level: Option<i32>,
24 pub allow_multi_entry: bool,
25}
26
27impl Default for ZipConfig {
28 fn default() -> Self {
29 Self {
30 max_decompressed_size: DEFAULT_MAX_DECOMPRESSED_SIZE,
31 max_input_size: DEFAULT_MAX_INPUT_SIZE,
32 compression_level: None,
33 allow_multi_entry: false,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Default)]
39pub struct ZipDataFormat {
40 config: ZipConfig,
41}
42
43impl ZipDataFormat {
44 pub fn new(config: ZipConfig) -> Self {
45 Self { config }
46 }
47}
48
49impl DataFormat for ZipDataFormat {
50 fn name(&self) -> &str {
51 "zip"
52 }
53
54 fn marshal(&self, body: Body) -> Result<Body, CamelError> {
55 let content =
56 super::materialize_marshal_input("ZipDataFormat", &body, self.config.max_input_size)?;
57
58 let mut buf = Vec::new();
59 {
60 let mut writer = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
61 let mut options = zip::write::SimpleFileOptions::default()
62 .compression_method(zip::CompressionMethod::Deflated);
63 if let Some(level) = self.config.compression_level {
64 if !(0..=9).contains(&level) {
65 return Err(CamelError::TypeConversionFailed(format!(
66 "ZipDataFormat::marshal compression_level must be 0-9, got {level}"
67 )));
68 }
69 options = options.compression_level(Some(level as i64));
70 }
71 writer.start_file(ENTRY_NAME, options).map_err(|e| {
72 CamelError::TypeConversionFailed(format!(
73 "ZipDataFormat::marshal failed to start entry: {e}"
74 ))
75 })?;
76 writer.write_all(&content).map_err(|e| {
77 CamelError::TypeConversionFailed(format!(
78 "ZipDataFormat::marshal failed to write entry: {e}"
79 ))
80 })?;
81 writer.finish().map_err(|e| {
82 CamelError::TypeConversionFailed(format!(
83 "ZipDataFormat::marshal failed to finalize archive: {e}"
84 ))
85 })?;
86 }
87
88 Ok(Body::Bytes(Bytes::from(buf)))
89 }
90
91 fn unmarshal(&self, body: Body) -> Result<Body, CamelError> {
92 let raw = super::raw_unmarshal_body("ZipDataFormat", "ZIP data", &body)?;
93
94 let reader = std::io::Cursor::new(&raw);
95 let mut archive = ZipArchive::new(reader).map_err(|e| {
96 CamelError::TypeConversionFailed(format!("ZipDataFormat::unmarshal invalid ZIP: {e}"))
97 })?;
98
99 if archive.is_empty() {
100 return Err(CamelError::TypeConversionFailed(
101 "ZipDataFormat::unmarshal ZIP archive has no entries".to_string(),
102 ));
103 }
104
105 if archive.len() > 1 && !self.config.allow_multi_entry {
106 return Err(CamelError::TypeConversionFailed(format!(
107 "ZipDataFormat::unmarshal ZIP has {} entries but allow_multi_entry is false",
108 archive.len()
109 )));
110 }
111
112 if archive.len() > 1 {
113 tracing::warn!(
114 entries = archive.len(),
115 "ZIP archive has multiple entries, extracting first only"
116 );
117 }
118
119 let mut entry = archive.by_index(0).map_err(|e| {
120 CamelError::TypeConversionFailed(format!(
121 "ZipDataFormat::unmarshal failed to read entry: {e}"
122 ))
123 })?;
124
125 let mut decompressed = Vec::new();
126 let limit = self.config.max_decompressed_size.saturating_add(1);
127 let mut limited = std::io::Read::take(&mut entry, limit);
128 limited.read_to_end(&mut decompressed).map_err(|e| {
129 CamelError::TypeConversionFailed(format!(
130 "ZipDataFormat::unmarshal failed to decompress: {e}"
131 ))
132 })?;
133
134 if decompressed.len() as u64 > self.config.max_decompressed_size {
135 return Err(CamelError::TypeConversionFailed(format!(
136 "ZipDataFormat::unmarshal decompressed size exceeds max {}",
137 self.config.max_decompressed_size
138 )));
139 }
140
141 Ok(Body::Bytes(Bytes::from(decompressed)))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use bytes::Bytes;
149 use serde_json::json;
150 use std::io::Cursor;
151 use std::io::Read;
152 use zip::ZipArchive;
153
154 fn extract_single_entry(zip_bytes: &[u8]) -> Vec<u8> {
155 let reader = Cursor::new(zip_bytes);
156 let mut archive = ZipArchive::new(reader).unwrap();
157 let mut entry = archive.by_index(0).unwrap();
158 let name = entry.name().to_string();
159 assert_eq!(name, "payload");
160 let mut buf = Vec::new();
161 entry.read_to_end(&mut buf).unwrap();
162 buf
163 }
164
165 #[test]
166 fn test_name() {
167 let df = ZipDataFormat::default();
168 assert_eq!(df.name(), "zip");
169 }
170
171 #[test]
172 fn test_marshal_text_to_zip() {
173 let df = ZipDataFormat::default();
174 let body = Body::Text("hello world".to_string());
175 let result = df.marshal(body).unwrap();
176 match result {
177 Body::Bytes(b) => {
178 let decompressed = extract_single_entry(&b);
179 assert_eq!(decompressed, b"hello world");
180 }
181 _ => panic!("expected Body::Bytes"),
182 }
183 }
184
185 #[test]
186 fn test_marshal_json_to_zip() {
187 let df = ZipDataFormat::default();
188 let body = Body::Json(json!({"key": "value"}));
189 let result = df.marshal(body).unwrap();
190 match result {
191 Body::Bytes(b) => {
192 let decompressed = extract_single_entry(&b);
193 let original = serde_json::to_vec(&json!({"key": "value"})).unwrap();
194 assert_eq!(decompressed, original);
195 }
196 _ => panic!("expected Body::Bytes"),
197 }
198 }
199
200 #[test]
201 fn test_marshal_bytes_to_zip() {
202 let df = ZipDataFormat::default();
203 let body = Body::Bytes(Bytes::from_static(b"raw bytes"));
204 let result = df.marshal(body).unwrap();
205 match result {
206 Body::Bytes(b) => {
207 let decompressed = extract_single_entry(&b);
208 assert_eq!(decompressed, b"raw bytes");
209 }
210 _ => panic!("expected Body::Bytes"),
211 }
212 }
213
214 #[test]
215 fn test_marshal_xml_to_zip() {
216 let df = ZipDataFormat::default();
217 let body = Body::Xml("<root><item>1</item></root>".to_string());
218 let result = df.marshal(body).unwrap();
219 match result {
220 Body::Bytes(b) => {
221 let decompressed = extract_single_entry(&b);
222 assert_eq!(decompressed, b"<root><item>1</item></root>");
223 }
224 _ => panic!("expected Body::Bytes"),
225 }
226 }
227
228 #[test]
229 fn test_marshal_empty_error() {
230 let df = ZipDataFormat::default();
231 let result = df.marshal(Body::Empty);
232 assert!(result.is_err());
233 }
234
235 #[test]
236 fn test_marshal_stream_error() {
237 use camel_api::body::{StreamBody, StreamMetadata};
238 use futures::stream;
239 use std::sync::Arc;
240 use tokio::sync::Mutex;
241
242 let stream = stream::iter(vec![Ok(Bytes::from_static(b"data"))]);
243 let body = Body::Stream(StreamBody {
244 stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
245 metadata: StreamMetadata::default(),
246 });
247 let df = ZipDataFormat::default();
248 let result = df.marshal(body);
249 assert!(result.is_err());
250 }
251
252 fn make_zip(content: &[u8]) -> Vec<u8> {
253 let mut buf = Vec::new();
254 {
255 let mut writer = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
256 let options = zip::write::SimpleFileOptions::default()
257 .compression_method(zip::CompressionMethod::Deflated);
258 writer.start_file("payload", options).unwrap();
259 writer.write_all(content).unwrap();
260 writer.finish().unwrap();
261 }
262 buf
263 }
264
265 #[test]
266 fn test_unmarshal_zip_bytes() {
267 let df = ZipDataFormat::default();
268 let zip_data = make_zip(b"decompressed content");
269 let body = Body::Bytes(Bytes::from(zip_data));
270 let result = df.unmarshal(body).unwrap();
271 match result {
272 Body::Bytes(b) => assert_eq!(b.as_ref(), b"decompressed content"),
273 _ => panic!("expected Body::Bytes"),
274 }
275 }
276
277 #[test]
278 fn test_unmarshal_zip_text() {
279 let df = ZipDataFormat::default();
280 let content = b"text from text body";
281 let zip_data = make_zip(content);
282 let body = Body::Bytes(Bytes::from(zip_data));
283 let result = df.unmarshal(body).unwrap();
284 match result {
285 Body::Bytes(b) => assert_eq!(b.as_ref(), content),
286 _ => panic!("expected Body::Bytes"),
287 }
288 }
289
290 #[test]
291 fn test_unmarshal_invalid_zip_error() {
292 let df = ZipDataFormat::default();
293 let body = Body::Bytes(Bytes::from_static(b"not a zip file"));
294 let result = df.unmarshal(body);
295 assert!(result.is_err());
296 }
297
298 #[test]
299 fn test_unmarshal_empty_zip_error() {
300 let mut buf = Vec::new();
301 {
302 let writer = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
303 writer.finish().unwrap();
304 }
305 let df = ZipDataFormat::default();
306 let body = Body::Bytes(Bytes::from(buf));
307 let result = df.unmarshal(body);
308 assert!(result.is_err());
309 }
310
311 #[test]
312 fn test_unmarshal_json_error() {
313 let df = ZipDataFormat::default();
314 let body = Body::Json(json!({"not": "zip"}));
315 let result = df.unmarshal(body);
316 assert!(result.is_err());
317 }
318
319 #[test]
320 fn test_unmarshal_xml_error() {
321 let df = ZipDataFormat::default();
322 let body = Body::Xml("<root/>".to_string());
323 let result = df.unmarshal(body);
324 assert!(result.is_err());
325 }
326
327 #[test]
328 fn test_unmarshal_multi_entry_error() {
329 let mut buf = Vec::new();
330 {
331 let mut writer = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
332 let options = zip::write::SimpleFileOptions::default();
333 writer.start_file("file1.txt", options).unwrap();
334 writer.write_all(b"one").unwrap();
335 writer.start_file("file2.txt", options).unwrap();
336 writer.write_all(b"two").unwrap();
337 writer.finish().unwrap();
338 }
339 let df = ZipDataFormat::default();
340 let body = Body::Bytes(Bytes::from(buf));
341 let result = df.unmarshal(body);
342 assert!(result.is_err());
343 }
344
345 #[test]
346 fn test_unmarshal_multi_entry_allowed() {
347 let mut buf = Vec::new();
348 {
349 let mut writer = zip::ZipWriter::new(std::io::Cursor::new(&mut buf));
350 let options = zip::write::SimpleFileOptions::default();
351 writer.start_file("file1.txt", options).unwrap();
352 writer.write_all(b"first").unwrap();
353 writer.start_file("file2.txt", options).unwrap();
354 writer.write_all(b"second").unwrap();
355 writer.finish().unwrap();
356 }
357 let config = ZipConfig {
358 allow_multi_entry: true,
359 ..Default::default()
360 };
361 let df = ZipDataFormat::new(config);
362 let body = Body::Bytes(Bytes::from(buf));
363 let result = df.unmarshal(body).unwrap();
364 match result {
365 Body::Bytes(b) => assert_eq!(b.as_ref(), b"first"),
366 _ => panic!("expected Body::Bytes"),
367 }
368 }
369
370 #[test]
371 fn test_roundtrip_text() {
372 let df = ZipDataFormat::default();
373 let original = Body::Text("roundtrip text content".to_string());
374 let compressed = df.marshal(original).unwrap();
375 let decompressed = df.unmarshal(compressed).unwrap();
376 match decompressed {
377 Body::Bytes(b) => assert_eq!(b.as_ref(), b"roundtrip text content"),
378 _ => panic!("expected Body::Bytes"),
379 }
380 }
381
382 #[test]
383 fn test_roundtrip_json() {
384 let df = ZipDataFormat::default();
385 let original = Body::Json(json!({"round": "trip"}));
386 let compressed = df.marshal(original).unwrap();
387 let decompressed = df.unmarshal(compressed).unwrap();
388 match decompressed {
389 Body::Bytes(b) => {
390 let v: serde_json::Value = serde_json::from_slice(&b).unwrap();
391 assert_eq!(v, json!({"round": "trip"}));
392 }
393 _ => panic!("expected Body::Bytes"),
394 }
395 }
396
397 #[test]
398 fn test_roundtrip_bytes() {
399 let df = ZipDataFormat::default();
400 let original = Body::Bytes(Bytes::from_static(b"\x00\x01\x02\xff"));
401 let compressed = df.marshal(original).unwrap();
402 let decompressed = df.unmarshal(compressed).unwrap();
403 match decompressed {
404 Body::Bytes(b) => assert_eq!(b.as_ref(), b"\x00\x01\x02\xff"),
405 _ => panic!("expected Body::Bytes"),
406 }
407 }
408
409 #[test]
410 fn test_max_decompressed_size_exceeded() {
411 let config = ZipConfig {
412 max_decompressed_size: 10,
413 ..Default::default()
414 };
415 let df = ZipDataFormat::new(config);
416 let zip_data = make_zip(b"this content is way longer than 10 bytes");
417 let body = Body::Bytes(Bytes::from(zip_data));
418 let result = df.unmarshal(body);
419 assert!(result.is_err());
420 }
421
422 #[test]
423 fn test_unmarshal_empty_error() {
424 let df = ZipDataFormat::default();
425 let result = df.unmarshal(Body::Empty);
426 assert!(result.is_err());
427 }
428
429 #[test]
430 fn test_unmarshal_stream_error() {
431 use camel_api::body::{StreamBody, StreamMetadata};
432 use futures::stream;
433 use std::sync::Arc;
434 use tokio::sync::Mutex;
435
436 let stream = stream::iter(vec![Ok(Bytes::from_static(b"data"))]);
437 let body = Body::Stream(StreamBody {
438 stream: Arc::new(Mutex::new(Some(Box::pin(stream)))),
439 metadata: StreamMetadata::default(),
440 });
441 let df = ZipDataFormat::default();
442 let result = df.unmarshal(body);
443 assert!(result.is_err());
444 }
445
446 #[test]
447 fn test_marshal_invalid_compression_level() {
448 let config = ZipConfig {
449 compression_level: Some(42),
450 ..Default::default()
451 };
452 let df = ZipDataFormat::new(config);
453 let result = df.marshal(Body::Text("test".to_string()));
454 assert!(result.is_err());
455 }
456
457 #[test]
458 fn test_marshal_input_size_cap() {
459 let config = ZipConfig {
460 max_input_size: 16,
461 ..Default::default()
462 };
463 let df = ZipDataFormat::new(config);
464 let body = Body::Text("x".repeat(64));
465 let result = df.marshal(body);
466 assert!(result.is_err());
467 let msg = format!("{}", result.unwrap_err());
468 assert!(
469 msg.contains("max_input_size"),
470 "error should mention max_input_size: {msg}"
471 );
472 }
473
474 #[test]
475 fn test_marshal_default_cap_accepts_small_input() {
476 let df = ZipDataFormat::default();
477 let body = Body::Text("hello".to_string());
478 let result = df.marshal(body).unwrap();
479 assert!(matches!(result, Body::Bytes(_)));
480 }
481
482 #[test]
483 fn test_builtin_zip_registered() {
484 let df = super::super::builtin_data_format("zip");
485 assert!(df.is_some());
486 assert_eq!(df.unwrap().name(), "zip");
487 }
488
489 #[test]
490 fn test_zip_config_deserialize_from_json() {
491 let json = serde_json::json!({
492 "max_decompressed_size": 2147483648u64,
493 "max_input_size": 134217728u64,
494 "compression_level": 6,
495 "allow_multi_entry": true
496 });
497 let cfg: ZipConfig = serde_json::from_value(json).unwrap();
498 assert_eq!(cfg.max_decompressed_size, 2147483648);
499 assert_eq!(cfg.compression_level, Some(6));
500 assert!(cfg.allow_multi_entry);
501 }
502
503 #[test]
504 fn test_zip_config_deny_unknown_fields() {
505 let json = serde_json::json!({"unknown_key": 42});
506 let result: Result<ZipConfig, _> = serde_json::from_value(json);
507 assert!(result.is_err());
508 }
509}