camel_processor/
archive_splitter.rs1use std::path::Path;
9
10use camel_api::CamelError;
11
12use serde::Deserialize;
13
14pub(crate) const DEFAULT_MAX_PATH_LENGTH: usize = 4096;
16
17#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
19#[serde(rename_all = "snake_case")]
20pub enum DuplicatePolicy {
21 #[default]
23 AllowWithIndex,
24 Reject,
26}
27
28pub(crate) fn validate_entry_path(
36 path: &str,
37 max_length: usize,
38 kind: &str,
39) -> Result<String, CamelError> {
40 if path.len() > max_length {
41 return Err(CamelError::TypeConversionFailed(format!(
42 "{kind} entry path exceeds max length: {} > {}",
43 path.len(),
44 max_length
45 )));
46 }
47
48 if path.contains('\0') {
49 return Err(CamelError::TypeConversionFailed(format!(
50 "{kind} entry path contains NUL byte"
51 )));
52 }
53
54 if Path::new(path).is_absolute() {
55 return Err(CamelError::TypeConversionFailed(format!(
56 "{kind} entry path is absolute: {path}"
57 )));
58 }
59
60 for component in Path::new(path).components() {
61 if let std::path::Component::ParentDir = component {
62 return Err(CamelError::TypeConversionFailed(format!(
63 "{kind} entry path contains '..' traversal: {path}"
64 )));
65 }
66 }
67
68 if path.contains('\\') {
69 return Err(CamelError::TypeConversionFailed(format!(
70 "{kind} entry path contains backslash: {path}"
71 )));
72 }
73
74 if let Some(c) = path.chars().next()
75 && c.is_ascii_alphabetic()
76 && path.chars().nth(1) == Some(':')
77 {
78 return Err(CamelError::TypeConversionFailed(format!(
79 "{kind} entry path contains Windows drive prefix: {path}"
80 )));
81 }
82
83 Ok(path.to_string())
84}
85
86pub(crate) fn indexed_duplicate_name(name: &str, occurrence: usize) -> String {
96 match name.rsplit_once('.') {
97 Some((stem, ext)) if !stem.is_empty() && !ext.is_empty() => {
98 format!("{stem}.{occurrence}.{ext}")
99 }
100 _ => format!("{name}.{occurrence}"),
101 }
102}
103
104pub(crate) fn next_free_indexed_name(
120 base: &str,
121 start: usize,
122 emitted: &std::collections::HashSet<String>,
123) -> (String, usize) {
124 let mut occurrence = start.max(1);
125 loop {
126 let candidate = indexed_duplicate_name(base, occurrence);
127 if !emitted.contains(&candidate) {
128 return (candidate, occurrence);
129 }
130 occurrence += 1;
131 }
132}
133
134#[cfg(test)]
135pub(crate) mod test_util {
136 pub(crate) fn crc32(data: &[u8]) -> u32 {
140 let mut crc: u32 = 0xFFFF_FFFF;
141 for &byte in data {
142 crc ^= u32::from(byte);
143 for _ in 0..8 {
144 let mask = (crc & 1).wrapping_neg();
145 crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
146 }
147 }
148 !crc
149 }
150
151 pub(crate) fn make_zip_raw(entries: &[(&str, &[u8])]) -> Vec<u8> {
154 struct Central {
155 name: String,
156 crc: u32,
157 size: u32,
158 offset: u32,
159 }
160
161 let mut out = Vec::new();
162 let mut centrals = Vec::with_capacity(entries.len());
163 for (name, data) in entries {
164 let offset = out.len() as u32;
165 let crc = crc32(data);
166 out.extend_from_slice(&0x0403_4b50_u32.to_le_bytes()); out.extend_from_slice(&20u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0x21u16.to_le_bytes()); out.extend_from_slice(&crc.to_le_bytes());
173 let size = data.len() as u32;
174 out.extend_from_slice(&size.to_le_bytes()); out.extend_from_slice(&size.to_le_bytes()); out.extend_from_slice(&(name.len() as u16).to_le_bytes());
177 out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(name.as_bytes());
179 out.extend_from_slice(data);
180 centrals.push(Central {
181 name: (*name).to_string(),
182 crc,
183 size,
184 offset,
185 });
186 }
187
188 let cd_start = out.len() as u32;
189 for central in ¢rals {
190 out.extend_from_slice(&0x0201_4b50_u32.to_le_bytes()); out.extend_from_slice(&20u16.to_le_bytes()); out.extend_from_slice(&20u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0x21u16.to_le_bytes()); out.extend_from_slice(¢ral.crc.to_le_bytes());
198 out.extend_from_slice(¢ral.size.to_le_bytes());
199 out.extend_from_slice(¢ral.size.to_le_bytes());
200 out.extend_from_slice(&(central.name.len() as u16).to_le_bytes());
201 out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(¢ral.offset.to_le_bytes());
207 out.extend_from_slice(central.name.as_bytes());
208 }
209 let cd_size = out.len() as u32 - cd_start;
210
211 out.extend_from_slice(&0x0605_4b50_u32.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&(centrals.len() as u16).to_le_bytes());
215 out.extend_from_slice(&(centrals.len() as u16).to_le_bytes());
216 out.extend_from_slice(&cd_size.to_le_bytes());
217 out.extend_from_slice(&cd_start.to_le_bytes());
218 out.extend_from_slice(&0u16.to_le_bytes()); out
220 }
221
222 #[test]
228 fn indexed_duplicate_name_is_deterministic_across_name_shapes() {
229 assert_eq!(super::indexed_duplicate_name("a.tar", 1), "a.1.tar");
230 assert_eq!(super::indexed_duplicate_name("a.tar", 3), "a.3.tar");
231 assert_eq!(super::indexed_duplicate_name("README", 1), "README.1");
232 assert_eq!(
233 super::indexed_duplicate_name("dir/file.bin", 2),
234 "dir/file.2.bin"
235 );
236 assert_eq!(super::indexed_duplicate_name(".hidden", 1), ".hidden.1");
238 assert_eq!(super::indexed_duplicate_name("name.", 1), "name..1");
240 }
241}