Η losLab PDF Library μπορεί να παράγει byte-identical έξοδο PDF για πανομοιότυπη είσοδο μόλις καλέσετε τη SetDeterministicDocumentID(1). Από προεπιλογή ο πίνακας trailer /ID είναι ένα MD5 digest του ρολογιού τοίχου, οπότε δύο εκτελέσεις της ίδιας γεννήτριας διαφέρουν τουλάχιστον σε αυτά τα bytes. Η ντετερμινιστική λειτουργία παράγει το /ID από έναν σταθερό seed αντ' αυτού, κάτι που αποκαθιστά τα reproducible builds
Το σύμπτωμα συνήθως εμφανίζεται στο CI πριν κανείς το ψάξει. Το template δεν έχει αλλάξει, η εγγραφή εισόδου δεν έχει αλλάξει, οι γραμματοσειρές δεν έχουν αλλάξει, και το παραγόμενο PDF εξακολουθεί να παράγει διαφορετικό hash σε κάθε εκτέλεση pipeline. Οι build caches δεν χτυπούν ποτέ. Η content addressable storage συσσωρεύει ένα φρέσκο blob ανά νυχτερινό build. Οι διαφορές byte-level regression ανάβουν σε αρχεία που κανείς δεν άγγιξε. Ακολουθήστε τη διαφορά μέχρι τα πραγματικά bytes και σχεδόν πάντα είναι το ίδιο μικρό σύνολο δεκαεξαδικών ψηφίων που κάθεται στο trailer του αρχείου
Σε τι χρησιμεύει ο πίνακας trailer ID
Το trailer /ID είναι δείκτης ταυτότητας αρχείου, όχι checksum του περιεχομένου. Το ISO 32000-1 §14.4 το ορίζει ως πίνακα δύο byte strings: το πρώτο στοιχείο είναι ο μόνιμος αναγνωριστικός κωδικός που ανατίθεται όταν δημιουργείται το έγγραφο και προορίζεται να επιβιώσει κάθε μεταγενέστερη επεξεργασία, και το δεύτερο στοιχείο είναι ο μεταβαλλόμενος αναγνωριστικός κωδικός που ένας writer ανανεώνει κάθε φορά που το αρχείο τροποποιείται. Μαζί επιτρέπουν σε ένα σύστημα να αποφασίσει αν δύο αρχεία είναι αναθεωρήσεις ενός εγγράφου ή δύο ασύνδετα έγγραφα. Το §7.5.5 καθιστά την καταχώριση ουσιαστικά υποχρεωτική στην πράξη, αφού ο trailer πρέπει να φέρει /ID όποτε φέρει και /Encrypt
Τίποτα στην προδιαγραφή δεν λέει πώς να υπολογιστεί η τιμή. Η σύσταση είναι ένα digest πραγμάτων όπως η τρέχουσα ώρα, η διαδρομή αρχείου, το μέγεθος αρχείου και το λεξικό πληροφοριών εγγράφου, και το ρολόι τοίχου είναι το συστατικό που κάνει το αποτέλεσμα μοναδικό. Αυτή είναι ακριβώς η ιδιότητα που θέλετε για ταυτότητα και ακριβώς η ιδιότητα που καταστρέφει την αναπαραγωγιμότητα, γι' αυτό αυτό πρέπει να είναι ρητός διακόπτης και όχι σιωπηλή αλλαγή συμπεριφοράς
Γιατί το ίδιο build παράγει διαφορετικό PDF κάθε φορά;
Επειδή το προεπιλεγμένο αναγνωριστικό παράγεται από τη στιγμή της δημιουργίας. Ιστορικά η losLab PDF Library έχτιζε τα strings /ID από ένα MD5 της τρέχουσας χρονοσφραγίδας, οπότε ένα έγγραφο που δημιουργείται δύο φορές με ένα δευτερόλεπτο διαφορά φέρει δύο διαφορετικούς μόνιμους αναγνωριστικούς κωδικούς ακόμη κι όταν κάθε άλλο byte στο αρχείο είναι πανομοιότυπο. Το κόστος downstream είναι πραγματικό: ένα σύστημα build που κλειδώνει artifacts με hash δεν μπορεί ποτέ να επαναχρησιμοποιήσει ένα βήμα PDF, ένα deduplicating object store κρατά ένα αντίγραφο ανά build αντί ένα αντίγραφο ανά έγγραφο, και ένας reviewer που κοιτάζει ένα binary diff πρέπει να αποδείξει ότι η μόνη αλλαγή είναι θόρυβος πριν εμπιστευτεί το υπόλοιπο diff. Η ντετερμινιστική παραγωγή /ID υπάρχει για να αφαιρέσει αυτόν τον θόρυβο, στο ίδιο πνεύμα με τη δουλειά σταθερότητας διάταξης που περιγράφεται στις σημειώσεις για τα object streams και cross reference streams
Μετάβαση σε αναπαραγόμενο αναγνωριστικό
Η ντετερμινιστική λειτουργία είναι opt-in, ανά έγγραφο, και απενεργοποιημένη από προεπιλογή ώστε η υπάρχουσα έξοδος να παραμένει αμετάβλητη μέχρι να τη ζητήσετε. Η SetDeterministicDocumentID δέχεται 0 ή 1 και επιστρέφει 1 όταν η τιμή έγινε αποδεκτή, 0 για οτιδήποτε εκτός εύρους· η GetDeterministicDocumentID αναφέρει την τρέχουσα κατάσταση. Η SetDocumentIDSeed παρέχει έναν ρητό seed string που υπερισχύει όλων· η μετάδοση κενού seed επαναφέρει τον παραγόμενο seed. Η GetDocumentFileID διαβάζει πίσω το /ID[0] μετά την αποθήκευση ώστε να μπορείτε να το καταγράψετε ή να το επιβεβαιώσετε
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;
Η ανανέωση γίνεται τη στιγμή της αποθήκευσης, όχι όταν αλλάζετε το flag, οπότε η ενεργοποίηση ντετερμινιστικής λειτουργίας αργά στη διάρκεια ενός build εγγράφου εξακολουθεί να ισχύει. Αυτό σημαίνει επίσης ότι ένας αλλαγμένος seed φτάνει στο αρχείο στην επόμενη πλήρη αποθήκευση: ορίστε seed A, αποθηκεύστε, ορίστε seed B, αποθηκεύστε, και τα δύο αρχεία φέρουν διαφορετικούς αναγνωριστικούς κωδικούς, ενώ η επαναφορά του seed A επαναφέρει την αρχική τιμή. Ένας ρητός seed είναι η σωστή επιλογή όποτε το έγγραφό σας έχει ένα φυσικό σταθερό κλειδί όπως αριθμός τιμολογίου, αναθεώρηση εγγραφής ή αναγνωριστικό git commit, επειδή αποσυνδέει το αναγνωριστικό από τυχαία metadata
Από πού προέρχεται ο seed όταν δεν παρέχετε έναν;
Χωρίς ρητό seed, η losLab PDF Library παράγει έναν από την κατάσταση εγγράφου που θα έπρεπε να είναι αμετάβλητη σε πανομοιότυπες αναδημιουργίες: την κεφαλίδα έκδοσης PDF, τον αριθμό σελίδων, και κάθε καταχώριση στο λεξικό πληροφοριών εγγράφου. Οι τιμές string και name λαμβάνονται αυτούσιες, άλλοι τύποι αντικειμένων συνεισφέρουν τη σειριοποιημένη μορφή τους, και όλο αυτό γίνεται hash στα strings /ID. Η σημαντική συνέπεια είναι ότι τα CreationDate και ModDate είναι μέρος του λεξικού πληροφοριών και επομένως μέρος του seed εξ ορισμού. Δύο εκτελέσεις κερδίζουν το ίδιο αναγνωριστικό μόνο όταν γνησίως παράγουν τα ίδια metadata εγγράφου
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');
Η καθήλωση του ModDate με το κλειδί 8 κάνει διπλή δουλειά, και αυτό είναι το σημείο που πιάνει τον κόσμο απροετοίμαστο. Ένα ντετερμινιστικό /ID από μόνο του δεν κάνει το αρχείο byte-identical, επειδή η διαδρομή αποθήκευσης σφραγίζει το ModDate με την τρέχουσα ώρα εκτός αν ο καλών το έχει ορίσει ρητά. Ο ορισμός του κλειδιού 8 σημαδεύει την τιμή ως παρεχόμενη από τον καλούντα και καταστέλλει αυτή τη σφραγίδα. Αν θέλετε ένα αναπαραγόμενο αρχείο και όχι απλώς ένα αναπαραγόμενο αναγνωριστικό, αντιμετωπίστε τις χρονοσφραγίδες metadata ως εισόδους build: παράγετέ τις από την εγγραφή πηγής ή από μια σταθερή εποχή, ποτέ από το Now
Γιατί η επανεγγραφή του ID σπάει ένα κρυπτογραφημένο PDF;
Επειδή το /ID[0] δεν είναι απλώς metadata σε ένα κρυπτογραφημένο έγγραφο, είναι υλικό κλειδιού. Το ISO 32000-1 §7.6.3.3 Algorithm 2 τροφοδοτεί το πρώτο στοιχείο του αναγνωριστικού αρχείου στον υπολογισμό κλειδιού κρυπτογράφησης για τον τυπικό security handler στις αναθεωρήσεις 2 έως 4, μαζί με τον padded κωδικό πρόσβασης, την τιμή /O και τα bits άδειας. Το παραγόμενο κλειδί μετά παράγει το validation string /U που ελέγχει ένας reader κατά το άνοιγμα, και το κλειδί αρχείου παράγεται και αποθηκεύεται στην cache όταν καλείτε την Encrypt ή όταν φορτώνεται ένα κρυπτογραφημένο έγγραφο, και τα δύο συμβαίνουν πριν την αποθήκευση. Η επανεγγραφή του αναγνωριστικού κατά την αποθήκευση θα παρήγαγε επομένως ένα δομικά έγκυρο αρχείο του οποίου ο έλεγχος /U αποτυγχάνει στο επανάνοιγμα: όχι μια λεπτή διαφθορά αλλά ένα έγγραφο που κανείς δεν μπορεί να ανοίξει, ούτε καν εσείς. Γι' αυτό η ντετερμινιστική ανανέωση περιορίζεται σε έγγραφα που δεν φέρουν κατάσταση κρυπτογράφησης, και γιατί ένα κρυπτογραφημένο έγγραφο κρατά όποιο /ID είχε ήδη, ντετερμινιστική λειτουργία ή όχι, και η ρύθμιση απλώς δεν έχει καμία επίδραση σε αυτή τη διαδρομή. Ο σχετικός χειρισμός αναθεωρήσεων και η σημασιολογία αδειών καλύπτονται στην περιγραφή του ελέγχου κρυπτογράφησης και αδειών PDF. Σημειώστε επίσης ότι η διαδρομή επαναφοράς κρυπτογράφησης ανανεώνει μόνο το /ID[1], τον αναγνωριστικό κωδικό αλλαγής, ακριβώς όπως προτίθεται το §14.4
Γιατί οι αυξητικές αποθηκεύσεις κρατούν το αρχικό αναγνωριστικό
Το δεύτερο όριο είναι η λειτουργία προσάρτησης. Μια αυξητική ενημέρωση αφήνει κάθε προηγούμενο byte του αρχείου ανέγγιχτο και γράφει μια νέα αναθεώρηση μετά από αυτό, και η μονιμότητα του /ID[0] στο §14.4 είναι αυτό που λέει σε έναν καταναλωτή ότι η νέα αναθεώρηση ανήκει στο ίδιο έγγραφο με την παλιά. Η επανεγγραφή του θα έκοβε αυτόν τον σύνδεσμο, θα αντέφασκε με τις αναθεωρήσεις που βρίσκονται ήδη στο αρχείο, και θα παρενέβαινε στη σημασιολογία υπογραφών, αφού μια υπογραφή καλύπτει ένα εύρος byte μιας συγκεκριμένης αναθεώρησης ενός συγκεκριμένου εγγράφου. Η losLab PDF Library επομένως ανανεώνει το ντετερμινιστικό αναγνωριστικό μόνο σε πλήρεις αποθηκεύσεις και ποτέ κατά τη λειτουργία προσάρτησης, κάτι που διατηρεί άθικτη την εγγύηση που περιγράφεται στο άρθρο για τις αυξητικές ενημερώσεις PDF και προσάρτηση σε stream
Ένα σημείο συμφόρησης για την παραγωγή αναγνωριστικού
Όλη η παραγωγή /ID στη losLab PDF Library τώρα διοχετεύεται μέσα από μία μόνο εσωτερική ρουτίνα, την NewFileIDString, κάτι που κάνει τον ντετερμινιστικό διακόπτη αξιόπιστο αντί για επιδιόρθωση σε μία διαδρομή κώδικα. Η δημιουργία κενού εγγράφου, η οκνηρή δημιουργία ενός λείποντος πίνακα /ID κατά παραγγελία, και η διαδρομή επαναφοράς αποτυπώματος κρυπτογράφησης όλα την καλούν, οπότε υπάρχει ακριβώς ένα σημείο όπου το ρολόι τοίχου θα μπορούσε να διαρρεύσει πίσω μέσα. Σημαίνει επίσης ότι μελλοντικές παραλλαγές, όπως ένα αναγνωριστικό παραγόμενο από περιεχόμενο, είναι αλλαγή σε μία συνάρτηση αντί για έλεγχο ολόκληρου του serializer
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');
Συνδέστε αυτή τη σύγκριση στη σουίτα δοκιμών σας πριν βασιστείτε στην αναπαραγόμενη έξοδο οπουδήποτε αλλού, επειδή αποτυγχάνει με θόρυβο τη στιγμή που κάποιο νέο χαρακτηριστικό επανεισάγει μια χρονοσφραγίδα. Η αναπαραγωγιμότητα είναι ιδιότητα που φθίνει σιωπηλά διαφορετικά, και μια μοναδική επιβεβαίωση πάνω σε δύο αποθηκεύσεις στη μνήμη κοστίζει σχεδόν τίποτα να εκτελεστεί σε κάθε build
Το API ντετερμινιστικού αναγνωριστικού που παρουσιάζεται εδώ διατίθεται με την losLab PDF Library για Delphi και C++Builder, μαζί με την πλήρη αναφορά πληροφοριών εγγράφου, κρυπτογράφησης και αυξητικής αποθήκευσης