Spaces:
Runtime error
Runtime error
| import path from "path"; | |
| import { unzipSync, type Unzipped } from "fflate"; | |
| export interface ExtractedZipFile { | |
| name: string; | |
| content: string; | |
| } | |
| export interface ExtractZipOptions { | |
| maxFiles?: number; | |
| maxFileSizeBytes?: number; | |
| maxTotalSizeBytes?: number; | |
| } | |
| const DEFAULT_MAX_FILES = 50; | |
| const DEFAULT_MAX_FILE_SIZE = 256 * 1024; | |
| const DEFAULT_MAX_TOTAL = 10 * 1024 * 1024; | |
| function isSafeEntryName(name: string): boolean { | |
| if (!name.toLowerCase().endsWith(".json")) return false; | |
| if (name.includes("..")) return false; | |
| if (path.isAbsolute(name)) return false; | |
| if (/[\r\n\0]/.test(name)) return false; | |
| return true; | |
| } | |
| export function extractCodexAuthZip( | |
| zipBuffer: Buffer, | |
| options: ExtractZipOptions = {} | |
| ): ExtractedZipFile[] { | |
| const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES; | |
| const maxFileSize = options.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE; | |
| const maxTotal = options.maxTotalSizeBytes ?? DEFAULT_MAX_TOTAL; | |
| let unzipped: Unzipped; | |
| try { | |
| unzipped = unzipSync(new Uint8Array(zipBuffer)); | |
| } catch { | |
| throw new Error("Could not parse ZIP archive β file may be corrupt or not a valid ZIP"); | |
| } | |
| const entries = Object.entries(unzipped).filter(([, data]) => data !== undefined); | |
| const jsonEntries = entries.filter(([name]) => name.toLowerCase().endsWith(".json")); | |
| if (jsonEntries.length === 0) { | |
| throw new Error("ZIP archive contains no .json files"); | |
| } | |
| if (jsonEntries.length > maxFiles) { | |
| throw new Error( | |
| `ZIP archive contains ${jsonEntries.length} .json files β max allowed is ${maxFiles}` | |
| ); | |
| } | |
| let totalBytes = 0; | |
| const result: ExtractedZipFile[] = []; | |
| for (const [entryName, data] of jsonEntries) { | |
| const baseName = path.basename(entryName); | |
| if (!isSafeEntryName(baseName)) { | |
| throw new Error( | |
| `ZIP entry "${baseName}" has an unsafe filename (must be a .json file without path traversal)` | |
| ); | |
| } | |
| if (!isSafeEntryName(entryName)) { | |
| throw new Error( | |
| `ZIP entry path "${entryName}" is unsafe (no "..", absolute paths, or control characters allowed)` | |
| ); | |
| } | |
| if (data.byteLength > maxFileSize) { | |
| throw new Error( | |
| `ZIP entry "${baseName}" is ${data.byteLength} bytes β exceeds ${maxFileSize} byte limit per file` | |
| ); | |
| } | |
| totalBytes += data.byteLength; | |
| if (totalBytes > maxTotal) { | |
| throw new Error(`ZIP archive total uncompressed size exceeds ${maxTotal} byte limit`); | |
| } | |
| const content = new TextDecoder("utf-8").decode(data); | |
| result.push({ name: baseName, content }); | |
| } | |
| return result; | |
| } | |