use std::path::Path;
use camel_api::CamelError;
use serde::Deserialize;
pub(crate) const DEFAULT_MAX_PATH_LENGTH: usize = 4096;
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DuplicatePolicy {
#[default]
AllowWithIndex,
Reject,
}
pub(crate) fn validate_entry_path(
path: &str,
max_length: usize,
kind: &str,
) -> Result<String, CamelError> {
if path.len() > max_length {
return Err(CamelError::TypeConversionFailed(format!(
"{kind} entry path exceeds max length: {} > {}",
path.len(),
max_length
)));
}
if path.contains('\0') {
return Err(CamelError::TypeConversionFailed(format!(
"{kind} entry path contains NUL byte"
)));
}
if Path::new(path).is_absolute() {
return Err(CamelError::TypeConversionFailed(format!(
"{kind} entry path is absolute: {path}"
)));
}
for component in Path::new(path).components() {
if let std::path::Component::ParentDir = component {
return Err(CamelError::TypeConversionFailed(format!(
"{kind} entry path contains '..' traversal: {path}"
)));
}
}
if path.contains('\\') {
return Err(CamelError::TypeConversionFailed(format!(
"{kind} entry path contains backslash: {path}"
)));
}
if let Some(c) = path.chars().next()
&& c.is_ascii_alphabetic()
&& path.chars().nth(1) == Some(':')
{
return Err(CamelError::TypeConversionFailed(format!(
"{kind} entry path contains Windows drive prefix: {path}"
)));
}
Ok(path.to_string())
}
pub(crate) fn indexed_duplicate_name(name: &str, occurrence: usize) -> String {
match name.rsplit_once('.') {
Some((stem, ext)) if !stem.is_empty() && !ext.is_empty() => {
format!("{stem}.{occurrence}.{ext}")
}
_ => format!("{name}.{occurrence}"),
}
}
pub(crate) fn next_free_indexed_name(
base: &str,
start: usize,
emitted: &std::collections::HashSet<String>,
) -> (String, usize) {
let mut occurrence = start.max(1);
loop {
let candidate = indexed_duplicate_name(base, occurrence);
if !emitted.contains(&candidate) {
return (candidate, occurrence);
}
occurrence += 1;
}
}
#[cfg(test)]
pub(crate) mod test_util {
pub(crate) fn crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= u32::from(byte);
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}
pub(crate) fn make_zip_raw(entries: &[(&str, &[u8])]) -> Vec<u8> {
struct Central {
name: String,
crc: u32,
size: u32,
offset: u32,
}
let mut out = Vec::new();
let mut centrals = Vec::with_capacity(entries.len());
for (name, data) in entries {
let offset = out.len() as u32;
let crc = crc32(data);
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());
let size = data.len() as u32;
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());
out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(name.as_bytes());
out.extend_from_slice(data);
centrals.push(Central {
name: (*name).to_string(),
crc,
size,
offset,
});
}
let cd_start = out.len() as u32;
for central in ¢rals {
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());
out.extend_from_slice(¢ral.size.to_le_bytes());
out.extend_from_slice(¢ral.size.to_le_bytes());
out.extend_from_slice(&(central.name.len() as u16).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(&0u16.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(¢ral.offset.to_le_bytes());
out.extend_from_slice(central.name.as_bytes());
}
let cd_size = out.len() as u32 - cd_start;
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());
out.extend_from_slice(&(centrals.len() as u16).to_le_bytes());
out.extend_from_slice(&cd_size.to_le_bytes());
out.extend_from_slice(&cd_start.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes()); out
}
#[test]
fn indexed_duplicate_name_is_deterministic_across_name_shapes() {
assert_eq!(super::indexed_duplicate_name("a.tar", 1), "a.1.tar");
assert_eq!(super::indexed_duplicate_name("a.tar", 3), "a.3.tar");
assert_eq!(super::indexed_duplicate_name("README", 1), "README.1");
assert_eq!(
super::indexed_duplicate_name("dir/file.bin", 2),
"dir/file.2.bin"
);
assert_eq!(super::indexed_duplicate_name(".hidden", 1), ".hidden.1");
assert_eq!(super::indexed_duplicate_name("name.", 1), "name..1");
}
}