gzip مقطوع: ملف الدليل الذي نُزّل بنجاح وكان مع ذلك معطوبًا

عضو gzip يُقطَع في منتصف التنزيل ينفك إلى مستند قصير، صالح، ومعقول. لا شيء يطلق استثناءً، وإذا كان خط المعالجة لديك يبوّب على تجزئة، فلن يطلق شيء أبدًا بعد ذلك.

var truncated = Gzip(Payload)[..^8];
await using var input = new MemoryStream(truncated);
await using var inflated = new MemoryStream();
await using (var gz = new GZipStream(input, CompressionMode.Decompress, leaveOpen: true))
{
    await gz.CopyToAsync(inflated);
}

Assert.Equal(Encoding.UTF8.GetByteCount(Payload), inflated.Length);

ثمانية بايتات تُحذَف من نهاية ملف gzip، فيسلّم فاكّ الضغط الحِمل كاملًا ولا يطلق شيئًا. ذلك التأكيد موجود في مجموعة اختباراتنا لأن المقدّمة كان لا بد من تثبيتها قبل بناء أي شيء عليها. ما كان لأي نهج قائم على الاستثناءات أن يلتقط هذا، ولهذا يقرأ الحارس المقطع الختامي للملف نفسه بدلًا من انتظار أن يُخبَر.

البايتات الثمانية التي ضاعت هي المقطع الختامي، الجزء الوحيد من الملف الذي كان يمكن أن يخالف ما خرج.

أعطب تلك البايتات الثمانية بدلًا من إزالتها فيطلق فاكّ الضغط نفسه استثناءً فورًا. قلبت بتًّا واحدًا في كل موضع من مواضع المقطع الختامي الثمانية، فأطلق كل منها InvalidDataException دون تسليم أي شيء. القارئ يفحص المقطع الختامي بدقة متناهية عندما يستطيع رؤيته. القطع خطير لأنه يأخذ المقطع الختامي بعيدًا بدلًا من إتلافه، والفحص الذي لا يُبلَغ أبدًا يساوي عدم وجود فحص.

الاستيعاب الموصوف أدناه يعمل على عناوين أدلة ننتقيها نحن، على ملفات يقدّمها ناشروها لأي أحد. أما عنوان الدليل الذي تقدّمه أنت فيُنزَّل ويُقرأ على جهازك، ونحن لا نحتفظ بشيء من تلك البيانات. وفاكّا الضغط هما الملف نفسه منقولًا بين قاعدتَي الشيفرة، مع تفرّع السقوف الافتراضية بنسبة ثمانية إلى واحد، فمقال واحد يغطيهما معًا.

العضو الذي ينتهي مبكرًا قد ينتهي نظيفًا مع ذلك

ملف gzip سلسلة من الأعضاء، وكل عضو ترويسة من عشرة بايتات تليها إضافات اختيارية، ثم تدفق deflate، ثم مقطع ختامي من ثمانية بايتات يحمل CRC32 يتبعه ISIZE، أي الحجم غير المضغوط بباقي القسمة على 2^32. داخل تدفق deflate تحمل كل كتلة بتًّا واحدًا يقول ما إذا كانت الكتلة الأخيرة في مجموعة البيانات. يتوقف فاكّ الضغط عندما يقرأ ذلك البتّ وتنتهي تلك الكتلة. لا شيء في ذلك التسلسل يُلزمه بقراءة المقطع الختامي، وإذا غاب المقطع الختامي فلا شيء يشير إلى أنه كان متوقعًا. انتهى التدفق لأن التدفق قال إنه انتهى.

الفكرة التالية البديهية أن قطعًا أعمق، يقع داخل بيانات deflate، لا بد أن يُنفد مدخل القارئ في مكان ما يلاحظه على الأقل. لا يحدث ذلك، وقد فاجأني. أزلت من 1 إلى 50 بايتًا من نهاية ملف بعضو واحد فأرجع فاكّ الضغط خرجًا جزئيًا عند كل عمق دون كلمة: 90 بايتًا عند قطع بايت واحد، و67 عند قطع عشرين بايتًا، و48 عند قطع أربعين بايتًا. لا يوجد عمق يبدأ عنده وقت التشغيل بالاعتراض.

