Technisch artikel

Waarom één Single AcroForm field flags in Delphi corrumpeert

HotPDF Delphi Component bewaart elk PDF-nummer in THPDFNumericObject.Value, dat als Single is gedeclareerd, en een Single kan niet elk 32-bit integer bevatten. AcroForm field flags (/Ff) zijn een 32-bit mask, dus elk mask met een hoge bit werd bij het laden afgerond: $8000FFFF kwam terug als $80010000. Sinds v2.989.5 parsen field flag-lezingen het originele integer-token exact en kopiëren zijn bits naar de bestaande Integer-API

De bug verdient een goede blik, ook als u PDF-formulieren nooit aanraakt. Er crashte niets, er werd geen exception gegooid, en de meeste testbestanden slaagden, want de meeste echte masks zijn klein. De misser verscheen alleen wanneer een hoge bit en de lage algemene flags samen voorkwamen, en toen verloor hij niet één bit, hij herschreef er meerdere tegelijk. Een bit mask in een floating-point type opslaan is een klassieke Delphi-valkuil, en dit is hoe die er in productiecode uitziet

Waarom verliest een Single bits van AcroForm field flags?

Een Delphi-Single heeft een significand van 24 bits, dus hij stelt elk integer tot 2^24 (16777216) exact voor en daarbuiten niets zonder gaten. Tussen 2^31 en 2^32 liggen opeenvolgende Single-waarden 256 uit elkaar. Een field flag-mask met bit 32 staat precies in dat bereik, dus zijn lage acht bits, en vaak meer, worden afgerond naar het dichtstbijzijnde veelvoud van 256

Afrondingsdiagram van de HotPDF AcroForm field flags voor Delphi: het 32-bit /Ff-mask $8000FFFF, 2147549183, gaat door een Single met een 24-bit significand en komt terug als $80010000, omdat voorstelbare Single-waarden tussen 2^31 en 2^32 256 uit elkaar liggen
Boven 2^31 kan een Delphi-Single alleen veelvouden van 256 opslaan, dus round-to-nearest herschrijft de lage byte van een 32-bit field flag-mask zonder enige exception

ISO 32000-1 §7.3.3 definieert integer- en real-objecten als aparte soorten getallen, en Annex C zet voor elk andere implementatielimieten op een rij, dus een PDF-integer is niet verplicht in een 32-bit float te passen. De field flags-entry is in Table 221 als integer gedefinieerd, en hij wordt bit voor bit gelezen, niet als een hoeveelheid. Een hoeveelheid met een piepkleine fractie afronden is onschuldig; een mask afronden loopt door in naburige bits. Het hele effect kunt u in een paar regels kale Delphi reproduceren:

var
  S: Single;
  Mask: Int64;
begin
  S := $8000FFFF;              // 2147549183, niet als Single voorstelbaar
  Mask := Round(S);            // 2147549184
  Writeln(IntToHex(Mask, 8));  // 80010000: bits 1-16 gewist, bit 17 gezet

  S := $80003FFF;
  Mask := Round(S);
  Writeln(IntToHex(Mask, 8));  // 80004000: bits 1-14 gewist, bit 15 gezet
end;

De les generaliseert voorbij PDF. Een Single, een Double, een Variant met een float of een JSON-nummer gedecodeerd in een float is de verkeerde container voor een bitveld, ook wanneer de waarden waar u toevallig mee test klein zijn. Double schuift het exacte bereik op tot 2^53, wat het probleem verbergt voor 32-bit masks, maar het type blijft de verkeerde intentie uitdrukken. Gebruik Cardinal, Integer of Int64 en converteer vanuit de brontekst, niet vanuit een float die al is afgerond

Welke field flags kantelen: Radio, Pushbutton, Password en Multiline

