{"record":{"id":"0f4c5e082c5ad92d","repo":"zeroclaw-labs/zeroclaw","slug":"plugin-archive-entry-has-no-parent","errorCode":null,"errorMessage":"plugin archive entry has no parent: {}","messagePattern":"plugin archive entry has no parent: (.+?)","errorType":"console","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src/plugin_registry.rs","lineNumber":189,"sourceCode":"    dest: &Path,\n    max_extracted_bytes: u64,\n) -> Result<PathBuf>\nwhere\n    R: Read + Seek,\n{\n    let mut archive = zip::ZipArchive::new(reader)?;\n    std::fs::create_dir_all(dest)?;\n    let mut extracted_bytes = 0_u64;\n    for i in 0..archive.len() {\n        let mut file = archive.by_index(i)?;\n        let enclosed = enclosed_zip_path(file.name(), &file)?;\n        let out_path = dest.join(enclosed);\n        if file.is_dir() {\n            std::fs::create_dir_all(&out_path)?;\n            continue;\n        }\n        let Some(parent) = out_path.parent() else {\n            bail!(\"plugin archive entry has no parent: {}\", file.name());\n        };\n        if extracted_bytes.saturating_add(file.size()) > max_extracted_bytes {\n            bail!(\"plugin archive exceeds extracted size limit of {max_extracted_bytes} bytes\");\n        }\n        std::fs::create_dir_all(parent)?;\n        let mut out = File::create(&out_path)?;\n        copy_zip_entry_capped(\n            &mut file,\n            &mut out,\n            &mut extracted_bytes,\n            max_extracted_bytes,\n        )?;\n    }\n    Ok(dest.to_path_buf())\n}\n\nfn enclosed_zip_path<R>(raw_name: &str, file: &zip::read::ZipFile<'_, R>) -> Result<PathBuf>\nwhere","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/src/plugin_registry.rs#L171-L207","documentation":"During safe extraction, a zip entry resolved to an output path that has no parent directory (out_path.parent() returned None). That only happens for a path that is a root/componentless target — a degenerate archive entry (e.g. name \"\" or \"/\"), since even \"file.txt\" yields a parent. extract_zip_safe_with_limit bails with the offending entry name.","triggerScenarios":"Extracting a plugin archive containing an entry whose (sanitized) path is empty or root-level with no directory component. Reached from extract_zip_safe during plugin install after the archive passes the download checks.","commonSituations":"Hand-rolled zip scripts appending an empty-named entry; corrupted zip metadata (zeroed name field); zip tools emitting odd directory markers; a fuzzed/maliciously crafted archive.","solutions":["Rebuild the plugin archive with a standard tool (cargo zip, `zip -r`) so every entry has a normal relative path","Inspect the archive to find the degenerate entry: `unzip -l plugin.zip` and look for blank/root entries","Verify the archive's sha256 against the registry to rule out corruption in transit","If you control the packer, filter out empty/root entries before writing the zip"],"exampleFix":"# before: zip contains an entry named \"\" (or \"/\")\n# after: rebuild cleanly\ncd plugin-root && zip -r ../p-0.1.0.zip . -x '*.DS_Store'\n# republish entry url+sha256 for the new archive","handlingStrategy":"try-catch","validationCode":"// Reject degenerate entries before extraction:\nfor i in 0..archive.len() {\n    let f = archive.by_index(i)?;\n    if f.enclosed_name().is_none() || f.name().is_empty() { /* reject archive */ }\n}","typeGuard":null,"tryCatchPattern":"// Catch 'plugin archive entry has no parent', quarantine the archive, and\n// rebuild it with a standard zipper — retrying the same bytes is pointless.","preventionTips":["Build plugin zips with mainstream tools, not ad-hoc scripts","Validate archives in CI with `unzip -t` before publishing","Prefer re-downloading from the publisher over patching broken zips by hand"],"tags":["plugin-registry","zip","extract","malformed-archive"],"backgroundTag":"malformed-zip-archive","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}