Salta ai contenuti
getnextpdf.com

Gli stessi byte ogni volta: PDF riproducibili

Spec: ISO 32000-2, §14.4Spec: ISO 32000-2, §14.3.3

Si costruisca un PDF dagli stessi input due volte e ci si aspetterebbe lo stesso file. La maggior parte delle librerie PDF non può prometterlo — si ricostruisca e si confronti con diff, e i byte derivano. NextPDF può fissare le due cose che si muovono, così che gli stessi input producano gli stessi byte, ogni volta.

L’output byte-identico non è una metrica di vanità. È la fondazione sotto tre cose che i team vogliono davvero.

La prima è il caching. Se una build è una funzione pura dei suoi input, l’hash del suo output è una chiave di cache. Stessi input, stesso hash, si salta il lavoro e si serve il file memorizzato. Quando i byte vagano, l’hash vaga, e la cache non fa mai centro.

La seconda è l’evidenza di manomissione. Una pipeline che può rigenerare il file esatto che ha spedito può provare, in seguito, che un documento archiviato non è stato alterato: lo si ricostruisce, si fa l’hash di entrambi, si confronta. Se anche un solo byte differisce a causa di un orologio incorporato, la prova è sparita e si è di nuovo a «fidati di me».

La terza è la CI affidabile. Un test golden-file registra un output noto-buono e fallisce quando una modifica lo altera. Quel segnale è significativo solo se un motore invariato riproduce un file invariato. Se ogni esecuzione differisce su un timestamp, il file golden è rumore, e il team impara a ignorare una build rossa — l’abitudine più costosa nel testing.

Nel profilo deterministico di NextPDF, i due campi controllati dal motore che altrimenti deriverebbero tra build identiche sono le date e l’/ID. Questo presuppone che il resto della pipeline sia già stabile — gli stessi input, e una serializzazione che non varia da sola (più su questo qui sotto):

  • Date incorporate. Il document information dictionary porta CreationDate e ModDate (Spec: ISO 32000-2, §14.3.3), e i metadati XMP le rispecchiano. Si catturi «ora» al momento della build e ogni ricostruzione differisce.
  • L’identificatore del file. L’array /ID è una coppia di byte string che identificano il file (Spec: ISO 32000-2, §14.4), memorizzata nel trailer dictionary (Spec: ISO 32000-2, §7.5.5). Le librerie di solito lo derivano dall’ora corrente più byte casuali, quindi è diverso a ogni esecuzione per progetto.

Si fissino entrambi — un timestamp fisso e un seed fisso per l’/ID — e l’output diventa una funzione deterministica del suo contenuto. Si lasci stare il contenuto e il file è byte-per-byte identico. Questa è la stessa disciplina che il progetto Reproducible Builds ha stabilito per il software compilato, applicata allo strato del documento.

Il determinismo in NextPDF è un oggetto di configurazione, non un trucco di test. Il motore espone un value object DeterministicSettings nel namespace NextPDF\Core. È final readonly, immutabile, e fissa esattamente le due fonti di deriva derivate dall’orologio e dal caso nominate sopra: le date e l’/ID. Fissarle rimuove le due fonti di deriva più comuni, ma di per sé non garantisce un output byte-identico. L’altro comportamento di serializzazione del motore — l’ordinamento degli oggetti, il subsetting dei font e le impostazioni di compressione — deve essere anch’esso deterministico affinché l’output si riproduca, e NextPDF li mantiene stabili per progetto.

Il suo costruttore prende due argomenti:

public function __construct(
public DateTimeImmutable $timestamp,
public string $fileIdSeed,
) {
// ...
}

$timestamp è il singolo istante fisso scritto in ogni campo di data — CreationDate, ModDate, e il loro riflesso XMP. Si passi un solo DateTimeImmutable e il documento smette di chiedere all’orologio da parete che ora è. $fileIdSeed è l’input che fissa l’/ID del trailer: una stringa esadecimale di 32 caratteri. Si dia lo stesso seed e il motore deriva lo stesso identificatore di file invece di campionare l’orologio e una fonte casuale.

L’oggetto valida il proprio input. Il seed deve essere esattamente 32 caratteri esadecimali; qualsiasi altra cosa è rifiutata alla costruzione con una InvalidConfigException anziché produrre silenziosamente un /ID dall’aspetto diverso. Questa è la stessa postura rifiuta-di-indovinare che adotta il resto del motore — un input ambiguo fallisce rumorosamente invece di cambiare in silenzio i byte.

Con entrambi fissati, la ricetta è quella che il progetto Reproducible Builds ha reso familiare: lo si ricostruisce, lo si confronta con diff, e il diff è vuoto.

  1. Fix the inputsThe same content, fonts, and settings that produced the original document.
  2. Pin the timestampOne DateTimeImmutable feeds CreationDate, ModDate, and the XMP dates — no wall clock.
  3. Pin the /ID seedA 32-character hex seed derives the trailer /ID instead of a clock-plus-random value.
  4. BuildThe output is now a pure function of content; the two moving parts are held still.
  5. Rebuild and diffRegenerate from the same inputs and compare bytes — an empty diff is the proof.