De twee masks hierboven zijn niet willekeurig; ze laten zien hoe één afrondingsstap het veldtype verandert dat een reader ziet. PDF nummert flag bits vanaf 1, dus bit n heeft de waarde 2^(n-1). ISO 32000-1 §12.7.3.1 Table 226 definieert bits 1 tot 3 voor elk veld, Table 228 definieert de button flags, en Table 230 definieert de text field flags

  • $8000FFFF naar $80010000: bit 16 (Radio, $8000, Table 228) wordt gewist en bit 17 (Pushbutton, $10000) wordt gezet, dus een radioknop wordt een drukknop
  • Dezelfde afronding wist bits 1 tot 3, dus ReadOnly, Required en NoExport uit Table 226 verdwijnen samen met Radio
  • $80003FFF naar $80004000: bit 13 (Multiline, $1000) en bit 14 (Password, $2000) uit Table 230 worden beide gewist, dus een gemaskeerd wachtwoordveld wordt een single-line plat tekstveld
  • Onder bit 32 begint de schade bij 2^24: 16777219 (Comb plus ReadOnly en Required) rondt af naar 16777220, wat ReadOnly en Required laat vallen en NoExport toevoegt

Het laatste geval telde voor HotPDF buiten formulierbewerking om. ExportLoadedPagesStructured bouwt zijn JSON- en XML-formuliermodel vanuit het effectieve mask en gebruikt NoExport in zijn beslissing om waarden te onderdrukken, dus een afgerond mask kon een gewone waarde verbergen of de verkeerde readOnly- en required-members melden. Het gestructureerde exportpad kreeg dezelfde exacte lezing in v2.989.8

Bitkaart van de HotPDF AcroForm field flags voor Delphi: het /Ff-mask $8000FFFF met bit 32 en bits 1 tot 16 gezet verliest bits 1 tot 16 in één Single-afronding en wint Pushbutton bit 17, en wordt $80010000
De flags uit ISO 32000-1 Tables 226, 228 en 230 bewegen als groep: Radio kantelt naar Pushbutton terwijl ReadOnly, Required, NoExport, Multiline en Password in dezelfde afrondingsstap kunnen verdwijnen

Hoe HotPDF het exacte 32-bit integer-token leest

HotPDF Delphi Component repareert de leeszijde zonder het numerieke objectmodel te veranderen. De parser onthoudt de originele tokentekst al naast de Single-waarde, dus de field flag-lezer parst dat token als integer, telt het op in een begrensde Int64 en kopieert de resulterende 32 bits naar de Integer die de publieke API teruggeeft. Generieke numerieke API's en de oude non-integer-fallback blijven zoals ze waren, en alleen field flag-consumers nemen het exacte pad

Exacte leesweg voor AcroForm field flags in HotPDF in Delphi: de parser houdt het brontoken naast de Single-waarde, TryParseFlagMask telt het op in een begrensde Int64, en de 32 bits komen als Integer aan bij GetLoadedFormFieldFlags en SetLoadedFormFieldFlags
Field flags parsen de onthouden tokentekst in plaats van de afgeronde Single, dus beide spellingen van een mask met hoge bit, 2147549183 en -2147418113, komen met identieke bits binnen

De parser moet beide manieren accepteren waarop echte bestanden een mask met hoge bit spellen. Sommige writers zenden de unsigned waarde 2147549183 uit, die boven de Annex C-integerlimiet van 2^31 - 1 ligt maar in de praktijk veel voorkomt; anderen zenden de signed waarde -2147418113 uit. Beide beschrijven dezelfde bits. Een versie van de begrensde parser ziet er zo uit:

function TryParseFlagMask(const Token: AnsiString; out Mask: Cardinal): Boolean;
var
  P, Digit: Integer;
  Negative: Boolean;
  Number, Limit: Int64;
begin
  Result := False;
  Mask := 0;
  if Token = '' then Exit;
  P := 1;
  Negative := Token[1] = '-';
  if Token[1] in ['+', '-'] then Inc(P);
  if P > Length(Token) then Exit;
  // Signed tokens tot -2^31, unsigned tokens tot 2^32 - 1
  if Negative then Limit := 2147483648 else Limit := 4294967295;
  Number := 0;
  while P <= Length(Token) do
  begin
    if not (Token[P] in ['0'..'9']) then Exit;  // reals nemen de oude fallback
    Digit := Ord(Token[P]) - Ord('0');
    if Number > (Limit - Digit) div 10 then Exit;  // begrensd, geen overflow
    Number := Number * 10 + Digit;
    Inc(P);
  end;
  if Negative and (Number <> 0) then Number := 4294967296 - Number;
  Mask := Cardinal(Number);
  Result := True;