فما يجعل القطع عند حد العضو هو الخطير هو شكل الخرج، لا حجم الصمت. اقطع أعمق فيُسلَّم قارئ XML عادةً عنصرًا لا يُغلَق أبدًا فيرفض الملف، وهذا بالضبط ما يقوله تعليقنا نحن على مسار دليل الأسرة. اقطع عند الحد فيكون الخرج بادئة من المستند الحقيقي بايتًا ببايت، حسن التكوين حتى آخر عنصر كامل فيه، والحقلان اللذان كانا سيناقضانه هما الاثنان اللذان كانا ما يزالان في الطريق حين انقطع الاتصال. الدليل الذي يصل على تلك الهيئة يبدو كمصدر بقوائم برامج شحيحة، وهي شكوى مختلفة عن دليل برامجه كلها مزاحة بساعة وتحتاج إلى إصلاح مختلف.

القطوع التي نستطيع التفكير فيها تأتي من HTTP، الذي يؤطّر الجسم بطريقتَين. الجسم الأقصر من Content-Length المصرّح استجابة معطوبة والعميل يقول ذلك بصوت عالٍ: مقابل مقبس وعد بـ 100 بايت وأرسل 40، أطلق .NET استثناء HttpRequestException يغلّف HttpIOException برسالة «The response ended prematurely». أرسل البايتات الـ40 نفسها بلا ترويسة طول وأغلق الاتصال، فيُرجعها العميل نفسه بلا خطأ، لأن الجسم المحدود بالإغلاق كامل بحكم التعريف ما لم يبلّغ الاتصال نفسه عن عيب. والوكيل الذي يستسلم في منتصف الجسم ينتج ذلك بالضبط: تدفقًا ينتهي، عن قصد بقدر ما تستطيع أي طبقة أدنى أن تعرف.

الفحص يبحث عن رقم يعرفه سلفًا

هذا هو كله. ويستحق أن يُقرأ سطرًا سطرًا، لأن كل سطر فيه تقريبًا تنازل لشيء حقيقي.

const int MinGzipLength = 18;
const int TailWindow = 512;

if (!input.CanSeek || input.Length < MinGzipLength || written is <= 0 or > uint.MaxValue)
{
    return;
}

var take = (int)Math.Min(TailWindow, input.Length);
var tail = new byte[take];
input.Position = input.Length - take;
await input.ReadExactlyAsync(tail, cancellationToken).ConfigureAwait(false);

var expected = (uint)written;
for (var i = 0; i + 8 <= tail.Length; i++)
{
    if (BinaryPrimitives.ReadUInt32LittleEndian(tail.AsSpan(i + 4, 4)) == expected)
    {
        return;
    }
}

var trailing = BinaryPrimitives.ReadUInt32LittleEndian(tail.AsSpan(take - 4, 4));
if (trailing > written)
{
    throw new InvalidDataException(
        "Gzip stream is truncated: the trailer reports more bytes than were decompressed.");
}

الـ18 ترويسة ومقطع ختامي بلا شيء بينهما، فهو حد أدنى لـ«أقصر من أن يحمل مقطعًا ختاميًا أصلًا»، لا اختبار صلاحية. لا شيء حقيقي يقترب من ذلك: أصغر عضو ينتجه هذا المشفّر لبايت واحد من المدخل يبلغ 21. شرط البحث في التدفق هو أول حد حقيقي: المستدعي الذي يتدفق مباشرة لا يمكن فحصه، فينسحب الفحص بدلًا من رفض أولئك المستدعين. مسارات الاستيعاب الثلاثة لدينا كلها تفك الضغط من ملف أو من مخزن مؤقت، فالثلاثة كلها تُفحَص.

الانسحاب عند > uint.MaxValue هو الحد الثاني، وهو بنيوي. ISIZE هو الحجم غير المضغوط بباقي القسمة على 2^32، فمتى نفك التشغيل أربعة غيبيبايتات لم يعد الحقل قابلًا للمقارنة بأي شيء. هذا عرض الحقل يتكلم: يطفئ الفحص نفسه لكل تشغيل، بصمت، لأكبر الأدلة بالتحديد، وأي سقف مطلق يوضع فوق تلك النقطة يشتري مساحة إضافية بثمن فحص القطع.

ثم المسح. يقرأ آخر 512 بايتًا، ويمشي على كل إزاحة في تلك النافذة، ويقرأ الثانية من كل زوج من الكلمات ذات الأربعة بايتات بوصفها ISIZE مرشّحًا. المطابقة في أي مكان تعني أن عضوًا بالحجم الذي نفكناه تمامًا ينتهي في مكان ما في ذلك الذيل، وهذا ما يتسامح مع الملف المحشو. وله ثمن: 505 إزاحة مرشّحة مقابل قيمة من أربعة بايتات تعطي مطابقة زائفة مرة واحدة تقريبًا في ثمانية ملايين ونصف المليون إذا لم تكن بايتات الذيل ذات صلة بنا. ذلك حساب، لا قياس، والنوافذ متداخلة، فليست سحوبات مستقلة.

