{"record":{"id":"1b2be984af192476","repo":"siyuan-note/siyuan","slug":"unzip-failed-s","errorCode":null,"errorMessage":"unzip failed: %s","messagePattern":"unzip failed: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill.go","lineNumber":742,"sourceCode":"// installFromZip 解压 zip 并安装其中的 skill\nfunc installFromZip(data []byte) (*InstallSkillResult, error) {\n\ttmpRoot := filepath.Join(TempDir, \"ai\", \"skill-install\", gulu.Rand.String(7))\n\tif err := os.MkdirAll(tmpRoot, 0755); err != nil {\n\t\treturn nil, err\n\t}\n\tdefer os.RemoveAll(tmpRoot)\n\n\tzipPath := filepath.Join(tmpRoot, \"src.zip\")\n\tif err := os.WriteFile(zipPath, data, 0644); err != nil {\n\t\treturn nil, err\n\t}\n\tunzipDir := filepath.Join(tmpRoot, \"unzip\")\n\tif err := os.MkdirAll(unzipDir, 0755); err != nil {\n\t\treturn nil, err\n\t}\n\t// gulu.Zip.Unzip 已内置 zip-slip 路径穿越防护\n\tif err := gulu.Zip.Unzip(zipPath, unzipDir); err != nil {\n\t\treturn nil, errors.New(\"unzip failed: \" + err.Error())\n\t}\n\n\tskillDirs := findSkillDirs(unzipDir)\n\tif len(skillDirs) == 0 {\n\t\treturn nil, errors.New(\"no SKILL.md found in the archive\")\n\t}\n\treturn installSkillDirs(skillDirs, unzipDir)\n}\n\n// findSkillDirs 在解压根下查找含 SKILL.md 的 skill 目录，返回相对 root 的路径。\n// 递归下钻以兼容任意包裹层（codeload 会把仓库内容包在 <repo-name>/ 下），\n// 但一旦某个目录被认定为 skill（直接含 SKILL.md）就停止下钻，避免误入 skill 内部的\n// references/scripts 等子目录。识别的结构：\n//   - SKILL.md 直接在 root（无包裹）\n//   - <wrap>/SKILL.md（单层或多层包裹的单 skill）\n//   - <wrap>/skills/<name>/SKILL.md（集合仓库，wrap 可有可无）\nfunc findSkillDirs(root string) []string {\n\tif gulu.File.IsExist(filepath.Join(root, \"SKILL.md\")) {","sourceCodeStart":724,"sourceCodeEnd":760,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/skill.go#L724-L760","documentation":"installFromZip writes the downloaded bytes to a temp zip and extracts it with gulu.Zip.Unzip; any extraction error is wrapped as 'unzip failed: <cause>'. gulu.Zip.Unzip also includes zip-slip protection, so an archive with entries escaping the target directory fails here too.","triggerScenarios":"The downloaded bytes are not a valid zip (HTML error page saved as zip, truncated download, wrong Content-Type guess), or the archive contains entries with unsafe paths blocked by the zip-slip guard, or disk I/O errors during extraction.","commonSituations":"A direct-link source that is not actually a zip (server returned an HTML login/rate-limit page); a corrupted or partial download; an archive crafted with ../ entry names; temp disk full.","solutions":["Verify the URL actually serves a zip file — open it in a browser; an HTML page means the URL is wrong or rate-limited","Re-download and retry in case of a truncated/corrupted transfer","Check the wrapped cause: a zip-slip message indicates a malicious or malformed archive — use a trusted source","Check free disk space in the temp directory if the cause is a write error"],"exampleFix":"// before\nInstallSkill(\"https://example.com/skill\") // serves HTML, not a zip\n// after\nInstallSkill(\"https://example.com/skill.zip\")","handlingStrategy":"validation","validationCode":"const r = await fetch(downloadUrl)\nconst ct = r.headers.get(\"content-type\") || \"\"\nconst head = new Uint8Array(await (await r.blob()).slice(0, 4).arrayBuffer())\nconst isZip = ct.includes(\"zip\") || (head[0] === 0x50 && head[1] === 0x4b)\nif (!isZip) throw new Error(\"URL does not serve a zip file\")","typeGuard":"const looksLikeZip = (bytes) => bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04","tryCatchPattern":"try {\n  await installSkill(zipUrl)\n} catch (e) {\n  if (String(e).startsWith(\"unzip failed:\")) {\n    if (String(e).includes(\"slip\") || String(e).includes(\"path\")) {\n      reportUntrustedArchive(zipUrl)\n    } else {\n      showHint(\"URL did not return a valid zip; check it in a browser\")\n    }\n  }\n}","preventionTips":["Only point at URLs that actually serve .zip content (verify in a browser first)","Treat a zip-slip-wrapped failure as a sign of a malicious/malformed archive — change the source","Retry once on corruption; persistent failures mean the source itself is bad"],"tags":["zip","archive","install","validation"],"backgroundTag":"invalid-archive-format","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}