end;

Twee details zijn met opzet zo. De grens wordt vóór elke vermenigvuldiging gecontroleerd, dus een token van 400 cijfers faalt netjes in plaats van Int64 te laten overlopen. En een token zoals 3.0 wordt geweigerd, wat de aanroeper terugstuurt naar Round(Value); het exacte pad verbetert integer-masks zonder nieuwe semantiek te verzinnen voor misvormde

Vanuit applicatiecode verandert er niets aan de signaturen. THotPDF.GetLoadedFormFieldFlags(FieldIndex: Integer): Integer geeft het effectieve mask terug, langs de /Parent-keten lopend op dezelfde manier als beschreven in hoe HotPDF overgeërfde AcroForm-veldwaarden resolvt en reset, en THotPDF.SetLoadedFormFieldFlags(FieldIndex, AFlags: Integer) schrijft een compleet lokaal mask. Een mask met bit 32 gezet is simpelweg een negatieve Integer, dus verplaats de bits in plaats van de waarde te converteren:

const
  FF_RADIO      = $00008000;  // bit 16, ISO 32000-1 Table 228
  FF_PUSHBUTTON = $00010000;  // bit 17, Table 228
var
  Pdf: THotPDF;
  I, Native: Integer;
  Bits: Cardinal;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
    for I := 0 to Pdf.GetFormFieldCount - 1 do
    begin
      Native := Pdf.GetLoadedFormFieldFlags(I);
      Move(Native, Bits, SizeOf(Bits));   // dezelfde 32 bits, geen conversie
      if (Bits and FF_RADIO) <> 0 then
        Writeln(Pdf.GetFormFieldName(I), ': radio, mask $', IntToHex(Bits, 8));
    end;

    Bits := $80000002;                    // hoge bit plus Required
    Move(Bits, Native, SizeOf(Native));   // Native = -2147483646
    Pdf.SetLoadedFormFieldFlags(0, Native);
    Pdf.SaveLoadedDocument('survey-out.pdf');
  finally
    Pdf.Free;
  end;
end;

Een overgeërfd mask lezen laat de childdictionary onaangeroerd; schrijven vervangt het hele mask door een lokale entry op het geselecteerde veld. De shortcuts SetLoadedFormFieldReadOnly en SetLoadedFormFieldRequired vertrekken vanuit het effectieve overgeërfde mask, kippen één bit en houden elke andere bit exact. Voor het vullen van waarden in plaats van flags, zie formulierveldwaarden zetten in een geladen PDF met HotPDF

Waarom maakt de volgorde van Value en RememberSourceToken uit?

De volgorde telt omdat THPDFNumericObject zijn onthouden token alleen vertrouwt zolang de Single-waarde onveranderd is. RememberSourceToken bewaart het token samen met een snapshot van de huidige Value, en de getter SourceToken geeft een lege string terug zodra Value van die snapshot afwijkt. Die invalidatieregel zorgt ervoor dat een bewerkt nummer niet met zijn verouderde originele tekst wordt opgeslagen

Op de schrijfkant wordt de regel een valkuil. Onthoud eerst het token en wijs daarna de waarde toe, dan bevat de snapshot de oude waarde, meldt de getter geen token en valt de serializer terug op het formatteren van de afgeronde Single. Elke flag writer in de library, inclusief de shortcuts en de native field constructors, volgt dezelfde volgorde:

// Goed: de snapshot matcht Value, dus het exacte token overleeft het opslaan
Item := THPDFNumericObject.Create(nil);
Item.Value := AFlags;
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));

// Fout: de snapshot wordt genomen voordat Value verandert, dus SourceToken
// vervalt en in plaats daarvan wordt de afgeronde Single weggeschreven
Item := THPDFNumericObject.Create(nil);
Item.RememberSourceToken(AnsiString(IntToStr(AFlags)));
Item.Value := AFlags;

Omdat SaveNumericObject het onthouden token wegschrijft wanneer er een geldig is, zit het juist zetten van de volgorde de sleutel tot een opgeslagen en heropend bestand dat het originele integer draagt, geen afgeronde benadering

