diff --git a/crates/engine/src/tools/artifact_tools.rs b/crates/engine/src/tools/artifact_tools.rs index cbf21fef..7a3299d3 100644 --- a/crates/engine/src/tools/artifact_tools.rs +++ b/crates/engine/src/tools/artifact_tools.rs @@ -686,6 +686,10 @@ fn core_archive_extract(ctx: &mut Ctx<'_>, params: Value) -> Result { let z = zip::parse(&bytes, p.max_files.saturating_mul(2)).map_err(|e| ToolError::new(ErrorCode::GarbledOutput, e.to_string()))?; for e in z.entries.iter().filter(|e| !e.is_dir) { + if let Some(kind) = e.special_kind() { + members.push((e.name.clone(), Member::Skip(format!("{kind} member is not a regular file")))); + continue; + } match zip::read_entry(&bytes, e, p.max_bytes) { Ok(d) => members.push((e.name.clone(), Member::Owned(d))), Err(err @ zip::ZipError::CrcMismatch { .. }) => { @@ -909,6 +913,28 @@ mod tests { assert_eq!(x["result"]["files_written"], 1); } + #[test] + fn zip_symlink_member_is_skipped() { + let dir = tempfile::tempdir().unwrap(); + let z = dir.path().join("link.zip"); + let mut b = fx::zip_stored(&[("good.txt", b"regular\n"), ("link_out", b"../../target.txt")]); + let at = (0..b.len().saturating_sub(54)).find(|&i| b.get(i..i + 4) == Some(b"PK\x01\x02") && b.get(i + 46..i + 54) == Some(b"link_out")).unwrap(); + b[at + 5] = 3; + b[at + 38..at + 42].copy_from_slice(&((0o120777u32) << 16).to_le_bytes()); + std::fs::write(&z, b).unwrap(); + let e = Engine::new(crate::testing::session()); + let x = call(&e, "core_archive_extract", json!({"path": z})).unwrap(); + assert_eq!(x["result"]["files_written"], 1); + let sk = x["result"]["skipped"].as_array().unwrap(); + assert_eq!(sk.len(), 1); + assert_eq!(sk[0]["path"], "link_out"); + assert!(sk[0]["reason"].as_str().unwrap().contains("symlink")); + let out = PathBuf::from(x["result"]["output_directory"].as_str().unwrap()); + assert!(out.join("good.txt").exists()); + assert!(!out.join("link_out").exists()); + std::fs::remove_dir_all(out).unwrap(); + } + #[test] fn compare_two_folders_and_evidence_inputs() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/formats/src/zip.rs b/crates/formats/src/zip.rs index 43fa48ed..33670d08 100644 --- a/crates/formats/src/zip.rs +++ b/crates/formats/src/zip.rs @@ -40,9 +40,24 @@ pub struct ZipEntry { pub local_header_offset: u64, pub is_dir: bool, pub encrypted: bool, + /// Unix mode from the external attributes (only when the entry was made on a Unix host). + #[serde(skip)] + pub unix_mode: Option, } impl ZipEntry { + /// Why this entry is not a regular file or directory (symlink, device, FIFO, socket), if so. + pub fn special_kind(&self) -> Option<&'static str> { + match self.unix_mode? & 0o170000 { + 0o120000 => Some("symlink"), + 0o020000 => Some("character device"), + 0o060000 => Some("block device"), + 0o010000 => Some("FIFO"), + 0o140000 => Some("socket"), + _ => None, + } + } + /// Human name of the compression method. pub fn method_name(&self) -> &'static str { match self.method { @@ -130,6 +145,8 @@ pub fn parse(data: &[u8], max_entries: usize) -> Result { let name_len = usize::from(r.u16_le(pos + 28).ok_or(ZipError::Truncated("directory entry"))?); let extra_len = usize::from(r.u16_le(pos + 30).ok_or(ZipError::Truncated("directory entry"))?); let comment_len = usize::from(r.u16_le(pos + 32).ok_or(ZipError::Truncated("directory entry"))?); + let host = r.slice(pos + 5, 1).and_then(|b| b.first().copied()).unwrap_or(0); + let unix_mode = if host == 3 { r.u32_le(pos + 38).map(|a| a >> 16) } else { None }; let mut local = u64::from(r.u32_le(pos + 42).ok_or(ZipError::Truncated("directory entry"))?); let name_bytes = r.slice(pos + 46, name_len).ok_or(ZipError::Truncated("entry name"))?; let extra = r.slice(pos + 46 + name_len, extra_len).unwrap_or(&[]); @@ -172,6 +189,7 @@ pub fn parse(data: &[u8], max_entries: usize) -> Result { uncompressed_size: usize_, local_header_offset: local.saturating_add(prefix), encrypted: flags & 1 != 0, + unix_mode, }); seen += 1; pos = pos.checked_add(46 + name_len + extra_len + comment_len).ok_or_else(|| ZipError::Corrupt("directory overflow".into()))?; @@ -386,6 +404,7 @@ mod tests { local_header_offset: 0, is_dir: false, encrypted: false, + unix_mode: None, }; assert_eq!(read_entry(&file, &e, 100).unwrap(), body); assert!(matches!(read_entry(&file, &e, 3), Err(ZipError::TooLarge(3)))); @@ -407,6 +426,7 @@ mod tests { local_header_offset: 0, is_dir: false, encrypted: false, + unix_mode: None, }; let mut z = b"PK\x03\x04".to_vec(); z.extend_from_slice(&[0; 26]);