/** * A minimal ZIP archive writer for creating VSDX (Visio) files. * Implements the ZIP file format specification (RFC 1952) to package XML files and binary data. * * Features: * - Adds files (text or binary) to an in-memory ZIP archive * - Generates complete ZIP file structure with proper headers * - Supports uncompressed (stored) files only * - Compatible with standard ZIP readers and VSDX format * * ZIP Structure Created: * 1. Local file headers + file data (one per file) * 2. Central directory entries (one per file) * 3. End of Central Directory (EOCD) record * * Limitations: * - Only supports stored (uncompressed) files (compression method 0) * - Does not calculate CRC-32 checksums * - Single-disk archive only (no multi-disk support) * - No encryption or digital signatures * - Files are not actually compressed * * @example * const writer = new MinimalZipWriter(); * writer.addFile('document.xml', 'content'); * writer.addFile('styles.xml', xmlData); * const zipBuffer = writer.generate(); * const blob = new Blob([zipBuffer], { type: 'application/zip' }); * * @see {@link MinimalZipReader} for reading ZIP files */ export declare class MinimalZipWriter { /** * Array of files to be included in the ZIP archive. * Each entry contains filename and uncompressed binary data. * * @private * @type {Array<{name: string, data: Uint8Array}>} */ private files; /** * Byte offset where the central directory starts in the final ZIP file. * Calculated after all local headers and file data are written. * This value is stored in the EOCD record so readers can locate the central directory. * * @private * @type {number} */ private centralDirOffset; /** * The date/time to use for all files in the archive. * Stored in DOS date/time format in file headers. * All files share the same timestamp (file creation time). * * @private * @type {Date} */ private date; /** * Creates a new MinimalZipWriter instance. * Initializes an empty archive that is ready to accept files. * * @constructor * * @example * const writer = new MinimalZipWriter(); */ constructor(); /** * Adds a file to the ZIP archive. * Accepts either text (string) or binary (Uint8Array) data. * Text data is automatically converted to UTF-8 bytes. * * Algorithm: * 1. Convert string data to Uint8Array using TextEncoder (UTF-8) * 2. Create file entry object with name and data * 3. Store in files array for later processing during generate() * * Notes: * - Files are stored in order they are added * - Filenames can include path separators (/) for directory structure * - No duplicate checking - same filename can be added multiple times * - Data is copied into Uint8Array so original can be garbage collected * * @private * @param {string} filename - The path/name of the file in the archive * Can include path separators for nested files * Example: "folder/document.xml", "xl/sheets/sheet1.xml" * @param {Uint8Array | string} data - The file content as either: * - Uint8Array: Binary data (no conversion) * - string: Text data (converted to UTF-8 bytes) * @returns {void} * * @example * const writer = new MinimalZipWriter(); * writer.addFile('document.xml', 'Hello'); * writer.addFile('image.bin', imageBuffer); * writer.addFile('folder/nested.txt', 'nested content'); * * @throws Does not throw - silently handles any encoding issues */ addFile(filename: string, data: Uint8Array | string): void; /** * Generates the complete ZIP archive as an ArrayBuffer. * Constructs the entire ZIP file structure including all headers and data. * * Algorithm: * 1. For each file: * a. Create local file header (30 bytes fixed + filename) * b. Add file data * c. Create central directory entry * 2. Calculate offset where central directory starts * 3. Add all central directory entries * 4. Create and add End of Central Directory (EOCD) record * 5. Combine all byte arrays into single Uint8Array * * ZIP File Structure: * ``` * [Local File Header 1][File Data 1][Local File Header 2][File Data 2]... * [Central Directory Entry 1][Central Directory Entry 2]... * [End of Central Directory Record] * ``` * * Offsets: * - Local headers start at byte 0 * - Central directory starts after all local headers and file data * - EOCD record appears at the very end * * @private * @returns {ArrayBuffer} The complete ZIP archive as an ArrayBuffer * Can be converted to Blob for download: * new Blob([buffer], {type: 'application/zip'}) * * @example * const writer = new MinimalZipWriter(); * writer.addFile('file1.txt', 'content1'); * writer.addFile('file2.txt', 'content2'); * const buffer = writer.generate(); * * // Save to file (browser) * const blob = new Blob([buffer], {type: 'application/zip'}); * const url = URL.createObjectURL(blob); * const a = document.createElement('a'); * a.href = url; * a.download = 'archive.zip'; * a.click(); * * // Or send to server * fetch('/upload', {method: 'POST', body: blob}); */ generate(): ArrayBuffer; /** * Creates a local file header for a file in the ZIP archive. * Local file headers appear before each file's data and contain metadata. * * Local File Header Structure (30 bytes fixed + variable): * - Offset 0: Local file header signature (4 bytes) = 0x04034B50 * - Offset 4: Version needed to extract (2 bytes) * - Offset 6: General purpose bit flag (2 bytes) * - Offset 8: Compression method (2 bytes) [0=stored, 8=deflate] * - Offset 10: Last mod file time (2 bytes) [DOS format] * - Offset 12: Last mod file date (2 bytes) [DOS format] * - Offset 14: CRC-32 (4 bytes) * - Offset 18: Compressed size (4 bytes) * - Offset 22: Uncompressed size (4 bytes) * - Offset 26: Filename length (2 bytes) * - Offset 28: Extra field length (2 bytes) * - Offset 30: Filename (variable) * - After: Extra field (variable) * * This implementation: * - Uses stored method (no compression) * - Sets CRC-32 to 0 (not calculated) * - Sets both compressed and uncompressed sizes to data.length * - Uses DOS date/time format * * @private * @param {Uint8Array} filenameBytes - The filename encoded as UTF-8 bytes * @param {Uint8Array} data - The uncompressed file data * @returns {Uint8Array} Complete local file header including filename * Total length = 30 + filenameBytes.length * * @example * const filename = new TextEncoder().encode('document.xml'); * const fileData = new Uint8Array([...]); * const header = this.createLocalFileHeader(filename, fileData); * // header.length = 30 + filename.length * */ private createLocalFileHeader; /** * Creates a central directory entry for a file in the ZIP archive. * Central directory entries are used by ZIP readers to locate and list files. * One entry exists for each file in the archive. * * Central Directory Header Structure (46 bytes fixed + variable): * - Offset 0: Central directory header signature (4 bytes) = 0x02014B50 * - Offset 4: Version made by (2 bytes) * - Offset 6: Version needed to extract (2 bytes) * - Offset 8: General purpose bit flag (2 bytes) * - Offset 10: Compression method (2 bytes) * - Offset 12: Last mod file time (2 bytes) [DOS format] * - Offset 14: Last mod file date (2 bytes) [DOS format] * - Offset 16: CRC-32 (4 bytes) * - Offset 20: Compressed size (4 bytes) * - Offset 24: Uncompressed size (4 bytes) * - Offset 28: Filename length (2 bytes) * - Offset 30: Extra field length (2 bytes) * - Offset 32: File comment length (2 bytes) * - Offset 34: Disk number start (2 bytes) * - Offset 36: Internal file attributes (2 bytes) * - Offset 38: External file attributes (4 bytes) * - Offset 42: Relative offset of local header (4 bytes) * - Offset 46: Filename (variable) * - After: Extra field (variable) * - After: File comment (variable) * * The central directory allows ZIP readers to: * - List all files without scanning entire archive * - Jump directly to any file using local header offset * - Verify file integrity * * @private * @param {Uint8Array} filenameBytes - The filename encoded as UTF-8 bytes * @param {Uint8Array} data - The file data (used to calculate sizes) * @param {number} offset - Byte offset where this file's local header is located * This allows readers to jump directly to the file * @returns {Uint8Array} Complete central directory entry including filename * Total length = 46 + filenameBytes.length * * @example * const filename = new TextEncoder().encode('document.xml'); * const fileData = new Uint8Array([...]); * const entry = this.createCentralDirEntry(filename, fileData, 1024); * // Creates entry pointing to local header at byte 1024 * */ private createCentralDirEntry; /** * Creates the End of Central Directory (EOCD) record for the ZIP archive. * This is the last record in a ZIP file and must be found by ZIP readers. * EOCD tells readers where the central directory is located. * * End of Central Directory Structure (22 bytes fixed + comment): * - Offset 0: EOCD signature (4 bytes) = 0x06054B50 * - Offset 4: Number of this disk (2 bytes) * - Offset 6: Disk where central directory starts (2 bytes) * - Offset 8: Number of central dir records on this disk (2 bytes) * - Offset 10: Total number of central dir records (2 bytes) * - Offset 12: Size of central directory (4 bytes) * - Offset 16: Offset of start of central directory (4 bytes) * - Offset 20: Comment length (2 bytes) * - Offset 22: Comment (variable, 0 bytes in this implementation) * * The EOCD record is critical: * - ZIP readers search backwards from end of file to find this * - Must be within last 65557 bytes (65535 + 22) due to max comment length * - Provides all info needed to locate and read the archive * * @private * @param {number} entries - The total number of files in the archive * Must match count of central directory entries * @param {number} cdOffset - Byte offset where central directory starts * Allows ZIP readers to locate the directory * @param {number} cdSize - Total size of central directory in bytes * Sum of all central directory entry sizes * @returns {Uint8Array} The EOCD record (exactly 22 bytes, no comment) * * @example * const eocd = this.createEndOfCentralDirRecord(5, 10240, 250); * // Creates EOCD for archive with 5 files * // Central directory starts at byte 10240 and is 250 bytes long * */ private createEndOfCentralDirRecord; /** * Converts a JavaScript Date object to DOS time format. * DOS time format encodes hours, minutes, and seconds in a 16-bit value. * * DOS Time Format: * - Bits 15-11: Hours (0-23) = 5 bits * - Bits 10-5: Minutes (0-59) = 6 bits * - Bits 4-0: Seconds/2 (0-29, represents 0-58 seconds) = 5 bits * Total: 16 bits * * Encoding Logic: * - Hours shifted left 11 positions (bits 15-11) * - Minutes shifted left 5 positions (bits 10-5) * - Seconds divided by 2 (DOS only has 2-second precision) * * Example: * - Time 14:30:45 (2:30:45 PM) * - Hours: 14 << 11 = 0xB800 * - Minutes: 30 << 5 = 0x03C0 * - Seconds: 45 >> 1 = 22 = 0x0016 * - Result: 0xBBD6 * * @private * @param {Date} date - The JavaScript Date object to convert * @returns {number} 16-bit DOS time value (little-endian format) * * @example * const date = new Date(2024, 0, 15, 14, 30, 45); * const dosTime = this.dosTime(date); * // Returns DOS time encoding 14:30:45 * */ private dosTime; /** * Converts a JavaScript Date object to DOS date format. * DOS date format encodes year, month, and day in a 16-bit value. * * DOS Date Format: * - Bits 15-9: Year (0-127, relative to 1980) = 7 bits * - Bits 8-5: Month (1-12) = 4 bits * - Bits 4-0: Day (1-31) = 5 bits * Total: 16 bits * * Encoding Logic: * - Year: subtract 1980 from actual year, then shift left 9 * - Month: no adjustment needed (1-12), shift left 5 * - Day: no adjustment needed (1-31) * * Year Range: * - Supports years 1980 to 2107 (1980 + 127) * - Values 0-127 represent 1980-2107 * - Cannot represent dates before 1980 * * Example: * - Date: January 15, 2024 * - Year: 2024 - 1980 = 44, 44 << 9 = 0x5600 * - Month: 1 << 5 = 0x0020 * - Day: 15 = 0x000F * - Result: 0x562F * * @private * @param {Date} date - The JavaScript Date object to convert * @returns {number} 16-bit DOS date value (little-endian format) * * @example * const date = new Date(2024, 0, 15); // January 15, 2024 * const dosDate = this.dosDate(date); * // Returns DOS date encoding 2024-01-15 * */ private dosDate; }