Het null-token dat twee velden één naam gaf

Dezelfde release repareerde een tweede parserdefect gevonden tijdens het testen van deze masks: een null-waarde in een dictionary die uit de stream werd gelezen werd nooit geconsumeerd. HotPDF kijkt het volgende token in om het te classificeren, en die check laat de streampositie aan het begin van het token staan. De otNull-tak van de streaming dictionary reader maakte het null-object aan maar liep niet voorbij de vier bytes van null

Het gevolg was geen hang maar een stille mislezing. De volgende sleutelopzoeking scande vooruit vanaf de verkeerde plek, liep voorbij het einde van de huidige dictionary en las sleutels uit het volgende indirecte object, dat daarna het huidige veld overschreef. In de veldlijst was het symptoom twee verschillende velden die dezelfde naam en dezelfde waarde meldden. De array- en span-parsers consumeerden null al correct, dus de fix laat de stream in die ene tak vier bytes opschuiven in plaats van de algemene parser te herschrijven. Een cursor die niet beweegt is dezelfde familie bug als de PDF-parser-cursorstalls die HotPDF lieten hangen op misvormde invoer, met een ander symptoom

De fix verduidelijkte ook de overerving. Een lokale /Ff null, of een verwijzing die naar null resolvt, wordt als afwezig behandeld en de opzoeking gaat door naar de ouder, terwijl een expliciete 0 een echt mask is dat de ouder overstijgt

Wat een {$R-}-directive bewijst en wat niet

De eerste hypothese voor de flag-bug was een range-check-fout wanneer een groot mask aan Integer werd toegekend. Die klopte niet, en de reden is een Delphi-detail die het kennen waard is: een lokale {$R-}-directive geplaatst vóór ParseSimpleDictionary in HPDFDoc.pas had geen matchende restore, dus elke functie erna in de unit compileerde met range checks uit. Delphi reset een switch-directive niet aan het eind van een functie, en een include-file erft de directive-status op het punt waar hij wordt ingevoegd

Een command-line -$R+ of -Cr bewijst dus alleen de beginsituatie van de unit. Over een specifieke functie verder naar beneden zegt hij niets. Om te weten of range checking voor een gegeven routine actief is, zoekt u de dichtstbijzijnde directive erboven en bevestigt u het gedrag met een echte grenswaarde. De twee oudere lokale flag-consumers gooiden nooit de range exception die de hypothese voorspelde, dus die afwezige exception kon niet als bewijs van een fix worden aangehaald. Het echte defect was de Single-afronding over bekende flag bits

De exacte tokenlezing is ook eerlijk over zijn reikwijdte. Hij verandert de unit-brede directive-status niet, kent aan gereserveerde bits geen betekenis toe en beweert niet dat elke bitcombinatie een geldig veld is. Hij garandeert dat de 32 bits in het bestand de 32 bits zijn die uw code ziet

FAQ

Waarom THPDFNumericObject.Value niet veranderen in Double of Int64?

Het type van een publiek veld veranderen zou elke aanroeper breken die het leest of eraan toekent, en PDF-nummers zijn net zo vaak real als integer. Single houden en field flags uit het originele token lezen repareert de bug voor bit masks zonder een bestaande declaratie of calling convention te veranderen

Accepteert SetLoadedFormFieldFlags masks met bit 32 gezet?

Ja. Geef het mask mee als signed Integer; $80000002 en -2147483646 zijn dezelfde 32 bits. GetLoadedFormFieldFlags geeft na opslaan en herladen dezelfde waarde terug, Required inbegrepen

Welke HotPDF-versie leest exacte AcroForm field flags?

Exacte field flag-lezingen en de null-token-fix verschenen in HotPDF v2.989.5, en gestructureerde JSON- en XML-export nam het exacte mask over in v2.989.8. Eerdere versies ronden elk mask boven 2^24 af via Single

Exacte AcroForm field flags, overgeërfde veldopzoeking en bewerking van geladen formulieren maken deel uit van de HotPDF Component for Delphi and C++Builder, een native PDF-library voor het maken, laden en bewerken van PDF-documenten