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