Amikor egy PDF kereszthivatkozási tábla használhatatlan, a megoldás az, hogy teljesen figyelmen kívül hagyjuk, és a fájltörzsből építjük fel újra. A PDFlibPas Delphi PDF Library ezt egyetlen menetes token-pásztázóval teszi, amely minden valódi indirekt objektumfejlécet rögzít, amit meglát, majd helyreállítja a trailer szótárt, és átadja az újraépített táblát a normál betöltőnek
Mi törik el elsőként, amikor egy PDF sérül?
A kereszthivatkozási tábla a PDF legtörékenyebb része, mert ez az egyetlen rész, amely abszolút bájteltolásokat tárol. Az ISO 32000-1 §7.5.4 tízjegyű, a fájl elejétől számított eltolásként definiálja ezeket a bejegyzéseket, a §7.5.5 pedig a startxref kulcsszót a vége felé helyezi el, amely magára a táblára mutat. Minden ilyen szám érvénytelenné válik bármely szerkesztéstől, amely bájtokat tol el. Egy szöveges módban futó FTP-munkamenet, amely CRLF-et fordított, egy megszakadt letöltés, egy megosztott meghajtón elromlott szektor, egy batch eszköz, amely helytelenül hozzáfűzött inkrementális frissítés írása nélkül: mindegyik tökéletesen olvashatóan hagyja az objektumadatokat, és az index szemétre mutat
Ezért olyan gyakori a "a fájl sérült, és javítás alatt áll" párbeszédablak. A bájtok szinte mindig ott vannak. Ami eltűnt, az a térkép. A rekonstrukció ezért nem az elveszett adatok törvényszéki helyreállítása, hanem egy olyan index újraépítése, amely levezethető a törzsből, és sokkal gyakrabban sikeres, mint azt a felhasználók várnák, mert a drága tartalom, az oldalfák, a betűtípusok és a képek érintetlenek
Miért talál hamis egyezéseket az N 0 obj keresése?
Egy naiv újraépítés az "egész szám, egész szám, obj" mintát keresi a nyers bájtokban, és minden találatot rögzít. Túl sokat talál. A PDF egy konténerformátum, és egy fájl három régiója átlátszatlan az objektumgrammatika számára: kommentek (§7.2), sztringek (§7.3.4) és stream-adatok (§7.3.8). Bármelyik tartalmazhat olyan bájtokat, amelyek pontosan úgy olvashatók, mint egy objektumfejléc, és egyikük sem az. Egy literál sztringben lévő felirat, egy megmaradt debug komment, vagy két megabájtnyi Flate vagy DCT kimenet mind szívesen produkál valamit, ami úgy néz ki, mint egy 99 0 obj
const
Trap: AnsiString =
'4 0 obj'#10 +
'(a caption that mentions 88 0 obj)'#10 + // literal string, not an object
'endobj'#10 +
'% 77 0 obj left over from a debug dump'#10 + // comment, not an object
'5 0 obj'#10 +
'<< /Length 2097152 >>'#10 +
'stream'#10 +
{ two MiB of compressed bytes that contain the byte sequence
99 0 obj and, further along, a complete endstream }
'endstream'#10 +
'endobj'#10;
Minden hamis bejegyzés kétszer kerül pénzbe. Beszennyezi az újraépített táblát egy nem létező objektumszámmal, és elárnyékolhat egy valódi, ugyanolyan számú objektumot, amely később jelenik meg a fájlban. A PDFlibPas ezért egyáltalán nem mintaillesztést végez. Tokenizál, ami azt jelenti, hogy mindig tudja, hogy a kurzor alatti bájtok kód-e vagy payload, és a payloadot anélkül hagyja ki, hogy valaha is értelmezné
Egyetlen menetes állapotgép 64 KiB-os blokkokon
A PDFlibPas pontosan egyszer pásztázza végig a teljes fájlt, 64 KiB-os blokkokban, egy olyan állapotgéppel, amely az ISO 32000-1 §7.2 token-szabályaira és a §7.3.10 indirekt objektum-szintaxisára épül. Egy token szóközön vagy az egyik elhatároló karakteren végződik, és egy objektumfejléc csak akkor kerül rögzítésre, amikor egy pozitív objektumszám, egy nemnegatív generációs szám és egy meztelen obj kulcsszó teljes sorozata megjelent. A rögzített eltolás az objektumszám-token kezdete, ami az, amire egy kereszthivatkozási bejegyzésnek mutatnia kell, nem az obj kulcsszó pozíciója
function RebuildIsWhiteSpace(Value: Byte): Boolean;
begin
Result := (Value = 0) or (Value = 9) or (Value = 10) or
(Value = 12) or (Value = 13) or (Value = 32);
end;
function RebuildIsDelimiter(Value: Byte): Boolean;
begin
Result := (Value = Ord('(')) or (Value = Ord(')')) or
(Value = Ord('<')) or (Value = Ord('>')) or
(Value = Ord('[')) or (Value = Ord(']')) or
(Value = Ord('{')) or (Value = Ord('}')) or
(Value = Ord('/')) or (Value = Ord('%'));
end;
A fontos részlet az, hogy a token-állapot és a sztring-állapot túléli a blokkhatárt. Egy fejléc, amely átnyúlik a 65536-bájtos vonalon, továbbra is felismerhető, mert a részleges token, a függőben lévő egészpár és az in-string jelzők mind átviszik magukat a következő blokkba. A pufferek fixek: 64 KiB a pásztázáshoz, 32 bájt a leghosszabb tokenhez, amely egyáltalán számíthat, és az egyetlen tömbök, amelyek a fájllal együtt nőnek, az objektumszám-, generációszám- és 64-bites eltolás-listák, amelyek a valódi objektumszámmal arányosak, nem a fájlmérettel. Gyakorlatban a pásztázás szekvenciális olvasásokat végez, és legfeljebb két explicit keresést (seek) az egész dokumentumon, ami teszi életképessé a több száz megabájtos bemeneteken, amelyeket a közvetlen hozzáférésű egyesítésről és felosztásról szóló cikk tárgyal
Miért nem lehet megbízni abban, hogy egy stream az endstream-nél ér véget?
Mert a stream-adat tetszőleges bájt, és a tetszőleges bájtok véletlenül kiadhatják az endstream szót. Egy stream, amely a stream kulcsszó után kezdődik, átlátszatlan adatként kihagyandó, amíg valóban véget nem ér, de a záró kulcsszó első előfordulása csak jelölt. A PDFlibPas ezt megerősítés megkövetelésével oldja meg: egy endstream token csak akkor fogadható el a stream valódi végeként, ha a következő nem-szóköz token egy önálló endobj, az a sorrend, amit a §7.3.8 megkövetel egy stream-objektum körül. Egy véletlen találat a tömörített adatokon belül szinte sosem rendelkezik ezzel a folytatással, így a pásztázó a streamen belül marad, és folytatja. Két kisebb szabály ugyanannyira számít. A stream kulcsszó csak akkor lép be stream-állapotba, ha meztelen kulcsszó, így egy névobjektum, mint például egy szótárban lévő /stream, sosem váltja ki. És egy obj vagy trailer token csak akkor fogadható el, ha a token nem lépte túl a 32-bájtos plafont, és nem kezdődött perjellel. E két őrzés nélkül egy rossz kulcsneveket tartalmazó erőforrás-szótár elég lenne ahhoz, hogy kisiklassa a pásztázást, ami pontosan az a fajta ellenséges bemenet, amelyet a nem megbízható PDF-ek biztonságos elemzéséről szóló jegyzetek tárgyalnak
A trailer szótár valódi végének megtalálása
Az objektumok helyreállítása csak a munka fele, mert a betöltőnek még mindig szüksége van egy trailerre, hogy megtalálja a /Root-ot. A PDFlibPas megjegyzi az utolsó 64 trailer kulcsszó-pozíciót, amelyet a pásztázás során talált, és visszafelé érvényesíti őket, a legfrissebbtől kezdve, így a legújabb használható trailer nyer, és egy elszabadult kulcsszó, amelyet nem szótár követ, egyszerűen elbukik az érvényesítésen, és átesik az előző jelölthöz. Minden jelölt 1 MiB-os korláttal kerül beolvasásra, és a szótár vége az beágyazott << és >> mélység nyomon követésével kerül megtalálásra, a literál sztring escape-ekkel, hexadecimális sztringekkel és kommentekkel együtt
// A naive reader that stops at the first '>>' truncates this trailer,
// and a fixed 2048-byte window can cut it in half on a large one
'trailer'#10 +
'<< /Size 5 /Root 1 0 R' +
' /Custom << /Text (value >> preserved) >> >>'#10
A mélységkövetés nem elméleti kérdés. Egy csonkolt trailer, amely elveszíti az /Encrypt-et, egy helyreállítható titkosított dokumentumot megnyithatatlanná tesz, és az /Info vagy egy egyéni alszótár elvesztése csendben eldob olyan metaadatokat, amelyektől egy downstream rendszer függhet. Ha a fájl titkosított, a helyreállított trailer az, ami lehetővé teszi a normál hitelesítő-útvonal futását, és az újrapróbálkozási szemantika ugyanaz, mint amit a titkosított dokumentumbetöltésről szóló cikk leír
Amit a rekonstrukció nem tud visszaadni
A rekonstrukció legjobb erőfeszítés, és tisztességesnek lenni a korlátaival kapcsolatban a szállítás része. Három eset teljes kudarcot vall. Az objektumfolyamokba csomagolt objektumok (§7.5.7) egyénileg nem láthatók egy bájtpásztázás számára, így ha egy tároló túlél, de a kereszthivatkozási folyama (§7.5.8) nem, az általa tartott objektumok nem lesznek indexelve az újraépítés által. Egy fájl, amelynek törzse ténylegesen megsérült, nem csupán rosszul indexelt, olyan fejléceket fog produkálni, amelyek tartalma már nem elemezhető. És egy fájl, amelynek nincs helyreállítható trailer kulcsszava és nincs olvasható katalógusa, semmihez nem tud horgonyozni egy dokumentumfát, függetlenül attól, hány objektumfejlécet találtak
A duplikált objektumszámok az érdekes köztes eset. Egy inkrementálisan frissített fájl jogosan tartalmazza ugyanannak az objektumszámnak több generációját, és a túlélő kereszthivatkozási lánc az egyetlen feljegyzés arról, melyik volt az aktuális. Egy újraépítésnek nincs ilyen lánca, így fájlsorrendben rögzít minden fejlécet, amit lát, és utólag objektumszám szerint oldja fel. Általában a későbbi revízió nyer, ami általában helyes, de egy dokumentum, amelyet frissítettek, majd részben visszaállítottak, finoman másképp térhet vissza, mint amit az eredeti xref leírt. A linearizált fájlok ugyanezt a figyelmeztetést hordozzák a másik irányból: az első oldal elrendezése és a hint-táblák értelmetlenné válnak, amint az index regenerálódik, így egy javított fájlt egyszerű, nem linearizált dokumentumként kell kezelni
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
if Pdf.LoadFromFile('truncated-invoice.pdf', '') = 1 then
begin
if Pdf.GetDocumentRepaired = 1 then
LogWarning('xref was unusable; the table was reconstructed');
if Pdf.PageCount > 0 then
Pdf.SaveToFile('recovered-invoice.pdf'); // writes a clean xref
end;
finally
Pdf.Free;
end;
end;
A visszaesési útvonal automatikus: a PDFlibPas lefuttatja a nyers pásztázást, valahányszor a kereszthivatkozási lánc nem olvasható, és akkor is, ha minden használatban lévő bejegyzés nulla eltolást állít, ami egy olyan tábla aláírása, amelyet megírtak, de sosem töltöttek ki. A GetDocumentRepaired 1-et ad vissza, amikor ez az útvonal futott, és érdemes naplózni ahelyett, hogy figyelmen kívül hagynánk, mert egy dokumentumot, amely rekonstrukción keresztül töltődött be, újra kell menteni egy tiszta fájlba ahelyett, hogy úgy hagynánk egy pipeline-ban, mintha semmi sem történt volna. A mentés friss, konzisztens kereszthivatkozási táblát ír, ami a legolcsóbb lehetséges javítás minden downstream fogyasztó számára
A rekonstrukciós útvonal, a GetDocumentRepaired jelző és az itt bemutatott streamelő betöltő a PDFlibPas Delphi PDF Library része, a máshol a blogon tárgyalt elemzési, renderelési és aláírási API-k mellett