یک ابزار بازیابی را روی یک درایو اجرا میکنید. با عددی تمام میشود که بوی خبرِ خوب میدهد — ۳٬۱۴۸ فایل پیدا شد. بعد شروع میکنید به باز کردنشان. شاید نصفشان کار نکند. بعضی باز میشوند و یک مستطیل خاکستری نشان میدهند. بعضی را نمایشدهنده میپذیرد و عکسِ اشتباهی را نشان میدهد. بعضی ۴ مگابایت روی دیسکاند و اصلاً هیچی درونشان نیست.
نرمافزار بازیابی به شما دروغ نمیگوید. مدخلِ فهرست را پیدا کرده: نام، اندازه، و جایی که فایل قبلاً اشغال میکرد. چیزی که نمیتواند بداند این است که آیا خوشههای پشتِ آن مدخل هنوز همان خوشههایی هستند که به آن تعلق دارند یا نه.
پاک کردنِ یک فایل آن را محو نمیکند. در NTFS مدخل علامت میخورد و فضا آزاد اعلام میشود، و بایتها همانجا میمانند تا چیزی دیگر رویشان نوشته شود. یعنی سه چیزِ مستقل میتواند خراب شود، و ابزارهای بازیابی هر سه را با عنوان «پیدا شد» گزارش میکنند.
روی داده نوشته شده. نام برگشته؛ محتوا حالا مالِ شخصِ دیگری است.
رویِ بخشی از داده نوشته شده. N خوشهٔ اول برگشته، بقیه رفته. این همان موردِ بدجنس است — فایل اندازهای باورپذیر و سرآیندی درست دارد، و در بعضی نمایشدهندهها باز میشود در حالی که آشغال رندر میکند.
داده اصلاً در فایلی که گرفتهاید وجود نداشته. ابزار میدانسته فایل قرار است ۴ مگابایت باشد، پس ۴ مگابایت برایتان نوشته. دو بایتِ اول و دو بایتِ آخر از قالب میآیند؛ هرچه میانشان است پُرکننده است.
نمیتوانید از فایلسیستم بپرسید کدام حالت هستید. اما میتوانید از بایتها بپرسید.
گام ۱: سر و ته
بیشتر قالبها یک امضای ثابت در ابتدا و یک تمامکننده در انتها میگذارند. اگر هرکدام نباشد، فایل بریده شده.
| قالب | باید با این شروع شود | باید با این تمام شود |
|---|---|---|
| JPEG | FF D8 FF | FF D9 (EOI) |
| PNG | 89 50 4E 47 0D 0A 1A 0A | IEND + CRC (49 45 4E 44 AE 42 60 82) |
%PDF- | %%EOF در دو کیلوبایتِ پایانی | |
| DOCX / XLSX / ZIP | PK | پایانِ فهرست مرکزی 50 4B 05 06 |
| MP4 / MOV | یک جعبهٔ ftyp | یک نمایهٔ moov، در هر کدام از دو سر |
به نابرابری توجه کنید، چون اشتباه در اینجا فایلهای معتبر را خراب علامت میزند. EOIِ یک JPEG خودِ دو بایت آخر است، پس به یک پنجرهٔ ۲ بایتی نگاه میکنید. اما EOCD یک ZIP جایی در ۶۴ کیلوبایتِ آخر به علاوهٔ ۲۲ بایت است، چون یک توضیحِ آرشیو میتواند بعد از آن بیاید. و %%EOF یک PDF میتواند هر جایِ ۲ کیلوبایتِ پایانی باشد — و اگر فایل بهروزرسانیِ افزایشی شده باشد، میتواند بیش از یکی باشد. اگر «چک کردنِ N بایت آخر» را روی همهٔ قالبها ثابت کنید، تمام بعدازظهر هشدارِ اشتباه خواهید گرفت.
و بعد MP4 است، که در آن تمامکننده اصلاً در انتها نیست. moov یک جعبهٔ سطحبالاست که میتواند قبل یا بعدِ mdat بنشیند، پس پنجرهٔ انتها آن را پیدا نمیکند. به جایش درختِ جعبهها را قدم میزنید — هر جعبه یک اندازهٔ ۴ بایتی است و بعد یک نوعِ ۴ بایتی، پس میتوانید از یکی به بعدی بپرید:
async function hasMoov(blob) { let off = 0; for (let i = 0; i < 64; i++) { const h = await range(blob, off, 16); if (h.length < 8) return false; if (String.fromCharCode(h[4], h[5], h[6], h[7]) === 'moov') return true; let sz = h[0] * 2 ** 24 + h[1] * 2 ** 16 + h[2] * 256 + h[3]; if (sz === 1) { sz = 0; for (let k = 8; k < 16; k++) sz = sz * 256 + h[k]; } if (sz === 0 || sz < 8) return false; // 0 means "extends to end of file" off += sz; if (off >= blob.size) return false; } return false; } سقفِ آن حلقه تزیینی نیست. یک فایلِ بریده اندازهٔ جعبهای به شما میدهد که به بعد از انتهایِ داده اشاره میکند، و یک قدمزدنِ بیمرز با خوشحالی تا ابد دنبالش میرود.
گام ۲: حفرهٔ میانه
این همان موردی است که هر بررسیِ «آیا معتبر است» را شکست میدهد: فایلی با سرآیندِ درست، تمامکنندهٔ درست، و هیچ در میانه.
ابزارهای بازیابی بهطورِ روتین فایل را تا اندازهٔ اعلامشدهاش بازسازی میکنند. اگر خوشههایی که میانه را نگه میداشتهاند رفته باشند، ابزار پُر میکند. یک JPEG چهار مگابایتی میگیرید که دو بایتِ اول و دو بایتِ آخرش کامل است و محتوایش پُرکننده است. بررسیای که فقط به سر و ته نگاه کند، آن را سالم گزارش میدهد.
پس از میانه نمونه میگیرید. و واضحترین راه انجام این کار — شمردنِ بایتهای صفر — حفرهای در خود دارد که یک کارِ بازیابی از آن رد میشود.
هر ابزاری با صفر پُر نمیکند. بعضی با 0xFF پُر میکنند. بعضی یک الگوی کوتاه را تکرار میکنند. اگر آشکارسازِ شما if (zeroRatio > 0.98) باشد، فایلی که با 0xFF پُر شده مستقیم از آن رد میشود، و این فایلها کمیاب نیستند.
اصلاح این است که دنبالِ یک مقدارِ خاص نگردید و در عوض دنبالِ نبودِ تنوع بگردید. یک تکهٔ ۸ کیلوبایتی از دادهٔ فشردهٔ واقعی عملاً از همهٔ ۲۵۶ مقدارِ بایت استفاده میکند. یک تکهٔ پُرکننده از یکی استفاده میکند، شاید چهار تا. این تمایز همهٔ سبکهای پُر کردن را یکجا میگیرد:
function analyze(u8) { const seen = new Uint8Array(256); let zero = 0; for (let i = 0; i < u8.length; i++) { const b = u8[i]; if (b === 0) zero++; seen[b] = 1; } let distinct = 0; for (let i = 0; i < 256; i++) if (seen[i]) distinct++; return { zeroRatio: +(zero / u8.length).toFixed(3), distinct }; } const isHole = (a) => a.zeroRatio > 0.98 || (a.distinct > 0 && a.distinct <= 4); آزمونِ صفر را هم نگه دارید — جای خودش را دارد، چون تکهای که ۹۹٪ صفر است با چند بایتِ جانبهدربرده هنوز یک حفره است، و ۹۹٪ صفر به شکل، مثلاً، ۳۰ مقدارِ متمایز ظاهر میشود.
گام ۳: نمونهگیری متناسب با اندازه
سه نقطه با فاصلهٔ مساوی یک حفرهٔ پیوسته را میگیرد، که همان چیزی است که بازنویسی معمولاً تولید میکند. اما سه نقطه روی یک ویدیوی ۲ گیگابایتی یک شکافِ ۶ مگابایتی را از دست میدهد، و ۶ مگابایت ویدیو کم نیست.
پس تعداد را با اندازه بزرگ کنید و برایش سقف بگذارید، چون کلِ فایدهٔ نمونهگیری این است که فایل را نخوانید:
function sampleOffsets(size) { if (size < 128 * 1024) return []; const n = Math.min(32, Math.max(3, Math.ceil(size / (4 * 1024 * 1024)))); const step = size / (n + 1); const pts = []; for (let i = 1; i <= n; i++) pts.push(Math.floor(i * step)); return pts; } فایلهای کوچک عمداً چیزی برنمیگردانند. زیرِ حدود ۱۲۸ کیلوبایت فایل همهاش لبه است و میانه ندارد — نمونهگیری از آن نویز تولید میکند نه سیگنال، و یک JPEG در اندازهٔ تصویرِ بندانگشتی بهخاطرِ داشتنِ درونِ کسالتباری علامت میخورد.
جایی که این عمداً میایستد
قالبهای فشردهنشده معافاند. یک BMP مشکیِ واقعی واقعاً و بهدرستی تقریباً همهاش صفر است. یک دقیقه سکوتِ دیجیتال در یک WAV هم همینطور. اگر آشکارسازِ حفره را روی اینها اجرا کنید، فایلهای کاملاً خوب را محکوم میکنید؛ پس اول قالب را چک کنید و وقتی فشرده نیست، نمونهگیری را رد کنید. این تنها جایی است که دانستنِ نوع قبل از قضاوت دربارهٔ محتوا اهمیت دارد.
ساختار محتوا نیست. یک JPEG با SOI و EOI و بدون حفره میتواند با این حال عکسِ اشتباه را دربر داشته باشد، چون خوشههایی که از آن بازسازی شده متعلق به فایلِ دیگری بوده که اتفاقاً نزدیک آن قرار داشته. هیچ بررسیِ سطحبایتی نمیتواند این را تشخیص دهد. فقط یک انسان که آن را باز کند میتواند.
خراب بهمعنای بیارزش نیست. فایلی که ما ناقص مینامیم هنوزهر کسری که جان بهدر برده را دارد. بازیابیِ عکس بهطورِ خاص: یک JPEG که تهاش را ندارد اغلب با این حال بیشترِ تصویر را رندر میکند، و اگر تنها نسخهٔ یک عکس باشد، «بیشترِ تصویر» نتیجهٔ بسیار خوبی است. نگذارید یک ابزارِ بررسی شما را قانع کند چیزی را پاک کنید.
و یک نکتهٔ فایلسیستمی، چون احتمالهایتان را قبل از شروع تغییر میدهد: ext4 اشارهگرهای بلوک را هنگامِ پاک کردن خالی میکند، در حالی که NTFS بیشتر وقتها آنها را باقی میگذارد. نامِ فایلها روی ext4 معمولاً یکسره از دست میرود، و این دلیلی است که بازیابی در لینوکس حتی وقتی خودِ داده جان بهدر برده سختتر از بازیابی در ویندوز است. اگر روی یک درایوِ ext4 کار میکنید، از یک USB زنده انجامش دهید — هرگز از سیستمِ نصبشده، که دارد روی همان دیسکی مینویسد که میخواهید نجاتش دهید.
کلِ چیز، آمادهٔ پیست کردن
سرهمشده. یک File (یا Blob) میگیرد، فقط پنجرههایی را که لازم دارد از طریق Blob.slice() میخواند — که تنبل است، پس تا وقتی بایتها را نخواهید چیزی خوانده نمیشود — و یک حکم همراه با شواهدِ پشتش برمیگرداند.
آن را در کنسولِ DevTools پیست کنید، بعد یا روی یک دستهفایل که دارید صدایش بزنید، یا یک هدفِ دراگانددراپ بسازید و یک پوشه را در آن بکشید:
document.addEventListener('dragover', (e) => e.preventDefault()); document.addEventListener('drop', async (e) => { e.preventDefault(); console.table(await ghostCheck.bulk(e.dataTransfer.files)); }); const ghostCheck = (() => { const KB = 1024, MB = 1024 * 1024; const CHUNK = 8 * KB; const MIN_SAMPLABLE = 128 * KB; // Blob.slice() is lazy: nothing is read until you ask for the bytes. async function range(blob, start, len) { const s = Math.max(0, Math.round(start)); const e = Math.min(blob.size, s + len); if (e <= s) return new Uint8Array(0); return new Uint8Array(await blob.slice(s, e).arrayBuffer()); } function startsWith(u8, sig) { if (u8.length < sig.length) return false; for (let i = 0; i < sig.length; i++) if (u8[i] !== sig[i]) return false; return true; } function indexOf(u8, sig) { outer: for (let i = 0; i + sig.length <= u8.length; i++) { for (let j = 0; j < sig.length; j++) if (u8[i + j] !== sig[j]) continue outer; return i; } return -1; } const ascii = (s) => [...s].map((c) => c.charCodeAt(0)); function analyze(u8) { const seen = new Uint8Array(256); let zero = 0; for (let i = 0; i < u8.length; i++) { const b = u8[i]; if (b === 0) zero++; seen[b] = 1; } let distinct = 0; for (let i = 0; i < 256; i++) if (seen[i]) distinct++; return { zeroRatio: +(zero / u8.length).toFixed(3), distinct }; } // A hole is not always zeros. Recovery tools pad with 0x00, sometimes 0xFF, // sometimes a short repeating pattern. Both look the same from here: // a chunk of real compressed data uses ~256 distinct byte values; padding uses ~1. const isHole = (a) => a.zeroRatio > 0.98 || (a.distinct > 0 && a.distinct <= 4); function sampleOffsets(size) { if (size < MIN_SAMPLABLE) return []; const n = Math.min(32, Math.max(3, Math.ceil(size / (4 * MB)))); const step = size / (n + 1); const pts = []; for (let i = 1; i <= n; i++) pts.push(Math.floor(i * step)); return pts; } const FORMATS = [ { id: 'jpeg', label: 'JPEG', head: [0xff, 0xd8, 0xff], tail: [0xff, 0xd9], tailWindow: 2, compressed: true }, { id: 'png', label: 'PNG', head: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], tail: [0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82], tailWindow: 16, compressed: true }, { id: 'zip', label: 'ZIP / Office', head: [0x50, 0x4b], tail: [0x50, 0x4b, 0x05, 0x06], tailWindow: 65557, compressed: true }, { id: 'pdf', label: 'PDF', head: ascii('%PDF-'), tailText: '%%EOF', tailWindow: 2048, compressed: true }, { id: 'bmp', label: 'BMP', head: ascii('BM'), compressed: false }, { id: 'wav', label: 'WAV', head: ascii('RIFF'), compressed: false }, ]; const FTYP = ascii('ftyp'); // Walk top-level ISO-BMFF boxes looking for `moov`. Capped: a truncated file // can point the offset past the end, and stopping beats looping. async function hasMoov(blob) { let off = 0; for (let i = 0; i < 64; i++) { const h = await range(blob, off, 16); if (h.length < 8) return false; if (String.fromCharCode(h[4], h[5], h[6], h[7]) === 'moov') return true; let sz = h[0] * 2 ** 24 + h[1] * 2 ** 16 + h[2] * 256 + h[3]; if (sz === 1) { // 64-bit extended size sz = 0; for (let k = 8; k < 16; k++) sz = sz * 256 + h[k]; } if (sz === 0 || sz < 8) return false; // 0 means "to end of file" off += sz; if (off >= blob.size) return false; } return false; } async function check(blob) { const size = blob.size; let bytesRead = 0; const headBytes = await range(blob, 0, 16); bytesRead += headBytes.length; const fmt = FORMATS.find((f) => startsWith(headBytes, f.head)) || null; let type = fmt ? fmt.label : 'unknown'; const compressed = fmt ? fmt.compressed : true; if (!fmt && startsWith(headBytes.subarray(4, 8), FTYP)) type = 'MP4 / MOV'; let tail = null; // null = this format has no terminator worth checking if (type === 'MP4 / MOV') { tail = await hasMoov(blob); } else if (fmt && (fmt.tail || fmt.tailText)) { const sig = fmt.tail || ascii(fmt.tailText); const win = await range(blob, size - fmt.tailWindow, fmt.tailWindow); bytesRead += win.length; tail = indexOf(win, sig) >= 0; } const holes = []; let minDistinct = null; if (compressed) { for (const at of sampleOffsets(size)) { const c = await range(blob, Source:https://dev.to/istidaaf/fyly-khh-bzyby-khrdyd-mmkhn-st-ykh-shbh-bshd-brrsy-slmt-fqt-z-rwy-byth-27n7Related posts
What’s !important #20: GTA 6 Scroll Effects, Scrollspies, random()

Steel beams in three.js from the same table as the weight calculator
How to Add Schema Markup in Bricks Builder: Complete WordPress Guide (2026)
Every reveal.js gap closed in Kadal Deck 0.11
