/** * @fileoverview Pure Binary ZIP Archive Builder & Parser Engine (PKZIP Standard) * @module utils/zip-archive * @description محرك ثنائي لإنشاء وقراءة وفك ضغط ملفات ZIP محلياً في المتصفح مع التحقق من سلامة البيانات عبر CRC-32. */ (global => { 'use strict'; /** * جدول جداول فحص التطابق الدوري (CRC-32 Lookup Table) */ const CRC32_TABLE = (() => { const table = new Uint32Array(256); for (let i = 0; i < 256; i++) { let c = i; for (let j = 0; j < 8; j++) { c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1); } table[i] = c; } return table; })(); /** * حساب قيمة التحقق CRC-32 للمصفوفة الثنائية * @param {Uint8Array} bytes * @returns {number} */ function computeCRC32(bytes) { let crc = 0xFFFFFFFF; for (let i = 0; i < bytes.length; i++) { crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ bytes[i]) & 0xFF]; } return (crc ^ 0xFFFFFFFF) >>> 0; } /** * فئة بناء ومعالجة المصفوفات الثنائية (Byte Builder) */ class ByteBuilder { constructor() { this.buffers = []; this.length = 0; } append(data) { if (typeof data === 'string') { data = new TextEncoder().encode(data); } if (data instanceof Uint8Array) { this.buffers.push(data); this.length += data.length; } else if (data instanceof ArrayBuffer) { const arr = new Uint8Array(data); this.buffers.push(arr); this.length += arr.length; } } appendUint8(val) { const arr = new Uint8Array(1); arr[0] = val & 0xFF; this.append(arr); } appendUint16(val) { const arr = new Uint8Array(2); arr[0] = val & 0xFF; arr[1] = (val >>> 8) & 0xFF; this.append(arr); } appendUint32(val) { const arr = new Uint8Array(4); arr[0] = val & 0xFF; arr[1] = (val >>> 8) & 0xFF; arr[2] = (val >>> 16) & 0xFF; arr[3] = (val >>> 24) & 0xFF; this.append(arr); } toUint8Array() { const out = new Uint8Array(this.length); let offset = 0; for (const buf of this.buffers) { out.set(buf, offset); offset += buf.length; } return out; } } /** * إنشاء ملف ZIP قياسي يحتوي على ملفات ثنائية * @param {Array<{ name: string, data: Uint8Array }>} files - قائمة الملفات المراد ضغطها * @returns {Uint8Array} المصفوفة الثنائية الكاملة لأرشيف الـ ZIP */ function createZipArchive(files) { const builder = new ByteBuilder(); const cdEntries = []; for (const file of files) { const nameBytes = new TextEncoder().encode(file.name); const dataBytes = file.data; const crc = computeCRC32(dataBytes); const offset = builder.length; // Local File Header Signature (0x04034b50) builder.appendUint32(0x04034b50); builder.appendUint16(10); // Version needed to extract (1.0) builder.appendUint16(0x0800); // General purpose bit flag (UTF-8) builder.appendUint16(0); // Compression method (0 = Stored / Uncompressed) builder.appendUint16(0); // File last mod time builder.appendUint16(0); // File last mod date builder.appendUint32(crc); // CRC-32 builder.appendUint32(dataBytes.length); // Compressed size builder.appendUint32(dataBytes.length); // Uncompressed size builder.appendUint16(nameBytes.length); // File name length builder.appendUint16(0); // Extra field length builder.append(nameBytes); builder.append(dataBytes); cdEntries.push({ nameBytes, crc, size: dataBytes.length, offset }); } const cdOffset = builder.length; const cdBuilder = new ByteBuilder(); // Central Directory Structure for (const entry of cdEntries) { // Central Directory Header Signature (0x02014b50) cdBuilder.appendUint32(0x02014b50); cdBuilder.appendUint16(20); // Version made by cdBuilder.appendUint16(10); // Version needed to extract cdBuilder.appendUint16(0x0800); // General purpose bit flag (UTF-8) cdBuilder.appendUint16(0); // Compression method cdBuilder.appendUint16(0); // File last mod time cdBuilder.appendUint16(0); // File last mod date cdBuilder.appendUint32(entry.crc); // CRC-32 cdBuilder.appendUint32(entry.size); // Compressed size cdBuilder.appendUint32(entry.size); // Uncompressed size cdBuilder.appendUint16(entry.nameBytes.length); // File name length cdBuilder.appendUint16(0); // Extra field length cdBuilder.appendUint16(0); // File comment length cdBuilder.appendUint16(0); // Disk number start cdBuilder.appendUint16(0); // Internal file attributes cdBuilder.appendUint32(0); // External file attributes cdBuilder.appendUint32(entry.offset); // Relative offset of local header cdBuilder.append(entry.nameBytes); } const cdData = cdBuilder.toUint8Array(); builder.append(cdData); // End of Central Directory Record (EOCD) Signature (0x06054b50) builder.appendUint32(0x06054b50); builder.appendUint16(0); // Number of this disk builder.appendUint16(0); // Disk where central directory starts builder.appendUint16(cdEntries.length); // Total entries on this disk builder.appendUint16(cdEntries.length); // Total entries builder.appendUint32(cdData.length); // Size of central directory builder.appendUint32(cdOffset); // Offset of start of central directory builder.appendUint16(0); // Comment length return builder.toUint8Array(); } /** * قراءة واستخراج محتويات ملف ZIP ثنائي * @param {ArrayBuffer} arrayBuffer - ملف الـ ZIP المضغوط * @returns {Array<{ name: string, data: Uint8Array }>} */ function readZipArchive(arrayBuffer) { const bytes = new Uint8Array(arrayBuffer); const view = new DataView(arrayBuffer); const files = []; let eocdOffset = -1; // البحث عن ترويسة نهاية الفهرس المركزي (EOCD Signature: 0x50, 0x4b, 0x05, 0x06) for (let i = bytes.length - 22; i >= 0; i--) { if (bytes[i] === 0x50 && bytes[i+1] === 0x4b && bytes[i+2] === 0x05 && bytes[i+3] === 0x06) { eocdOffset = i; break; } } if (eocdOffset === -1) { throw new Error("خطأ في بنية الملف: ترويسة الفهرس المركزي للأرشيف معطوبة أو ليس ملف .zip مشفر صالح."); } const cdCount = view.getUint16(eocdOffset + 10, true); const cdStartOffset = view.getUint32(eocdOffset + 16, true); let currentCdPtr = cdStartOffset; for (let k = 0; k < cdCount; k++) { if (bytes[currentCdPtr] !== 0x50 || bytes[currentCdPtr+1] !== 0x4b || bytes[currentCdPtr+2] !== 0x01 || bytes[currentCdPtr+3] !== 0x02) { throw new Error("تطابق الفهرس المركزي تعثر عند النقطة: " + currentCdPtr); } const nameLen = view.getUint16(currentCdPtr + 28, true); const extraLen = view.getUint16(currentCdPtr + 30, true); const commentLen = view.getUint16(currentCdPtr + 32, true); const localHeaderOffset = view.getUint32(currentCdPtr + 42, true); const nameBytes = bytes.subarray(currentCdPtr + 46, currentCdPtr + 46 + nameLen); const fileName = new TextDecoder().decode(nameBytes); if (bytes[localHeaderOffset] !== 0x50 || bytes[localHeaderOffset+1] !== 0x4b || bytes[localHeaderOffset+2] !== 0x03 || bytes[localHeaderOffset+3] !== 0x04) { throw new Error("فشل الربط بالترويسة المحلية للملف عند الإزاحة: " + localHeaderOffset); } const fileDataLen = view.getUint32(localHeaderOffset + 18, true); const fileLocalNameLen = view.getUint16(localHeaderOffset + 26, true); const fileLocalExtraLen = view.getUint16(localHeaderOffset + 28, true); const fileDataStart = localHeaderOffset + 30 + fileLocalNameLen + fileLocalExtraLen; const fileDataBytes = bytes.subarray(fileDataStart, fileDataStart + fileDataLen); files.push({ name: fileName, data: fileDataBytes }); currentCdPtr += 46 + nameLen + extraLen + commentLen; } return files; } /** * تصدير محرك ZIP */ global.FritreeZipArchive = Object.freeze({ createZip: createZipArchive, readZip: readZipArchive, computeCRC32, ByteBuilder }); })(typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : this);