إن لم تطابق أي إزاحة، تُقرأ آخر أربعة بايتات كرقم وتُقارَن. المقطع الختامي الذي يبلّغ عن بايتات أقل ممّا كتبناه متسق مع آخر عضو من عدة أعضاء مسلسلة، فيمرّ. والمقطع الختامي الذي يبلّغ عن أكثر لا يمكن أن يأتي من ملف كامل، فيفشل. ذلك التباين هو الحكم بكامله، وهو موضع وجوب الصدق. عندما يغيب المقطع الختامي، تكون تلك البايتات الأربعة بيانات مضغوطة تُقرأ كطول، والفحص يعمل فقط لأن قيمة عشوائية من 32 بتًّا تكون عادةً أكبر من عدد البايتات التي نفكناها. عند نفك 40 MB يفلت ذلك مرة واحدة تقريبًا في مئة وسبعة. وعند نفك غيغابايتَين يبلغ 0.47، ولأن الرقم في تلك النسبة هو ما نفكناه فعلًا وليس الحجم الحقيقي للدليل، تكون الاحتمالات في أسوأ حالاتها عند القطع الأهم: ذاك الذي يصل شبه كامل.

فالحارس يضعف كلما كبرت الأدلة، وهذا هو الاتجاه المعكوس، وأكبر الأدلة هي التي يطول تنزيلها بما يكفي لأن يُقطَع.

وظيفتان من الثلاث يؤديهما وقت التشغيل أصلًا

الحلقة التي تمرّ على الأعضاء المسلسلة تحمل في معالج الاستثناءات فيها ما اعتقدت طويلًا أنه منطق الحشو.

catch (Exception ex) when (ex is InvalidDataException or EndOfStreamException)
{
    // Trailing padding after a member that ended cleanly is tolerated;
    // some upstreams append junk after the gzip trailer. Anything else
    // is a truncated download, and swallowing it would write a partial
    // guide, hash it as the content, and then skip every later fetch as
    // "unchanged" until the upstream bytes happen to move.
    if (completedMembers == 0 || guard.Written > writtenBefore)
    {
        throw;
    }

    break;
}

زوّدت نسخة أمينة من تلك الحلقة بأدوات قياس فوق اثني عشر شكلًا من المدخلات، كتدفق ذاكرة وكتدفق ملف على السواء، فلم تُطلَق فقرة catch ولا مرة في التشغيلات الـ24 كلها. GZipStream يسلسل الأعضاء بنفسه، فيصل عضوان كقراءة واحدة من 180 بايتًا وتعدّ الحلقة عضوًا واحدًا. ويتسامح مع القمامة الزائدة بنفسه، فالملف المحشو لا يطلق شيئًا أيضًا. في كل شكل تثبّته مجموعة اختباراتنا، تعمل الحلقة دورة واحدة بالضبط ويتخذ فحص المقطع الختامي القرارات الثلاثة كلها: قبول الملف المحشو، ورفض المقطوع، ورفض الملف الذي قُطع عضوه الثاني. المعالج بقية من الماضي، وتعليقه هو المكان الوحيد الذي كُتبت فيه قاعدة الحشو.

وهذا مهم لأن القاعدة الحقيقية أضيق من التعليق. التسامح مع الحشو يأتي من مسح الـ512 بايتًا، فيدوم بالضبط ما دام المقطع الختامي الحقيقي داخل النافذة. مسحت أطوال حشو من 0 إلى 520 بايتًا: 504 بايتات من الحشو تُقبَل و505 تُرفَض على أنها قطع. بعد ذلك يسقط الفحص إلى آخر أربعة بايتات من الحشو ويقرأها كطول. الحشو المملوء بالأصفار ينجو من الحد بمحض الصدفة، لأن أربعة بايتات صفرية تُقرأ كطول لا شيء، وقاعدة قبول الأصغر تمرّرها. أما حشو 0xFF بالطول نفسه فيُقرأ 4,294,967,295 ويُرفَض.

لم أعاين قط السلوك الذي يصفه ذلك التعليق. المصادر العلوية التي تلحق قمامة بعد المقطع الختامي زعم في شيفرتنا واسم اختبار، لا ملاحظة.

سقفان يعيشان بجوار فحص المقطع الختامي

