{"record":{"id":"14c268939ea5d642","repo":"farion1231/cc-switch","slug":"archive-too-large","errorCode":"ARCHIVE_TOO_LARGE","errorMessage":"{\"code\":\"ARCHIVE_TOO_LARGE\",\"context\":{\"limit_mb\":\"{limit_mb}\"},\"suggestion\":\"checkZipContent\"}","messagePattern":"(.+?)\"\\},\"suggestion\":\"checkZipContent\"\\}","errorType":"error_code","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"src-tauri/src/services/skill.rs","lineNumber":3148,"sourceCode":"                &[(\"status\", &status)],\n                match status.as_str() {\n                    \"403\" => Some(\"http403\"),\n                    \"404\" => Some(\"http404\"),\n                    \"429\" => Some(\"http429\"),\n                    _ => Some(\"checkNetwork\"),\n                },\n            )));\n        }\n\n        // 逐块读并卡住压缩体大小：`response.bytes()` 会先把攻击者控制的整个归档\n        // 收进内存，之后才轮到 ZipArchive 和解压预算——那时候堆已经被吃光了。\n        // 不能只信 Content-Length（可以撒谎或缺失），必须按实际收到的字节数算。\n        let mut response = response;\n        let mut body: Vec<u8> = Vec::new();\n        while let Some(chunk) = response.chunk().await? {\n            if body.len().saturating_add(chunk.len()) as u64 > MAX_ARCHIVE_DOWNLOAD_BYTES {\n                let limit_mb = (MAX_ARCHIVE_DOWNLOAD_BYTES / 1024 / 1024).to_string();\n                return Err(anyhow::anyhow!(format_skill_error(\n                    \"ARCHIVE_TOO_LARGE\",\n                    &[(\"limit_mb\", &limit_mb)],\n                    Some(\"checkZipContent\"),\n                )));\n            }\n            body.extend_from_slice(&chunk);\n        }\n\n        let cursor = std::io::Cursor::new(body);\n        let archive = zip::ZipArchive::new(cursor)?;\n        Self::extract_repo_archive(archive, dest)\n    }\n\n    /// 按预算把单个归档条目写出，累计超限即中止。\n    ///\n    /// 逐块累加而非读取归档头里声明的 size：那个值由归档作者填写，压缩炸弹会撒谎。\n    fn copy_entry_within_budget<R: std::io::Read, W: std::io::Write>(\n        reader: &mut R,","sourceCodeStart":3130,"sourceCodeEnd":3166,"githubUrl":"https://github.com/farion1231/cc-switch/blob/a2e22f330273a5b6ffa87cb8b82b624601bac562/src-tauri/src/services/skill.rs#L3130-L3166","documentation":"Structured error from download_and_extract: the streamed response body exceeded MAX_ARCHIVE_DOWNLOAD_BYTES (128 MiB, skill.rs:327) while being read chunk-by-chunk into memory. This limit exists because the whole archive is buffered as a Vec<u8> before ZipArchive sees it — without the cap a multi-GB body would exhaust the heap before the extraction budget ever engages. Content-Length is deliberately not trusted; only bytes actually received are counted.","triggerScenarios":"Discovering/downloading a repo whose zip archive exceeds 128 MiB — typically repos bundling binaries, media, or vendored dependencies; or a malicious repo deliberately shipping a huge body (repos can be added via deeplink).","commonSituations":"Monorepos with assets; users expecting any GitHub repo to be installable; the doc comment notes skill repos are markdown, so 128 MiB is far beyond normal.","solutions":["Pick a leaner repo or a fork with assets stripped (git filter-repo / BFG) and add that instead","Download the skill files manually and import them as a local ZIP — but note the local path enforces the same entry/byte budgets","Only in a fork: raise MAX_ARCHIVE_DOWNLOAD_BYTES, understanding it raises peak memory usage of the app"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Rust — pre-flight size check via the GitHub API (size is in KB)\nasync fn repo_too_big(owner: &str, name: &str) -> Result<bool> {\n    let url = format!(\"https://api.github.com/repos/{owner}/{name}\");\n    let resp: serde_json::Value = crate::proxy::http_client::get().get(url).send().await?.json().await?;\n    Ok(resp[\"size\"].as_u64().unwrap_or(0) > 128 * 1024) // conservative proxy for archive size\n}","typeGuard":"export function isArchiveTooLarge(e: unknown): boolean {\n  return typeof e === \"string\" && e.includes('\"code\":\"ARCHIVE_TOO_LARGE\"');\n}","tryCatchPattern":"match download_repo(&repo).await {\n    Err(e) if e.to_string().contains(\"ARCHIVE_TOO_LARGE\") => {\n        // suggest a leaner repo or manual local import; do NOT retry — the size will not shrink\n    }\n    other => other,\n}","preventionTips":["Check the repo's size on GitHub (Insights or API 'size' field) before adding large projects","Keep skill repos markdown-only; strip binaries/media from history when curating","Do not raise MAX_ARCHIVE_DOWNLOAD_BYTES casually — it bounds peak heap usage of the whole response body"],"tags":["security","archive","limits","download","memory"],"backgroundTag":null,"analyzedSha":"a2e22f330273a5b6ffa87cb8b82b624601bac562","analyzedAt":"2026-08-16T03:46:07.889Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}