import { type ArchiveSink } from "../io/archive-sink.js"; import { type ArchiveSource } from "../io/archive-source.js"; import type { RandomAccessReader, HttpRangeReaderOptions } from "../io/random-access.js"; import { type ZipStringEncoding } from "../shared/text.js"; import type { ZipEntryOptions } from "./index.js"; import type { ZipTimestampMode } from "../zip-spec/timestamps.js"; import type { ZipEntryInfo } from "../zip-spec/zip-entry-info.js"; import type { ZipPathOptions } from "../zip-spec/zip-path.js"; import type { Zip64Mode } from "../zip-spec/zip-records.js"; import type { ZipOperation, ZipProgress, ZipStreamOptions } from "./progress.js"; import type { ZipEditPlan } from "./zip-edit-plan.js"; /** * Warning issued when an entry cannot be preserved and will be skipped. */ export interface ZipEditWarning { /** The entry name that triggered the warning */ entry: string; /** Warning code for programmatic handling */ code: "raw_unavailable" | "encryption_unsupported" | "unknown"; /** Human-readable description */ message: string; } /** * Options for opening and editing a ZIP archive. */ export interface ZipEditOptions { /** Whether to decode entry names/comments from UTF-8. Default: true */ decodeStrings?: boolean; /** Optional string encoding for legacy (non-UTF8) names/comments. */ encoding?: ZipStringEncoding; /** Password for decrypting encrypted entries (only needed for extraction, not passthrough). */ password?: string | Uint8Array; /** * How to handle passthrough of unchanged entries. * * - `"strict"` (default): require raw passthrough data to be available; otherwise throw. * - `"best-effort"`: if raw passthrough data is unavailable, fall back to extracting and * re-adding the entry (may increase CPU/memory usage). If extraction also fails, the entry * is skipped and a warning is emitted (if `onWarning` is set). */ preserve?: "strict" | "best-effort"; /** Default compression level (0=store, 1-9=deflate). Default: 6 */ level?: number; /** Timestamp mode for new/updated entries. Default: "extended" (or "dos" if reproducible) */ timestamps?: ZipTimestampMode; /** Archive-level comment */ comment?: string; /** * Path normalization mode for entry names. * - `false` (default): no normalization, names are used as-is * - `{ mode: "safe" }`: normalize and reject unsafe paths (recommended) * - `{ mode: "posix" }`: normalize but allow any path * - `{ mode: "legacy" }`: minimal normalization (backslash → slash) */ path?: false | ZipPathOptions; /** Default modification time for new entries. */ modTime?: Date; /** * If true, use reproducible defaults: * - `modTime` = 1980-01-01 00:00:00 * - `timestamps` = "dos" */ reproducible?: boolean; /** If true (default), auto-store incompressible data instead of deflating. */ smartStore?: boolean; /** ZIP64 mode: "auto" (default), true (force), or false (disable). */ zip64?: Zip64Mode; /** AbortSignal for cancellation support */ signal?: AbortSignal; /** Progress callback */ onProgress?: (p: ZipProgress) => void; /** Minimum interval between progress updates (ms) */ progressIntervalMs?: number; /** * Callback for warnings (e.g., entries that cannot be preserved). * If not provided, warnings are silently ignored. */ onWarning?: (warning: ZipEditWarning) => void; } export interface ZipEditUrlOptions extends ZipEditOptions, HttpRangeReaderOptions { } /** * High-level editor for an existing ZIP archive. * * ## Features * - **Cross-platform**: Works in both Node.js and browsers (in-memory) * - **Filesystem-like API**: `set()`, `delete()`, `rename()`, `has()` * - **Efficient passthrough**: Unchanged entries preserve raw compressed bytes (no re-compression) * - **Dual output**: Both streaming (`stream()`) and non-streaming (`bytes()`) output * - **Progress & cancellation**: Full `AbortSignal` and progress callback support * * ## Usage * ```ts * const editor = await editZip(existingZipBytes); * editor.delete("old-file.txt"); * editor.set("new-file.txt", "hello world"); * editor.rename("foo.txt", "bar.txt"); * const output = await editor.bytes(); * ``` * * ## Path Normalization * By default, entry names are used as-is. Use the `path` option to enable normalization: * - `{ mode: "safe" }` — Recommended. Normalizes and rejects unsafe paths (absolute, `..` traversal). * - `{ mode: "posix" }` — Normalizes but allows any path. * - `{ mode: "legacy" }` — Minimal normalization (backslash → slash). * * ## Rename Semantics * `rename(from, to)` will **overwrite** `to` if it already exists (like `mv -f`). */ export declare class ZipEditor { private readonly _remote; private readonly _reader; private readonly _onWarning?; /** Original entries for quick lookup (never mutated). */ private readonly _baseEntries; /** Final output view managed by ZipEditView. */ private readonly _view; private readonly _options; private readonly _streamDefaults; private readonly _stringCodec; private constructor(); /** * Open an existing ZIP archive for editing. * * @param source - The ZIP data (Uint8Array, ArrayBuffer, Blob, string, or async iterable) * @param options - Edit options * @returns A new `ZipEditor` instance * * @example * ```ts * // From Uint8Array * const editor = await ZipEditor.open(zipBytes); * * // From fetch response * const response = await fetch("/archive.zip"); * const editor = await ZipEditor.open(response.body!); * * // With options * const editor = await ZipEditor.open(zipBytes, { * path: { mode: "safe" }, * onWarning: (w) => console.warn(w.message) * }); * ``` */ static open(source: ArchiveSource | RandomAccessReader, options?: ZipEditOptions): Promise; static openReader(reader: RandomAccessReader, options?: ZipEditOptions): Promise; /** * Open a remote ZIP archive for editing using HTTP Range requests. */ static openUrl(url: string, options?: ZipEditUrlOptions): Promise; /** * Close underlying resources (only relevant for reader-backed editors). */ close(): Promise; /** * Get a snapshot of the original parsed entries (ignores pending edits). * * Use this to inspect the archive before making changes. */ getEntries(): ZipEntryInfo[]; /** * Check if an entry exists (considering pending edits). * * @param name - Entry name to check * @returns `true` if the entry exists and is not deleted */ has(name: string): boolean; /** * Delete an entry from the archive. * * @param name - Entry name to delete * @returns `true` if the entry existed and was deleted, `false` otherwise */ delete(name: string): boolean; /** * Delete a directory and all its contents recursively. * * This method deletes the directory entry itself (if it exists) and all entries * whose paths start with the directory prefix. Similar to `rm -rf` behavior. * * @param prefix - The directory path prefix to delete (with or without trailing slash) * @returns The number of entries deleted * * @example * ```ts * // Delete "assets/" folder and all files inside it * const deletedCount = editor.deleteDirectory("assets"); * * // With trailing slash (same result) * editor.deleteDirectory("assets/"); * * // Delete nested directory * editor.deleteDirectory("src/components/old"); * ``` */ deleteDirectory(prefix: string): number; /** * Add or update an entry. * * If an entry with the same name already exists, it will be replaced. * * @param name - Entry name (path in the archive) * @param source - Entry data (Uint8Array, string, Blob, or async iterable) * @param options - Per-entry options (level, modTime, etc.) * @returns `this` for chaining * * @example * ```ts * editor * .set("readme.txt", "Hello World") * .set("data.bin", binaryData, { level: 0 }) // store without compression * .set("config.json", JSON.stringify(config)); * ``` */ set(name: string, source: ArchiveSource, options?: ZipEntryOptions): this; /** * Rename an entry. * * **Overwrite behavior**: If an entry with the target name already exists, * it will be replaced (similar to `mv -f`). * * @param from - Current entry name * @param to - New entry name * @returns `true` if the rename was successful, `false` if source doesn't exist * * @example * ```ts * editor.rename("old-name.txt", "new-name.txt"); * ``` */ rename(from: string, to: string): boolean; /** * Set or update the archive-level comment. * * @param comment - Comment string (or `undefined` to remove) * @returns `this` for chaining */ setComment(comment?: string): this; /** * Apply a reusable edit plan to this editor. */ apply(plan: ZipEditPlan): this; /** * Get a list of entry names that will appear in the output archive. * This accounts for deletes, renames, and additions. */ getOutputEntryNames(): string[]; private _getBuildOptions; private _buildPreservedRawFile; private _buildPreservedRawEntry; private _emitWarning; private _buildRawPreservedEntries; private _buildSetEntries; /** * Get the output as an async iterable of chunks. * * This is the most memory-efficient way to get the output for large archives. * * @param options - Streaming options (signal, onProgress) * @returns An async iterable of Uint8Array chunks * * @example * ```ts * const chunks: Uint8Array[] = []; * for await (const chunk of editor.stream()) { * chunks.push(chunk); * } * ``` */ stream(options?: ZipStreamOptions): AsyncIterable; /** * Get the output as an async iterable with full operation control. * * Returns an object with the iterable plus progress tracking and abort methods. * * @param options - Streaming options * @returns A `ZipOperation` object * * @example * ```ts * const op = editor.operation({ * onProgress: (p) => console.log(`${p.entriesDone}/${p.entriesTotal}`) * }); * * for await (const chunk of op.iterable) { * // process chunk * } * ``` */ operation(options?: ZipStreamOptions): ZipOperation; /** * Get the output as a single Uint8Array. * * This is the simplest output method. For large archives, consider using * `stream()` instead to avoid holding the entire output in memory. * * @param options - Streaming options (signal, onProgress) * @returns A Promise that resolves to the complete ZIP data * * @example * ```ts * const zipData = await editor.bytes(); * * // With progress tracking * const zipData = await editor.bytes({ * onProgress: (p) => console.log(`${p.entriesDone}/${p.entriesTotal}`) * }); * ``` */ bytes(options?: ZipStreamOptions): Promise; /** * Pipe the output to a sink (e.g., a writable stream). * * @param sink - The sink to write to * @param options - Streaming options * * @example * ```ts * // Node.js: pipe to file * import { createWriteStream } from "fs"; * await editor.pipeTo(createWriteStream("output.zip")); * ``` */ pipeTo(sink: ArchiveSink, options?: ZipStreamOptions): Promise; } /** * Open an existing ZIP archive for editing. * * This is a convenience function equivalent to `ZipEditor.open()`. * * @param source - The ZIP data * @param options - Edit options * @returns A new `ZipEditor` instance * * @example * ```ts * import { editZip, zip } from "../index.js"; * * // Create and edit * const original = await zip().add("a.txt", "hello").bytes(); * const editor = await editZip(original); * editor.set("b.txt", "world"); * const modified = await editor.bytes(); * * // Edit with options * const editor = await editZip(original, { * path: { mode: "safe" }, * reproducible: true, * onWarning: (w) => console.warn(w.message) * }); * ``` */ export declare function editZip(source: ArchiveSource | RandomAccessReader, options?: ZipEditOptions): Promise; /** * Open a remote ZIP archive for editing using HTTP Range requests. * * This is a convenience function equivalent to `ZipEditor.openUrl()`. */ export declare function editZipUrl(url: string, options?: ZipEditUrlOptions): Promise;