حلقة النسخ نفسها تفرض حدَّين لا علاقة لهما بالقطع: سقفًا مطلقًا للبايتات المكتوبة، وسقفًا لنسبة المنفوك إلى المضغوط، يلتقط ملفًا صغيرًا يتمدد إلى شيء ليس دليلًا. يُختبَر كلاهما داخل الحلقة، عند كل كتلة من 80 KB، فتفشل القنبلة في منتصف الطريق بدلًا من أن تفشل بعد كتابة كل شيء.

لا أنشر أرقامنا الحالية، ووثائقنا نحن هي السبب. مواصفة الخدمة، المؤرخة في 19 يوليو 2026، ما زالت تسجّل زوجًا من سقوف الجلب تجاوزته التهيئة المشحونة منذ ذلك الحين. ما حرّكهما دليل وطني حقيقي واحد تبيّن أنه يبلغ نحو 200 MB مضغوطًا ويتمدد إلى غيغابايتَين تقريبًا: كان سقف الضغط يومها يقع على ذلك الخط تمامًا، ورفض سقف فك الضغط الملف رفضًا قاطعًا. الرقم في مقال يشيخ كما يشيخ الرقم في مواصفة. إذًا: ضع سقفًا للحجم المطلق، وسقفًا للنسبة، واختبر كليهما أثناء الكتابة، وأبقِ السقف المطلق تحت النقطة التي يتوقف عندها ISIZE عن كونه قابلًا للمقارنة، وإلا كلفتك المساحة الإضافية فحص القطع.

تفصيلان يحملان أكثر من القيم. سقف النسبة لا يُطبَّق إلا عندما يعرف المستدعي الحجم المضغوط، فالمستدعي الذي يمرّر صفرًا يحصل على السقف المطلق وحده، واختبارات القطع لدينا تفعل ذلك بالضبط عن قصد. ومسار دليل الأسرة على الجهاز لا يستدعي فاكّ الضغط المشترك أصلًا: يغلّف تدفق gzip خامًا في تدفق عدّ يفرض سقفًا للبايتات ولا يفرض سقفًا للنسبة، ثم يستدعي فحص المقطع الختامي مباشرة. ذاك ثغرة لا قرار، ولم أجده مكتوبًا في أي مكان.

البوابة التي تحوّل الجسم السيء إلى جسم دائم

والآن النصف الذي لا يكتب عنه أحد.

استيعاب الدليل مكلف ومعظم عمليات الجلب لا تغيّر شيئًا، فالجلب شرطي: If-None-Match وIf-Modified-Since ممّا خزّنه التشغيل الأخير، واستجابة 304 تنهي التشغيل. المدقّقات لا تكفي وحدها. فهرس عام واحد نحلّه يقدّم ETag بلا علامات اقتباس، وهو لا يعود سالمًا في الرحلة ذهابًا وإيابًا، فالطلبات الشرطية إليه لا تختصر أبدًا. البوابة المرجعية هي SHA-256 للبايتات المنفوكة، تُقارَن بالتجزئة المخزّنة على صف المصدر.

تجزئة المحتوى هي البوابة الأفضل بكل معنى يهم. تُحسَب محليًا، ولا يستطيع أي مصدر علوي خداعها بالكذب في مدقّقاته أو بإغفالها. وليس لها أيضًا أي مخرج طوارئ، وهذه هي المشكلة كلها. تُؤخَذ التجزئة عند الجلب، فوق الملف المؤقت المنفوك، قبل أن ينظر محلِّل في عنصر واحد، فلا تستطيع البوابة التمييز بين جسم دُمج بنظافة وجسم انفجر بعد خطوتَين.

ثلاث خطوات ويصبح الفشل دائمًا:

  1. يصل جسم لا ينبغي قبوله، مقطوع أو مجرد غير قابل للاستخدام.
  2. تُحسَب تجزئته وتُخزَّن كتجزئة محتوى المصدر.
  3. كل جلب لاحق للبايتات نفسها يعطي القيمة نفسها ويُرجع «متخطى، بلا تغيير» دون أن يصل الجسم إلى محلِّل مرة أخرى.

شرط التعافي هو أن تتحرك بايتات المصدر العلوي مصادفةً، وهي العبارة التي يستعملها تعليق فاكّ الضغط نفسه. للدليل الوطني الذي يُولَّد كل ليلة، ذلك يوم. وللملف الذي لا يولّده أحد من جديد، ذلك أبدًا، وآلية إعادة المحاولة الموجودة للأعطال العابرة لا تستطيع المساعدة، لأنه من التشغيل الثاني فصاعدًا لا يوجد فشل يُعاد.

