mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-05 20:47:45 +01:00
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:
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user