A losLab PDF Library bájtra pontosan azonos PDF-kimenetet tud előállítani azonos bemenethez, ha meghívjuk a SetDeterministicDocumentID(1) függvényt. Alapértelmezés szerint a trailer /ID tömbje a rendszeróra MD5-digestje, így ugyanazon generátor két futtatása legalább ezekben a bájtokban eltér egymástól. A determinisztikus mód ehelyett egy stabil seedből származtatja az /ID-t, ami visszaállítja a reprodukálható buildeket
A tünet általában a CI-ban jelentkezik, még mielőtt bárki elkezdené keresni. A sablon nem változott, a bemeneti rekord nem változott, a betűtípusok nem változtak, mégis a generált PDF minden pipeline-futásnál más hasht kap. A build cache soha nem talál. A tartalomcímezhető tárolóban minden éjszakai buildhez egy friss blob halmozódik fel. A bájtszintű regressziós diffek olyan fájloknál is jeleznek, amelyekhez senki sem nyúlt. Ha a diffet visszakövetjük a tényleges bájtokig, szinte mindig ugyanaz a maroknyi hex számjegy okozza a problémát, a fájl trailerében
Mire való a trailer ID tömbje
A trailer /ID egy fájlazonosító jelölő, nem a tartalom ellenőrzőösszege. Az ISO 32000-1 §14.4 két bájtsorozatból álló tömbként definiálja: az első elem a dokumentum létrehozásakor hozzárendelt állandó azonosító, amelynek minden későbbi szerkesztést túl kell élnie, a második elem pedig a változó azonosító, amelyet egy író minden fájlmódosításkor frissít. Együtt lehetővé teszik egy rendszer számára eldönteni, hogy két fájl egyetlen dokumentum revíziói-e, vagy két, egymással kapcsolatban nem álló dokumentum. A §7.5.5 gyakorlatilag kötelezővé teszi a bejegyzést, mivel a trailernek mindig tartalmaznia kell az /ID-t, amikor tartalmazza az /Encrypt-et is
A specifikáció semmit nem mond arról, hogyan kell kiszámítani az értéket. Az ajánlás olyan dolgok digestje, mint az aktuális idő, a fájl elérési útja, a fájlméret és a dokumentum információs szótára, és a rendszeróra az az összetevő, amely egyedivé teszi az eredményt. Ez pontosan az a tulajdonság, amelyet az azonosításhoz szeretnénk, és pontosan az a tulajdonság, amely tönkreteszi a reprodukálhatóságot, ezért ennek explicit kapcsolónak kell lennie, nem pedig egy csendes viselkedésváltozásnak
Miért állít elő ugyanaz a build minden alkalommal más PDF-et?
Mert az alapértelmezett azonosító a generálás pillanatából származik. Történetileg a losLab PDF Library az /ID sztringeket az aktuális időbélyeg MD5-jéből építette fel, így egy egy másodperccel egymás után kétszer létrehozott dokumentum két különböző állandó azonosítót hordoz, még akkor is, ha a fájl minden más bájtja azonos. A hátrányos hatás valós: egy hash alapján azonosított műveleteket futtató build rendszer soha nem tudja újrahasznosítani a PDF lépést, egy deduplikáló objektumtár buildenként egy másolatot tart fenn dokumentumonkénti másolat helyett, egy bináris diffet néző lektornak pedig bizonyítania kell, hogy az egyetlen változás zaj, mielőtt megbízna a diff többi részében. A determinisztikus /ID-generálás azért létezik, hogy megszüntesse ezt a zajt, ugyanabban a szellemben, mint az objektumfolyamokról és keresztreferencia-folyamokról szóló jegyzetekben bemutatott elrendezés-stabilitási munka
Váltás reprodukálható azonosítóra
A determinisztikus mód opcionális, dokumentumonkénti és alapértelmezés szerint kikapcsolt, így a meglévő kimenet nem változik, amíg nem kérjük explicit módon. A SetDeterministicDocumentID 0-t vagy 1-et fogad el, és 1-et ad vissza, ha az érték elfogadásra került, minden tartományon kívüli értékre 0-t; a GetDeterministicDocumentID az aktuális állapotot jelenti. A SetDocumentIDSeed egy explicit seed sztringet ad meg, amely mindennél elsőbbséget élvez, egy üres seed átadása pedig visszaáll a levezetett seedre. A GetDocumentFileID a mentés után visszaolvassa az /ID[0] értékét, így naplózható vagy assertálható
var
Lib: TPDFlib;
FileID: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed('invoice-4471-rev3');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Invoice 4471');
Lib.SaveToFile('invoice.pdf');
FileID := Lib.GetDocumentFileID; // identical on every run
finally
Lib.Free;
end;
end;
A frissítés mentéskor történik meg, nem a jelző átbillentésekor, így a determinisztikus mód késői bekapcsolása egy dokumentum összeállítása során is érvényesül. Ez azt is jelenti, hogy egy megváltoztatott seed a következő teljes mentéssel kerül be a fájlba: állítsuk be az A seedet, mentsük el, állítsuk be a B seedet, mentsük el, és a két fájl különböző azonosítót fog hordozni, míg az A seed visszaállítása visszaállítja az eredeti értéket. Egy explicit seed a helyes választás, amikor a dokumentumnak természetes, stabil kulcsa van, például egy számlaszám, egy rekordrevízió vagy egy git commit azonosító, mert ez leválasztja az azonosítót az esetleges metaadatokról
Honnan származik a seed, ha nem adunk meg egyet?
Explicit seed hiányában a losLab PDF Library egy olyan dokumentumállapotból származtat egyet, amelynek azonos regenerálások között invariánsnak kellene lennie: a PDF verzió fejléce, az oldalszám és a dokumentum információs szótárának minden bejegyzése. A sztring és a name értékek szó szerint kerülnek felhasználásra, más objektumtípusok a szerializált formájukkal járulnak hozzá, és az egész be lesz hashelve az /ID sztringekbe. A fontos következmény az, hogy a CreationDate és a ModDate az információs szótár része, ezért tervezésénél fogva a seed része is. Két futtatás csak akkor kap ugyanolyan azonosítót, ha valóban ugyanazokat a dokumentum-metaadatokat állítják elő
Lib.SetDeterministicDocumentID(1);
// No SetDocumentIDSeed: the seed is derived from document state,
// so the timestamps in the Info dictionary have to be pinned.
Lib.SetInformation(2, 'Quarterly Report'); // Title
Lib.SetInformation(5, 'reporting-service 4.2'); // Creator
Lib.SetInformation(7, 'D:20260101000000Z'); // CreationDate
Lib.SetInformation(8, 'D:20260101000000Z'); // ModDate
Lib.SaveToFile('report.pdf');
A ModDate rögzítése a 8-as kulccsal kettős feladatot lát el, és ez az a rész, amin sokan fennakadnak. Egy determinisztikus /ID önmagában nem teszi a fájlt bájtra azonossá, mert a mentési útvonal az aktuális idővel bélyegzi meg a ModDate-et, hacsak a hívó explicit módon nem állította be azt. A 8-as kulcs beállítása a hívó által megadottnak jelöli az értéket, és elnyomja ezt a bélyegzést. Ha nem csupán reprodukálható azonosítót, hanem reprodukálható fájlt szeretnénk, kezeljük a metaadat-időbélyegeket build bemenetként: származtassuk őket a forrásrekordból vagy egy rögzített epochból, soha ne a Now-ból
Miért töri el az ID átírása egy titkosított PDF-et?
Mert az /ID[0] egy titkosított dokumentumban nem csupán metaadat, hanem kulcsanyag. Az ISO 32000-1 §7.6.3.3 2. algoritmusa a fájlazonosító első elemét beleszámítja a szabványos biztonsági kezelő titkosítási kulcsának kiszámításába a 2-től 4-ig terjedő revíziók esetén, a kitöltött jelszó, az /O érték és a jogosultsági bitek mellett. A levezetett kulcs ezután előállítja a /U ellenőrző sztringet, amelyet egy olvasó megnyitáskor ellenőriz, a fájlkulcs pedig a Encrypt meghívásakor vagy egy titkosított dokumentum betöltésekor kerül levezetésre és gyorsítótárazásra, mindkettő a mentés előtt történik. Az azonosító átírása mentés közben ezért strukturálisan érvényes fájlt eredményezne, amelynek /U ellenőrzése újranyitáskor meghiúsulna: ez nem egy finom sérülés, hanem egy dokumentum, amelyet senki, még mi sem tudunk megnyitni. Ezért van korlátozva a determinisztikus frissítés azokra a dokumentumokra, amelyek nem hordoznak titkosítási állapotot, és ezért tartja meg egy titkosított dokumentum bármilyen /ID-t, amellyel már rendelkezett, akár determinisztikus a mód, akár nem, a beállításnak egyszerűen nincs hatása ezen az útvonalon. A kapcsolódó revíziókezelést és jogosultsági szemantikát a PDF titkosítás és jogosultság-auditálás bemutatója tárgyalja. Fontos megjegyezni azt is, hogy a titkosítás-visszaállítási útvonal kizárólag az /ID[1], azaz a változásazonosító frissítését végzi, pontosan a §14.4 szándéka szerint
Miért tartják meg az inkrementális mentések az eredeti azonosítót
A második határ a hozzáfűzési mód. Egy inkrementális frissítés a fájl minden korábbi bájtját érintetlenül hagyja, és utána egy új revíziót ír, és az /ID[0] §14.4 szerinti állandósága az, ami közli egy fogyasztóval, hogy az új revízió ugyanahhoz a dokumentumhoz tartozik, mint a régi. Az átírása megszakítaná ezt a kapcsolatot, ellentmondana a fájlban már meglévő revízióknak, és zavarná az aláírás-szemantikát, mivel egy aláírás egy adott dokumentum egy adott revíziójának bájttartományát fedi le. A losLab PDF Library ezért csak teljes mentéseknél frissíti a determinisztikus azonosítót, hozzáfűzési mód közben soha, ami érintetlenül hagyja a PDF inkrementális frissítésekről és a streamhez való hozzáfűzésről szóló cikkben leírt garanciát
Egyetlen áteresztőpont az azonosítógeneráláshoz
Az összes /ID-generálás a losLab PDF Libraryban most egyetlen belső rutinon, a NewFileIDString-en keresztül fut, ez teszi megbízhatóvá a determinisztikus kapcsolót ahelyett, hogy csak egy kódútvonalra rakott folt lenne. Az üres dokumentum létrehozása, egy hiányzó /ID tömb igény szerinti lusta létrehozása, és a titkosítási ujjlenyomat-visszaállítási útvonal is ezt hívja, így pontosan egyetlen olyan hely van, ahol a rendszeróra visszaszivároghatna. Ez azt is jelenti, hogy jövőbeli variánsok, például egy tartalomból levezetett azonosító, egyetlen függvény módosítását igénylik, nem a teljes szerializáló átvizsgálását
function BuildQuote(const Seed: WideString): AnsiString;
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetDeterministicDocumentID(1);
Lib.SetDocumentIDSeed(Seed);
Lib.SetInformation(7, 'D:20260101000000Z');
Lib.SetInformation(8, 'D:20260101000000Z');
Lib.SetOrigin(1);
Lib.DrawText(100, 700, 'Quote 8812');
Result := Lib.SaveToString;
finally
Lib.Free;
end;
end;
// Regression guard: two independent builds, one byte sequence.
if BuildQuote('quote-8812') = BuildQuote('quote-8812') then
WriteLn('reproducible')
else
WriteLn('nondeterminism leaked into the output');
Kapcsoljuk be ezt az összehasonlítást a teszt-suite-ba, mielőtt bárhol máshol reprodukálható kimenetre támaszkodnánk, mert hangosan elbukik, amint valamilyen új funkció újra bevezet egy időbélyeget. A reprodukálhatóság egyébként csendben lebomló tulajdonság, egyetlen assertion két memóriabeli mentés fölött pedig szinte semmibe nem kerül minden buildnél lefuttatva
Az itt bemutatott determinisztikus azonosító API a losLab PDF Library részeként érhető el Delphihez és C++Builderhez, a teljes dokumentuminformációs, titkosítási és inkrementális mentési referenciával együtt