آخر دورة في البرغي: هذه الحالة تبلّغ عن الصحة. التشغيل المتخطى بلا تغيير يعني أن المصدر العلوي قابل للوصول وأن الدليل حديث، فيحسبه فحص الصحة نجاحًا ويحمل عدد القنوات السابق إلى الأمام تأييدًا. المصدر المجمّد على تجزئة جسم لم يُدمَج قط يبدو، على كل لوحة معلومات لدينا، مصدرًا على ما يرام.

كانت الكتابة لدينا في المكان الخطأ

نسختنا نحن من هذا لم يكن لها علاقة بالقطع. كانت مسألة ترتيب.

كانت المدقّقات تُكتَب فور الجلب، قبل التحليل وقبل الدمج. التشغيل الذي جلب جسمًا سليمًا ثم فشل لاحقًا، بسبب تعثر في قاعدة البيانات، أو قتل بسبب نفاد الذاكرة، أو مهمة صيانة كانت في ذلك الوقت تحذف كل جداول التهيئة المؤقتة لكل تشغيل بما فيها جدول ما زال استيعاب جارٍ يستخدمه، كان يسجّل التشغيل فاشلًا ويخزّن التجزئة الجديدة رغم ذلك. الدورة التالية تجلب البايتات نفسها، فتطابق، فتتخطى. يتقادم المصدر ويبلّغ عن الصحة، ولا يمسّه أي قدر من إعادة المحاولة.

تصادم جداول التهيئة المؤقتة مُصلَح الآن بحد ذاته: الصيانة تتخطى أي جدول ما زال تشغيله موسومًا بأنه جارٍ وأحدث من عتبة الإهمال. وإصلاح الترتيب استدعاء واحد نُقل، والتعليق حيث تقع الكتابة الآن يقول لماذا:

// Validators persist only after the merge has succeeded. Writing
// them right after the fetch made any parse or merge failure
// permanent: the run was marked failed, but the new hash was already
// stored, so every later fetch of the same bytes short-circuited to
// skipped_unchanged and the source never re-merged until upstream
// changed. With the write here, a failed run leaves the old
// validators in place and the next cycle retries the full ingest.

مسار 304 يحتفظ بكتابته الخاصة، وتلك آمنة، لأن استجابة 304 تثبت أن المحتوى يطابق ما دُمج بالفعل، وتحديث المدقّقات لا يمكن أن يحجب شيئًا. القاعدة أن تكتب المدقّقات فقط بعد أن يفرغ الجسم من كونه نافعًا لك.

اختبار الانحدار على الهيئة التي أريدها لأي بوابة كهذه. ينصب مصدرًا علويًا حقيقيًا يقدّم 22 محرفًا من XML معطوب، ويؤكد أن التشغيل فشل مع بقاء تجزئة المحتوى المخزّنة null، ثم يشغّله مرة أخرى فوق البايتات نفسها ويؤكد أنه فشل للمرة الثانية بدلًا من أن يتخطى. واختبار مرآة يثبت أن تحسين التخطي ما زال يعمل بعد النجاح، فلا يمكن أن يكون الإصلاح «أوقفوا التخبئة» بالصدفة.

وجدنا الأصل بقراءة شيفرتنا نحن أثناء تدقيق للمنصة في أغسطس 2026، لا من بلاغ، وهذا هو الجزء غير المريح. أنتج التدقيق 110 نتيجة وصنّف هذه بدرجة High. لا أستطيع أن أقول لك كم مرة وقع ذلك في الإنتاج، لأن الحالة التي ينتجها لا يمكن تمييزها من الصحة، والدليل الذي يقدّمه قديم لا غائب.

الجهاز له الهيئة نفسها وبلا تجزئة

نصف الخادم مُصلَح. ونصف الجهاز ليس كذلك، وأفضّل أن أكتب ذلك على أن أترك هذه الصفحة توحي بالتماثل.

استيراد الدليل لكل مصدر في تطبيق Windows لدينا يخزّن ETag وLast-Modified بعد سطر واحد من عودة الجلب، قبل فك الضغط، وقبل التحليل، وقبل أن يصل أي شيء إلى المخزن المحلي. ولا يوجد مسار شيفرة يمسحهما بعد ذلك. مصرف الفشل يحذف جيل الدليل نصف المكتوب ويترك بيانات الجلب الوصفية كما هي؛ والتخلص لا يمسّ هذا ولا ذاك. فالمصدر العلوي الذي يحترم الطلبات الشرطية يستطيع أن يقدّم جسمًا نفشل في تحليله، ويستلم مدقّقاتنا المحدّثة، ويجيب الطلب التالي باستجابة 304 لا تدمج شيئًا.

