Node.js 26.8 يضيف دعم ZIP أصلي في node:zlib
في 26 أغسطس 2026 صدر Node.js 26.8.0 على خط Current، ومعه إضافة ظلّت مطلباً مفتوحاً في مستودع المشروع سنوات: قراءة أرشيفات ZIP وكتابتها من داخل node:zlib مباشرة، بلا أي حزمة خارجية.
المشكلة: ZIP ليست خوارزمية ضغط
وحدة node:zlib تعرف Gzip و Deflate و Brotli و Zstd منذ زمن، وكلها خوارزميات تشتغل على تيار بايتات واحد: بايتات تدخل، بايتات أصغر تخرج. أما ZIP فهي صيغة حاوية، وهذا فرق جوهري. الأرشيف يحمل فهرساً مركزياً (central directory) يقع في نهاية الملف، ولكل مُدخل ترويسة محلية وبيانات مضغوطة وقيمة crc32 وبيانات وصفية عن الأذونات ووقت التعديل.
هذا التصميم — الفهرس في النهاية لا في البداية — هو ما يجعل ZIP قابلاً للقراءة بكفاءة: يكفي قراءة ذيل الملف لمعرفة كل ما بداخله ومواقعه، دون لمس المحتوى. لكنه أيضاً ما يجعل التعامل معه منطق تحليل للصيغة نفسها، لا مجرد فك ضغط. ولهذا لم تكن zlib وحدها كافية يوماً.
النتيجة العملية أن أي مشروع يفتح ملف ZIP — استيراد بيانات من عميل، فك حزمة plugin، معالجة تصدير من نظام خارجي، قراءة artifact من pipeline — كان يضيف dependency مثل adm-zip أو yauzl أو jszip. والإضافة ليست مجانية: كل حزمة جديدة سطر إضافي في شجرة الاعتماديات، ومسؤولية تحديث إضافية، وسطح أوسع أمام هجمات سلسلة التوريد على npm. حزمة صغيرة تُستدعى مرة واحدة في السنة تبقى مع ذلك حاضرة في كل عملية install.
ما الذي تغيّر
يضيف 26.8.0 إلى node:zlib ثلاث فئات وواجهتين مساعدتين:
- ZipFile — وصول عشوائي إلى أرشيف على القرص. عند الفتح يقرأ ذيل الملف والـ central directory فقط، ويُحمّل محتوى كل مُدخل عند الطلب. يوفّر
open()وget()وstream()وadd()وdelete()وcompact()، ولكلٍّ نسخة متزامنة. هذا هو الخيار الصحيح لأرشيف كبير تحتاج منه ملفاً أو ملفين. - ZipBuffer — عرض zero-copy فوق أرشيف موجود أصلاً في الذاكرة كـ Buffer. لا ينسخ شيئاً، ويقرأ المُدخلات مباشرة من نفس المنطقة. مناسب لأرشيف وصل عبر الشبكة في طلب واحد ولا داعي لكتابته على القرص أولاً.
- ZipEntry — يمثّل مُدخلاً واحداً، مع
content()للقراءة المُخزّنة وcontentIterator()للقراءة المتدفقة بذاكرة محدودة، وcreate()وcreateStream()وcreateSymlink()للبناء. ويكشف خصائص مثلnameوsizeوcompressedSizeوcrc32وisFileوisDirectoryوmode.
ومعها createZipArchive() التي تحوّل تسلسلاً من ZipEntry إلى stream قابل للقراءة — وتنتقل إلى بنى Zip64 تلقائياً عند تجاوز حدود الصيغة الكلاسيكية — و setMaxZipContentSize() التي تضبط سقف الذاكرة الافتراضي لـ content().
مثال عملي
import { ZipFile, ZipEntry, createZipArchive } from 'node:zlib';
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
// القراءة: يُحلَّل الذيل والـ central directory فقط عند الفتح
const zip = await ZipFile.open('release.zip');
try {
for (const entry of await zip.entries()) {
if (!entry.isFile) continue;
console.log(entry.name, entry.size, '->', entry.compressedSize);
}
// مُدخل صغير: اقرأه في الذاكرة
const manifest = await zip.get('manifest.json');
const config = JSON.parse((await manifest.content()).toString('utf8'));
// مُدخل كبير: مرّره كـ stream بذاكرة محدودة
await pipeline(zip.stream('bundle.js'), createWriteStream('bundle.js'));
} finally {
await zip.close();
}
// الكتابة: مُدخلات تدخل، أرشيف يخرج كـ stream
await pipeline(
createZipArchive([
await ZipEntry.create('manifest.json', JSON.stringify({ ok: true })),
ZipEntry.createStream('bundle.js', createReadStream('dist/bundle.js')),
]),
createWriteStream('out.zip'),
);الفكرة التي تستحق الانتباه هنا أن الواجهة مبنية حول التدفّق لا حول التحميل الكامل. createZipArchive() تستهلك المُدخلات أثناء الإنتاج، و ZipEntry.createStream() تسمح ببناء أرشيف من مصدر لا يسع الذاكرة أصلاً.
ملاحظات من الواقع
- الواجهات كلها مصنّفة Stability 1.0 — Early development. التوقيعات قابلة للتغيير بين الإصدارات، فلا يُبنى عليها عقد عام مستقر بعد.
setMaxZipContentSize()سقفه الافتراضي 256 MiB (268435456 بايت)، وهو حاجز أمام zip bombs: أي مُدخل يتجاوزه يُرفض قبل الحجز في الذاكرة. لكن الحاجز يخصcontent()وحدها؛ القراءة عبرcontentIterator()لا تتأثر به. هذا سبب إضافي لتفضيل التدفّق مع أي مُدخل غير موثوق، لا لأنه أسرع فقط.- التعديل في المكان عبر ZipFile المفتوح للكتابة ليس crash-atomic. انقطاع في منتصف الكتابة قد يترك الأرشيف غير قابل للقراءة، فالنمط الآمن يبقى: اكتب إلى ملف جديد ثم استبدل استبدالاً ذرّياً.
- ZipBuffer لا ينسخ الذاكرة. تعديل الـ Buffer الأصلي أو إعادة استخدامه أثناء الاستعمال يفسد القراءة بصمت، وهذا نوع الأخطاء التي لا تظهر في الاختبارات.
- أسماء المُدخلات تأتي من ملف لا تتحكم فيه، وقد تحمل
../أو مساراً مطلقاً. التحقق من path traversal قبل الكتابة على القرص مسؤولية التطبيق لا مسؤولية الوحدة — وهذه ثغرة كلاسيكية أصابت مكتبات أرشفة كثيرة قبل اليوم. - الميزة نزلت في خط Current لا LTS، ومن كان على 24.x فالانتظار وارد. ونقطة عملية: 26.8.1 صدر بعد 26.8.0 مباشرة لإصلاح سلسلة إصدار خاطئة كانت تُظهر النسخة كـ alpha — ثبّت على 26.8.1.
الخلاصة
ZIP في node:zlib لا يفتح باباً جديداً بقدر ما يغلق واحداً: عملية شائعة كان ثمنها dependency دائمة، صارت جزءاً من المنصّة. والاتجاه واضح في Node.js منذ فترة — test runner ثم SQLite والآن ZIP — وكل خطوة فيه تقلّص شجرة الاعتماديات، وتقلّص معها المساحة التي تُبنى عليها هجمات سلسلة التوريد. القرار العملي بسيط: جرّبها على فرع منفصل، وقِس، وأجّل الاعتماد في الإنتاج حتى تستقر الواجهة.
المصدر الأساسي: ملاحظات إصدار Node.js 26.8.0
وللسياق الأمني حول شجرة الاعتماديات: دودة npm تزرع hooks داخل الـ AI coding agent