Skip ZIP symlink and special members when extracting (#568)

Fixes #520. Supersedes #521. core_archive_extract skips ZIP symlink, device, FIFO and socket members and names each in skipped, instead of writing a symlink as a file holding its target.

Co-authored-by: NetterSilver <133535268+Silvertree2010@users.noreply.github.com>
This commit is contained in:
BFlat
2026-11-03 15:41:50 +07:00
committed by GitHub
parent e1d1974468
commit 6bbcb0d1bd
2 changed files with 46 additions and 0 deletions
+26
View File
@@ -686,6 +686,10 @@ fn core_archive_extract(ctx: &mut Ctx<'_>, params: Value) -> Result<ToolOutput,
"zip" | "apk" | "ipa" | "jar" | "hap" | "msix" => {
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();
+20
View File
@@ -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<u32>,
}
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<ZipArchive, ZipError> {
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<ZipArchive, ZipError> {
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]);