/** * Encodes and decodes zip archives as streams in Effect channels. * * `encode` writes each entry as it arrives and adds the central directory once * the input ends; `decode` reads entries in archive order from their local * headers. `decode` never buffers an entire entry body, and `encode` only * collects deflated bodies with a `size` of at most 64 KiB. * * Entries are stored or compressed with deflate, the methods every zip tool * reads. Compression uses the runtime's `CompressionStream("deflate-raw")`; * decompression is implemented here, so that `decode` can find the end of * entries whose sizes follow their data. ZIP64 fields are written when sizes, * offsets or the entry count need them, and read from local headers and data * descriptors. * Encryption, other compression methods and archives split across files are * not supported. * * This module is experimental and may receive breaking changes in patch * releases. * * @stability experimental * @since 4.0.6 */ import * as Arr from "../Array.ts"; import * as Cause from "../Cause.ts"; import * as Channel from "../Channel.ts"; import * as Stream from "../Stream.ts"; declare const ZipErrorTypeId = "~effect/encoding/Zip/ZipError"; declare const ZipError_base: new = {}>(args: import("../Types.ts").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & { readonly _tag: "ZipError"; } & Readonly; /** * Error raised when a zip archive cannot be encoded or decoded. * * **Details** * * The `kind` field identifies the failure: * * - `InvalidHeader`: a header has an unknown signature or a malformed field, or * an entry has a value that cannot be written. * - `InvalidData`: the compressed data of an entry is corrupt. * - `Truncated`: the input ended inside an entry or before the central * directory. * - `Checksum`: the data of an entry does not match its CRC-32. * - `SizeMismatch`: an entry body produced a byte count other than its declared * `size`, or the data of an entry does not match the sizes in the archive. * - `Unsupported`: the archive uses a feature this module does not read, such * as encryption or a compression method other than store and deflate. * - `BodyConsumed`: an entry body was read after the decoder had moved past it. * * @stability experimental * @category errors * @since 4.0.6 */ export declare class ZipError extends ZipError_base<{ readonly kind: "InvalidHeader" | "InvalidData" | "Truncated" | "Checksum" | "SizeMismatch" | "Unsupported" | "BodyConsumed"; readonly message: string; }> { /** * Marks this value as a zip error for runtime guards. * * @stability experimental * @since 4.0.6 */ readonly [ZipErrorTypeId] = "~effect/encoding/Zip/ZipError"; } /** * Checks whether a value is a `ZipError`. * * @stability experimental * @category guards * @since 4.0.6 */ export declare const isZipError: (u: unknown) => u is ZipError; /** * Kind of object described by a zip entry. * * @stability experimental * @category models * @since 4.0.6 */ export type EntryType = "File" | "Directory"; /** * Compression method of a zip entry. * * @stability experimental * @category models * @since 4.0.6 */ export type Method = "Store" | "Deflate"; /** * Metadata of a zip entry. * * **Details** * * `path` is the archive path, with `/` separators; directory paths end with * `/`. `size` is the uncompressed byte length when it is known before the body: * decoded entries whose sizes follow their data have no `size`. `mode` holds * Unix permission bits and is written to the central directory, which `decode` * does not read, so decoded headers have no `mode`. `mtime` has a resolution of * one second when the archive has an extended timestamp and two seconds * otherwise. * * @stability experimental * @category models * @since 4.0.6 */ export interface Header { readonly path: string; readonly type: EntryType; readonly method: Method; readonly mtime: Date; readonly size?: number | undefined; readonly mode?: number | undefined; } /** * Zip entry: a header and a stream of the uncompressed body bytes. * * **Details** * * `decode` returns these, and `fileStream` builds them. `encode` writes the * sizes and CRC-32 of a streamed body in a data descriptor after the data. * * @see {@link ContentEntry} for entries whose content is held in memory * * @stability experimental * @category models * @since 4.0.6 */ export interface Entry { readonly header: Header; readonly body: Stream.Stream; readonly content?: undefined; } /** * Zip entry whose uncompressed content is held in memory. * * **Details** * * `file` and `directory` build these. Because the content is known before the * entry is written, `encode` puts its sizes and CRC-32 in the local header and * writes no data descriptor, which every zip reader accepts. `content` and * `body` exclude each other, so an entry whose body is replaced must drop * `content`. * * @see {@link Entry} for entries whose content is streamed * * @stability experimental * @category models * @since 4.0.6 */ export interface ContentEntry { readonly header: Header; readonly content: Uint8Array; readonly body?: undefined; } /** * Options for the entry constructors. * * **Details** * * Unset fields default to an mtime of 1980-01-01T00:00:00Z, the earliest time * a zip header can hold, and mode `0o644` (`0o755` for directories). The * defaults make archives reproducible. * * @stability experimental * @category models * @since 4.0.6 */ export interface EntryOptions { readonly mtime?: Date | undefined; readonly mode?: number | undefined; } /** * Options for file entries. * * **Details** * * `method` defaults to `"Deflate"`. Use `"Store"` for content that is already * compressed, such as images or archives. * * @stability experimental * @category models * @since 4.0.6 */ export interface FileOptions extends EntryOptions { readonly method?: Method | undefined; } /** * Options for file entries whose content is streamed. * * **Details** * * `size` is the exact byte length of the body. It is required for `Store`, * because streaming readers can only find the end of stored data from its * size, and optional for `Deflate`, where it is checked against the body. * * @stability experimental * @category models * @since 4.0.6 */ export type FileStreamOptions = EntryOptions & ({ readonly method?: "Deflate" | undefined; readonly size?: number | undefined; } | { readonly method: "Store"; readonly size: number; }); /** * Creates a file entry from in-memory content. * * **Details** * * String content is encoded as UTF-8, and `size` is the byte length of the * content. Because the content is known up front, its sizes and CRC-32 are * written in the local header, which every zip reader accepts. * * @see {@link fileStream} for content that is streamed instead of held in memory * @see {@link encode} for writing entries to an archive * * @stability experimental * @category constructors * @since 4.0.6 */ export declare const file: (path: string, content: string | Uint8Array, options?: FileOptions) => ContentEntry; /** * Creates a file entry whose content is streamed from `body`. * * **When to use** * * Use when the content is too large to hold in memory, or is read from a file * or another stream. * * **Details** * * `body` is not run until the entry is encoded. Its sizes and CRC-32 are * written in a data descriptor after the data. * * **Gotchas** * * - A stored entry without `size` fails with an `InvalidHeader` `ZipError`. * - When `size` is set, `encode` fails with a `SizeMismatch` `ZipError` if the * body produces fewer or more bytes. * - Non-empty stored entries are followed by a data descriptor, which some * streaming readers mishandle. Java's `ZipInputStream` rejects them, and * libarchive reading from a pipe (as `bsdtar` does) ends them at the first * bytes that look like a descriptor, so content can cut the entry short and * add entries. Use `Deflate` or an in-memory `file` for untrusted content * and for those readers. * * @see {@link file} for in-memory content * @see {@link encode} for writing entries to an archive * * @stability experimental * @category constructors * @since 4.0.6 */ export declare const fileStream: (path: string, body: Stream.Stream, options?: FileStreamOptions) => Entry; /** * Creates a directory entry. * * **Details** * * A trailing `/` is added to a non-empty `path` when missing, as zip tools * expect for directories. * * @stability experimental * @category constructors * @since 4.0.6 */ export declare const directory: (path: string, options?: EntryOptions) => ContentEntry; /** * Creates a channel that decodes zip archive bytes into entries. * * **Details** * * Entries are read in archive order from their local headers, so archives can * be decoded as they arrive, for example from an HTTP response. Each output * chunk holds one entry, and each body streams the uncompressed bytes of the * entry. Pulling the next entry skips whatever is left of the current body. * Stored and deflated entries are read, including ZIP64 sizes and deflated * entries whose sizes follow their data in a data descriptor. Every body is * checked against its CRC-32 and sizes. Reading stops at the central directory. * * Paths use the UTF-8 flag when set, and otherwise the first Info-ZIP Unicode * path field when it matches the name, as Info-ZIP and 7-Zip do; other paths * are read as UTF-8 when valid and as CP437 otherwise. Times are read from the * extended timestamp field when present, and otherwise from the DOS time and * date, which have no time zone and are read as UTC. * * **Gotchas** * * - Read each body before pulling the next entry: a body read afterwards fails * with a `BodyConsumed` `ZipError`. Operators that pull ahead, such as * `Stream.buffer`, therefore can't sit between `decode` and the body reads. * - Read bodies while the entry stream runs, for example in `Stream.mapEffect`: * once it has ended, as after `Stream.runCollect`, `Stream.runHead` or * `Stream.take`, every body fails with a `BodyConsumed` `ZipError`. * - A body reads the entry's bytes once: running it again continues where the * previous run stopped. * - The central directory is not read, so `mode`, comments and entries that * only the central directory lists are not reported. Tools that read the * central directory can see other entries and paths, for example in * archives updated by appending. * - Archives that do not start with an entry or a central directory, such as * self-extracting archives, fail with an `InvalidHeader` `ZipError`. * - Stored entries whose sizes only follow their data fail with an * `Unsupported` `ZipError`. Python's `zipfile` writes them for stored * entries on unseekable output, and Info-ZIP's `zip` for empty files when * writing to a pipe. Searching for their end would let the content of an * entry forge the entries after it. * - A body that grows past its `size` fails with a `SizeMismatch` `ZipError` * before the extra bytes are emitted, so `size` can be checked before reading. * - Deflated data can expand about a thousand times, and pulling the next * entry inflates the rest of the current body to find its end. To bound the * work on untrusted archives, check `size`, count the bytes of bodies * without one, and end the stream at an entry that is too large rather than * skip it. * - Every `ZipError` ends the archive: later entry and body reads fail with * the same error. * - The channel completes only when its input ends: input after the central * directory is read and discarded, so that the upstream done value can be * passed on. * - Paths are returned as written in the archive: they can be absolute, * contain `..`, drive letters, control characters or backslashes, which some * tools read as separators, and can repeat. Validate them before writing * entries to disk; `encode` rejects absolute paths, drive letters and * backslashes. Paths with a NUL character, which other tools take as the end * of the name, fail with an `InvalidHeader` `ZipError`. * * @see {@link encode} for the inverse operation * * @stability experimental * @category decoding * @since 4.0.6 */ export declare const decode: () => Channel.Channel>, ZipError | IE, Done, Arr.NonEmptyReadonlyArray, IE, Done>; /** * Creates a channel that encodes entries into zip archive bytes. * * **Details** * * Each entry is written as soon as it arrives: a local header, the stored or * deflated body and, for entries with a streamed `body`, a data descriptor * with the CRC-32 and sizes. A `ContentEntry` has them in its local header. * The central directory is written once the input ends. Paths are written as * UTF-8, with the UTF-8 flag set when they are not ASCII. `mtime` * is written both as a DOS time in UTC, which keeps two-second resolution and * covers 1980 to 2107, and as an extended timestamp in whole seconds when it * falls between 1970 and 2038. ZIP64 fields are added when a size, an offset * or the entry count does not fit the classic fields, and to the local header * of a deflated entry streamed without a `size` or with one near 4 GiB, so * that readers know the width of the sizes after its data. * * Headers and body chunks below 16 KiB are gathered into output chunks of * about 64 KiB, and larger body chunks pass through without copying. Output is * handed out before waiting for more entries, but up to 64 KiB of it can be * held while a body is pulled. Each body chunk of a streamed `Deflate` entry is * a separate compressor job, so larger chunks compress faster. A `Deflate` * body with a `size` of at most 64 KiB is instead collected and compressed in * one job, and still gets a data descriptor; omit `size` to stream it. * * **Gotchas** * * - Deflated entries need `CompressionStream("deflate-raw")`. On a runtime * without it, encoding a deflated entry dies with the runtime's error, as * it does when the compressor fails: check for the API or use `Store`. * - Deflated bytes come from the runtime's `CompressionStream`, so they can * differ between runtimes. Archives of stored entries are byte-identical * everywhere. * - A body that produces a byte count other than a set `header.size`, or any * bytes for a directory, fails with a `SizeMismatch` `ZipError`. * - Directories are always stored, whatever their `method`. * - Empty paths, paths starting with `/` or a drive letter such as `C:`, paths * containing `\` or NUL, `File` paths ending with `/`, `Directory` paths not * ending with `/`, and invalid modes, sizes or times fail with an * `InvalidHeader` `ZipError`. Other paths are written as given, including * `..` segments, so validate untrusted paths before encoding them. * * **Example** (Writing and reading an archive) * * ```ts import.meta.vitest * import { Effect, Stream } from "effect" * import { Zip } from "effect/encoding" * * const archive = Stream.make( * Zip.directory("docs"), * Zip.file("docs/readme.txt", "Hello, zip!") * ).pipe(Stream.pipeThroughChannel(Zip.encode())) * * const listing = archive.pipe( * Stream.pipeThroughChannel(Zip.decode()), * Stream.mapEffect((entry) => * Stream.mkString(Stream.decodeText(entry.body)).pipe( * Effect.map((text) => `${entry.header.path} ${JSON.stringify(text)}`) * ) * ), * Stream.runCollect * ) * await Effect.runPromise(listing) // => ['docs/ ""', 'docs/readme.txt "Hello, zip!"'] * ``` * * @see {@link decode} for the inverse operation * @see {@link file} for building entries from in-memory content * @see {@link fileStream} for building entries from streamed content * * @stability experimental * @category encoding * @since 4.0.6 */ export declare const encode: | ContentEntry = Entry | ContentEntry>() => Channel.Channel, ZipError | IE | (In extends Entry ? E : never), Done, Arr.NonEmptyReadonlyArray, IE, Done, In extends Entry ? R : never>; export {}; //# sourceMappingURL=Zip.d.ts.map