شيئان يخففانه، ولا يسدّه أي منهما. الجهاز لا يحتفظ بأي تجزئة محتوى، بل بالمدقّقَين فقط، فالفخ يحتاج إلى مصدر علوي يحترم الطلبات الشرطية فعلًا، وأصحاب المدقّقات المعطوبة محصّنون بالصدفة. والاستيراد الفاشل يترك الدليل السابق قابلًا للقراءة، لأن مؤشر الجيل لا يُبدَّل إلا عند الإكمال، فالعَرَض الظاهر دليل يتوقف عن التقدم لا دليل يتلاشى. ذاك فشل أفضل من فشل الخادم، وهو مع ذلك العلة نفسها.

أما مسار الأسرة، الذي يجمع أدلة أسرة كاملة في مخزن واحد، فيصيب في هذا بطريقة مثيرة للاهتمام. لا يستطيع أن ينفك إلى مخزن مؤقت ثم يفحص، لأن المجموع أكبر من أن يُحتفَظ به، فيحلّل مباشرة من تدفق gzip ويتحقق من المقطع الختامي بعد ذلك. وقبل التحقق يستنزف ما تركه قارئ XML خلفه، لأن العدد المسلَّم إلى الفحص يجب أن يكون العضو كاملًا لا المدى الذي بلغه القارئ مصادفةً. حلّل أولًا، ثم استنزف، ثم افحص. ذلك الترتيب مزعج في القراءة وهو الوحيد الذي يعمل.

ما الذي يعود عليك من هذا

تطبيق Windows لدينا يشغّل فاكّ الضغط هذا على جهازك، مقابل عنوان الدليل الذي أعطيته إياه، ولا يمرّ أي تدفق عبرنا في أي لحظة. الدليل الذي يفشل في فحص المقطع الختامي يترك الدليل الذي لديك بالفعل على الشاشة، وهذا السلوك الذي تريده من تحديث في الخلفية لم تكن تراقبه. يمكنك إنشاء حساب مجاني وتوجيهه إلى مصدر لديك بالفعل.

إن أخذت عادة واحدة من هذه الصفحة، فلتكن عدم الثقة بكلمة «بلا تغيير». بوابة التخبئة التي تفتح بمحتوى الجسم هي البوابة الصحيحة والتي كنت سأبنيها مرة أخرى، وهي أيضًا بوابة لا تملك طريقة لتقول «قبلت ذلك الجسم ثم ساء كل ما بعده». مهما حسبت عند الباب، خزّنه عند النقطة التي تعرف فيها أن العمل نجح، وأعطِ فحص الصحة لديك شيئًا ينظر إليه غير غياب الخطأ.