Reproducible build: identical inputs plus a pinned timestamp and a pinned /ID seed produce the same bytes, which a rebuild-and-diff step confirms.

Una forma piccola e completa. Le impostazioni sono costruite una volta e riutilizzate, così che due esecuzioni dello stesso programma emettano lo stesso file.

<?php
declare(strict_types=1);
use NextPDF\Core\DeterministicSettings;
use NextPDF\Exception\InvalidConfigException;
// One fixed instant for every date field — never the wall clock.
$timestamp = new DateTimeImmutable('2026-01-01T00:00:00+00:00');
// A 32-character hex seed pins the trailer /ID. Same seed, same /ID.
$fileIdSeed = '0123456789abcdef0123456789abcdef';
try {
$deterministic = new DeterministicSettings(
timestamp: $timestamp,
fileIdSeed: $fileIdSeed,
);
} catch (InvalidConfigException $e) {
// A malformed seed (not exactly 32 hex chars) is refused here,
// before any document is built — not silently coerced.
error_log($e->getMessage());
throw $e;
}
// Hand $deterministic to the document configuration. With both moving
// parts pinned, building the same content twice yields identical bytes:
//
// sha256(build_one) === sha256(build_two)

Il seed è un input di build che si controlla, non un segreto. Lo si memorizzi accanto al resto della configurazione di build. Il punto è che è fisso, così che l’identificatore di file che produce sia anch’esso fisso.

La prima trappola è «Ho rimosso il timestamp, quindi la mia build è ora riproducibile». Di solito non lo è, perché l’array /ID è la più silenziosa delle due fonti. Le date sono visibili in un pannello di metadati e facili da ricordare; l’/ID del trailer è invisibile alla maggior parte dei lettori ed è rigenerato dall’orologio e da una fonte casuale a ogni esecuzione. Una build che fissa solo le date produce comunque un file diverso ogni volta. Si devono tenere fermi entrambi.

La seconda trappola è trattare il determinismo come una funzionalità di sicurezza di per sé. Un /ID fissato rende un file riproducibile; non lo rende firmato, e di per sé non prova che due build coincidano. Un confronto byte-per-byte o un hash prova che le build coincidono; fissare l’/ID rimuove solo una fonte di differenza spuria. E nessuno dei due prova che una terza parte garantisca per il file. Riproducibilità e firma sono strati complementari, non sostituti.

Il determinismo fissa le parti mobili del motore stesso. Non fissa i tuoi input. Se il tuo contenuto incorpora un timestamp vivo, attinge a un font che è cambiato su disco, o renderizza un valore che dipende dalla data corrente, l’output cambia perché l’input è cambiato — e questo è corretto. DeterministicSettings rimuove il non-determinismo del motore, non il tuo. Una build riproducibile richiede comunque input riproducibili.

Deterministic byte-identical output — edition availability
EditionAvailability
Core

Pieno supporto. DeterministicSettings è incluso nel core open-source: si fissano il timestamp e il seed dell’/ID e lo stesso contenuto si ricostruisce negli stessi byte — nessun gate di edizione.

ProNot in this edition
EnterpriseNot in this edition
  • Test golden-file — la tecnica di CI che dipende dall’output byte-identico, e perché un motore deterministico è la sua precondizione.
  • Aggiornamenti incrementali — come un PDF cresce per accodamento, dove l’array /ID conta di nuovo per relazionare un file alle sue versioni precedenti.
  • Metadati e pacchetto XMP — dove vivono le date incorporate, e come il pacchetto XMP rispecchia il document information dictionary.
  • L’anatomia di un file PDF — il trailer, la cross-reference table, e dove si trova l’array /ID nella struttura del file.
  • Byte-identico — due file che coincidono esattamente, byte per byte. La forma più forte di «lo stesso», e quella che un hash o un diff può verificare.
  • /ID (identificatore di file) — l’array di due byte string che identifica un PDF e le sue versioni (ISO 32000-2 §14.4), memorizzato nel trailer dictionary (§7.5.5). Di solito derivato dall’orologio più byte casuali, motivo per cui cambia a ogni build non fissata.
  • Document information dictionary — la struttura che porta CreationDate e ModDate (ISO 32000-2 §14.3.3). Una delle due fonti di non-determinismo che una build deterministica deve fissare.
  • File golden — un output noto-buono registrato con cui un test si confronta; significativo solo quando un motore invariato riproduce un file invariato.
  • Build riproducibile — una build il cui output è una funzione deterministica dei suoi input, così che ricostruire dagli stessi input produca gli stessi byte. Il termine viene dal progetto Reproducible Builds per il software compilato.