ما قياس هذه المادة32 ادعاءات، كل منها مزود بالأدلة التي تقف خلفه
المطالبةالأدلةعد
ملف gzip سلسلة من الأعضاء؛ كل عضو ترويسة ثابتة من عشرة بايتات تليها حقول اختيارية، ثم تدفق deflate، ثم مقطع ختامي من ثمانية بايتات يحمل CRC32 يتبعه ISIZE، أي الحجم غير المضغوط بباقي القسمة على 2^32.RFC 1952 section 2.2 (file format, series of members), section 2.3 (member format), section 2.3.1 (member header and trailer, which defines both CRC32 and ISIZE as the size of the original input modulo 2^32). https://www.rfc-editor.org/rfc/rfc1952.htmlالمواصفاتلا ينطبق
كل كتلة deflate تحمل بتّ BFINAL يحدد ما إذا كانت الكتلة الأخيرة في مجموعة البيانات، فيستطيع فاكّ الضغط إنهاء التدفق دون أن يقرأ ما يليه أبدًا.RFC 1951 section 3.2.3: "BFINAL is set if and only if this is the last block of the data set." https://www.rfc-editor.org/rfc/rfc1951.htmlالمواصفاتلا ينطبق
الاستجابة بلا طول مصرّح، التي ينتهي جسمها عند إغلاق الاتصال، تُعامَل على أنها كاملة ما لم يبلّغ الاتصال الأساسي عن خطأ، فلا يجد العميل شيئًا يقارن به الطول المستلم.RFC 9112 section 6.3 rule 8 (body length is the octets received before the server closes the connection) and section 8, Handling Incomplete Messages. https://www.rfc-editor.org/rfc/rfc9112.htmlالمواصفاتلا ينطبق
مع إزالة المقطع الختامي كاملًا بثمانية بايتاته، يُرجع GZipStream الحِمل كاملًا ولا يطلق شيئًا.n = 123‏/08‏/2026
لا يوجد عمق قطع يمكن اكتشافه عند فاكّ الضغط: إزالة من 1 إلى 50 بايتًا من ملف بعضو واحد تُرجع خرجًا جزئيًا ولا تطلق شيئًا عند كل عمق، 90 بايتًا عند قطع 1 بايت و48 بايتًا عند قطع 40 بايتًا.n = 5023‏/08‏/2026
المقطع الختامي الموجود لكن المعطوب يطلق استثناءً فعلًا: قلب بتّ واحد في أي من مواضع بايتات المقطع الختامي الثمانية يطلق InvalidDataException دون تسليم أي شيء.n = 823‏/08‏/2026
خمسة أطوال قطع مثبّتة كحالات فشل في خط المعالجة: إزالة 1 و4 و8 و20 و40 بايتًا من عضو مبني فوق مستند من 90 بايتًا، والرفض في الحالات الخمس كلها يأتي من فحص المقطع الختامي لا من GZipStream.n = 523‏/08‏/2026
يقرأ فحص المقطع الختامي نافذة ذيل من 512 بايتًا، ويتخطى الملفات الأقل من 18 بايتًا والتدفقات غير القابلة للبحث، ويمسح كل إزاحة بحثًا عن مقطع ختامي مرشّح كلمته الثانية تساوي عدد البايتات المنفوكة، وإن لم يطابق أي منها قرأ آخر أربعة بايتات وفشل إذا تجاوزت ما نُفِك.n = 123‏/08‏/2026
حد الـ18 بايتًا الأدنى هو ترويسة ومقطع ختامي بلا شيء بينهما؛ وأصغر عضو gzip حقيقي أكبر من ذلك، 21 بايتًا لبايت واحد من المدخل.n = 123‏/08‏/2026
505 إزاحة مرشّحة مقابل قيمة من أربعة بايتات تعطي مطابقة زائفة بمعدل يقارب واحدًا في ثمانية ملايين ونصف المليون عندما لا تكون بايتات الذيل ذات صلة بعدد البايتات المنفوكة.Arithmetic: the loop condition i + 8 <= 512 runs i in [0, 504], so 505 offsets; 505 * 2^-32 = 1.18e-7, or 1 in 8.50 million. Offset count confirmed by measurement.المواصفاتلا ينطبق
عندما يغيب المقطع الختامي، تكون آخر أربعة بايتات بيانات مضغوطة تُقرأ كطول، ويقبل الفحص كلما لم تكن تلك القيمة أكبر من عدد البايتات المنفوكة: نحو واحد في مئة وسبعة عند نفك 40 MB، و0.47 عند نفك غيغابايتَين.Arithmetic: P(accept) = written / 2^32; 40e6/2^32 = 0.0093 (1 in 107.4), 2e9/2^32 = 0.466.المواصفاتلا ينطبق
الحشو الزائد بعد عضو نظيف يُتسامح معه فقط ما دام المقطع الختامي الحقيقي داخل نافذة الـ512 بايتًا: 504 بايتات من الحشو تُقبَل و505 تُرفَض على أنها قطع.n = 52123‏/08‏/2026
يسلسل GZipStream الأعضاء ويتسامح مع القمامة الزائدة بنفسه، ففي كل شكل تثبّته مجموعة الاختبارات تعمل حلقة الأعضاء دورة واحدة، ولا تُطلَق فقرة catch فيها ولا مرة، ويأتي كل حكم من فحص المقطع الختامي.n = 1223‏/08‏/2026
ينسحب الفحص عندما يتجاوز عدد البايتات المنفوكة uint.MaxValue، لأن ISIZE هو الحجم بباقي القسمة على 2^32، فالتشغيل الذي يبلغ خرجه أربعة غيبيبايتات يخسر الفحص بصمت.n = 123‏/08‏/2026
مسارات الاستيعاب الثلاثة كلها تفك الضغط من ملف أو من مخزن مؤقت، فالفحص المعتمد على البحث في التدفق يعمل عليها جميعًا.n = 323‏/08‏/2026
تفرض حلقة النسخ سقفًا مطلقًا للبايتات وسقفًا لنسبة المضغوط إلى المنفوك، وكلاهما يُختبَر عند كل كتلة من 80 KB؛ وسقف النسبة لا يُطبَّق إلا عندما يقدّم المستدعي حجمًا مضغوطًا.n = 123‏/08‏/2026
فاكّا الضغط هما الملف نفسه منقولًا بين قاعدتَي الشيفرة، مع تفرّع السقف المطلق الافتراضي بنسبة ثمانية إلى واحد.n = 223‏/08‏/2026
مسار الجهاز على مستوى الأسرة يغلّف فاكّ الضغط في تدفق عدّ يفرض سقفًا للبايتات ولا يفرض أي سقف للنسبة إطلاقًا.n = 123‏/08‏/2026
ما زالت مواصفة الخدمة تسجّل زوجًا من سقوف الجلب تجاوزته التهيئة المشحونة بالفعل.n = 123‏/08‏/2026
دليل وطني حقيقي واحد يبلغ نحو 200 MB مضغوطًا ويتمدد إلى غيغابايتَين تقريبًا؛ وكان سقف الضغط يومها يقع على ذلك الخط تمامًا، ورفض سقف فك الضغط الملف رفضًا قاطعًا.n = 123‏/08‏/2026
يجلب الاستيعاب جلبًا شرطيًا ويبوّب على SHA-256 للبايتات المنفوكة، لأن مصدرًا علويًا واحدًا على الأقل يقدّم ETag لا يعود سالمًا في الرحلة ذهابًا وإيابًا.n = 123‏/08‏/2026
تُحسَب التجزئة عند الجلب، قبل أن يرى أي محلِّل البايتات، فلا تستطيع البوابة التمييز بين فشل التحليل وفشل الدمج.n = 123‏/08‏/2026
الجسم الأقصر من Content-Length المصرّح يطلق HttpRequestException يغلّف HttpIOException بالرسالة "The response ended prematurely"؛ أما البايتات الـ40 نفسها إذا سُلّمت بحدّ الإغلاق فتُرجَع بلا أي خطأ إطلاقًا.n = 223‏/08‏/2026
كانت المدقّقات تُحفَظ فور الجلب، فكان أي فشل في التحليل أو الدمج يصبح دائمًا؛ وهي الآن تُحفَظ فقط بعد دمج ناجح، ويحتفظ مسار 304 بكتابته الخاصة.n = 123‏/08‏/2026
التدقيق الذي كشفه أنتج 110 نتيجة وصنّف هذه النتيجة بدرجة High.n = 11011‏/08‏/2026
كانت مهمة صيانة مجدولة تحذف كل جداول التهيئة المؤقتة لكل تشغيل، بما فيها جدول ما زال استيعاب جارٍ يستخدمه، وكان ذلك المنتج الفعلي لفشل في منتصف التشغيل؛ وهي الآن تستثني الجداول التي ما زال تشغيلها جاريًا وأحدث من عتبة الإهمال.n = 123‏/08‏/2026
التشغيل المتخطى بسبب عدم التغيير يُحسَب نجاحًا في فحص الصحة ويحمل عدد القنوات المتحقَّق منه سابقًا إلى الأمام.n = 123‏/08‏/2026
يقدّم اختبار الانحدار 22 محرفًا من XML معطوب، ويؤكد أن التشغيل يفشل مع بقاء تجزئة المحتوى null، ويؤكد أن تشغيلًا ثانيًا فوق البايتات نفسها يفشل مرة أخرى بدلًا من أن يتخطى؛ واختبار مرآة يثبت أن تحسين التخطي ما زال يعمل بعد النجاح.n = 223‏/08‏/2026
مسار الجهاز لكل مصدر يخزّن ETag وLast-Modified بعد سطر واحد من عودة الجلب، قبل فك الضغط والتحليل، ولا يوجد مسار شيفرة يمسحهما، فيمكن أن يتبع فشل التحليل استجابة 304 لا تدمج شيئًا.n = 123‏/08‏/2026
مسار الجهاز لكل مصدر يحتفظ فقط بـ ETag وLast-Modified، بلا تجزئة محتوى، فنسخته من الفخ تعتمد على احترام المصدر العلوي للطلبات الشرطية.n = 123‏/08‏/2026
الاستيراد الفاشل يترك الدليل السابق قابلًا للقراءة، لأن مؤشر الجيل لا يُبدَّل إلا عند الإكمال.n = 123‏/08‏/2026
مسار الأسرة يحلّل أولًا ثم يتحقق من المقطع الختامي بعد ذلك، مستنزفًا ما تركه قارئ XML حتى يكون العدد المسلَّم إلى الفحص هو العضو كاملًا.n = 123‏/08‏/2026