Bewerk een QDF-bestand met de hand, smokkel er een tweede streammarker voor hetzelfde object in, en draai daarna een in-place reparatie met de meest natuurlijke Delphi-code die u kunt schrijven. De reparatiestap merkt de beschadiging op en weigert. Als u daarna naar het bestand kijkt, is hij 0 bytes groot, want TFileStream.Create(OutputFileName, fmCreate) kapte precies dat pad af, één regel voordat de validator de invoer ooit zag. Bytes in het geheugen repareren is de makkelijke helft van het werk. Ze weer op schijf krijgen zonder ooit een bestand van nul lengte, half geschreven of met verkeerde rechten achter te laten, is de andere helft, en die heeft eigen faalmodi
Waarom leegt een in-place reparatie het bestand?
Een in-place reparatie leegt het bestand omdat de volgorde van de bewerkingen fout is: fmCreate kapt af bij het openen, dus elke weigering die ná het openen komt, komt te laat. Zijn InputFileName en OutputFileName hetzelfde pad, dan wordt een geweigerde invoer een verloren document. Een zorgvuldige parser kan u hier niet redden. Hij mag zijn uitvoerstream met rust laten wanneer hij dubbelzinnige markers weigert, maar de aanroeper heeft het doel al één aanroep eerder vernietigd
// De voor de hand liggende versie: de bestemming wordt afgekapt voordat er iets is gevalideerd
Input := TMemoryStream.Create;
try
Input.LoadFromFile(InputFileName); // zelfde pad als OutputFileName
Output := TFileStream.Create(OutputFileName, fmCreate); // het bestand is nu 0 bytes
try
if not MyRepairQDF(Input, Output) then // uw eigen validator: te laat om nee te zeggen
raise Exception.Create('QDF structure rejected');
finally
Output.Free;
end;
finally
Input.Free;
end;
De gebruikelijke volgende poging is repareren naar een TMemoryStream en de uitvoer pas openen nadat de reparatie is geslaagd. Dat dicht het gat van slechte invoer en niets anders. De schrijffase is nog steeds afkappen-daarna-kopiëren, dus een volle schijf, een sharing violation halverwege, of een exception tussen het afkappen en de laatste WriteBuffer laat de bestemming nog steeds beschadigd achter. Parsen en publiceren zijn twee aparte grenzen, en elk heeft zijn eigen bescherming nodig
In PDF Library for Delphi dekt TPDFlib.RepairQDFFile(InputFileName, OutputFileName) beide af. Hij leest de invoer, repareert volledig in het geheugen, en keert vroeg terug zonder ooit iets op de bestemming te openen wanneer de QDF-structuur wordt geweigerd. Alleen een geslaagde reparatie gaat door naar publicatie, en publicatie opent de bestemming helemaal niet voor schrijven
Vier stappen tussen de gerepareerde bytes en de bestemming
RepairQDFFile garandeert dat het bestemmingspad ofwel het volledige oude bestand ofwel het volledige nieuwe bestand bevat, voor elke storing die het proces zelf kan waarnemen. Hij komt daar in vier stappen, en geen enkele begint voordat de vorige is afgerond. De bestemming wordt met GetFullPathNameW naar een volledig pad opgelost, één keer aangeroepen om de vereiste lengte te leren en nog een keer in een buffer van precies die grootte, zodat een lang pad niet bij MAX_PATH wordt afgekapt. Daarna wordt een tijdelijk bestand genaamd .pdflib-qdf- plus een GUID plus .tmp in de bestemmingsmap aangemaakt met CreateFileW en CREATE_NEW op Windows, of open(2) met O_CREAT or O_EXCL en mode 0600 op POSIX. Beide flags laten het aanmaken mislukken als de naam al bestaat. Twee processen die toevallig dezelfde GUID trekken, kunnen dus niet op één handle uitkomen
De gerepareerde stream wordt daarna in blokken van 64 KiB via WriteBuffer gekopieerd, dat bij een korte write een fout gooit in plaats van een aantal terug te geven dat niemand controleert. FlushFileBuffers of fsync(2) duwt de data naar beneden, en de handle wordt gesloten. De close gooit bewust geen fout: zodra de flush is geslaagd, is een mislukte close geen bedreiging voor de data, en een exception daar zou de echte write- of flushfout verbergen voor wie hem moet diagnosticeren. De laatste stap is de rename
De rename is waar de meeste zelfgebouwde veilig-opslag-routines stukgaan. Op Windows vervangt het tijdelijke bestand het doel via MoveFileExW met MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH, wat op één volume een enkele bestandssysteemoperatie is. MOVEFILE_COPY_ALLOWED wordt bewust weggelaten. Een verplaatsing over volumes ontaardt stilletjes in kopiëren-daarna-verwijderen, precies de niet-atomaire reeks die het hele ontwerp wil vermijden, en ik zie liever een geweigerde rename dan een "geslaagde" kopie die half af was toen de machine uitviel. Omdat het tijdelijke bestand in de bestemmingsmap staat, zit het toch al op het bestemmingsvolume. Het oude bestand wordt ook nooit eerst verwijderd; een paar van verwijderen-daarna-hernoemen heeft een venster waarin het pad niet bestaat, en een crash daarin kost het document. MOVEFILE_WRITE_THROUGH houdt de aanroep tegen totdat de rename op schijf staat. Op POSIX vervangt rename(2) een bestaande naam al atomair, en dezelfde plaatsing in de map houdt hem van EXDEV af
Opruimen draait op elk pad in een finally-blok. Na een geslaagde rename bestaat de tijdelijke naam niet meer, dus de delete doet niets; na een storing haalt hij het gedeeltelijke bestand weg. De map houdt aan het eind precies de bestanden over die hij vóór de aanroep had
Waarom verruimt een tijdelijk bestand de rechten op Windows?
Een bestand dat met een nil security descriptor wordt aangemaakt, erft zijn DACL van de bovenliggende map; het bestand dat hij op het punt staat te vervangen heeft niets te zeggen. Voor een gloednieuw document is die default goed. Voor een in-place reparatie is hij fout. Stel een beheerder die contract.pdf heeft dichtgezet op één account met een beschermde, niet-overgeërfde DACL. Een kaal tijdelijk bestand ernaast pakt de ruimere rechten van de map op, en na de rename draagt de gepubliceerde contract.pdf die ruime DACL, want NTFS-beveiliging hoort bij het bestandsobject, niet bij de naam. De bytes zijn goed en de retourwaarde is 1. De toegangsbeperking die de beheerder had ingesteld is simpelweg verdwenen
RepairQDFFile leest daarom de DACL van de bestemming voordat hij het tijdelijke bestand aanmaakt en geeft die via lpSecurityAttributes aan CreateFileW door, zodat het nieuwe bestand geboren wordt met de rechten van het oude bestand en de rename niets verandert wat een beheerder zou merken. Het uitlezen is GetFileSecurityW met DACL_SECURITY_INFORMATION, met een buffer gedimensioneerd op het ERROR_INSUFFICIENT_BUFFER-antwoord van de eerste aanroep. Bestaat de bestemming nog niet, het normale geval bij een apart uitvoerpad, dan wordt geen descriptor meegegeven en erft het nieuwe bestand van zijn map zoals elk ander nieuw bestand
Drie voorwaarden stoppen de publicatie in plaats van dat de code gokt, en elk meldt LastErrorCode 305:
- De DACL kan niet worden gelezen, bijvoorbeeld omdat het aanroepende account
READ_CONTROLop de bestemming mist - De teruggegeven descriptor heeft
SE_DACL_PRESENTniet gezet; zo'n descriptor aanCreateFileWgeven zou Windows laten terugvallen op de standaard-DACL van het proces en toegangssemantiek veranderen waar niemand om vroeg - De bestemming draagt
FILE_ATTRIBUTE_ENCRYPTED; het tijdelijke bestand zou plaintext zijn, en het eroverheen hernoemen naar een EFS-beschermd bestand publiceert een onversleutelde vervanging van iets wat de gebruiker bewust had versleuteld
EFS is een bestandssysteemfunctie en heeft niets te maken met de security handlers van PDF, die aan bod komen in het artikel over het laden van versleutelde documenten. De faalvorm is wel dezelfde soort stille degradatie, en weigeren is de juiste default. Een reparatie die op een EFS-share af en toe nee zegt, is een supportticket. Een reparatie die een bestand ontsleutelt dat de gebruiker had versleuteld, is een incident
Schrijft u zelf een DACL-preservationstest, dan is één Windows-detail makkelijk fout te doen. Om het beperkte testbestand te bouwen, past u een owner-only DACL toe en zet u SE_DACL_PROTECTED expliciet in de descriptor control; PROTECTED_DACL_SECURITY_INFORMATION aan SetFileSecurityW meegeven maakt van een onbeschermde descriptor geen beschermde. Controleer daarna dat het gepubliceerde bestand nog steeds de protected-bit en een expliciete, niet-null DACL meldt, één keer voor een apart uitvoerpad en één keer voor reparatie over de bron zelf
Welke LastErrorCode vertelt u wat er faalde?
RepairQDFFile geeft 1 terug bij succes en 0 bij falen, en TPDFlib.LastErrorCode benoemt de fase die weigerde. Een invoer die niet bestaat, niet te openen is, of groter is dan 2 GB meldt 401, en een exception tijdens het lezen wordt ook naar 401 in kaart gebracht in plaats van in de write-code te lekken. Ongeldige of dubbelzinnige QDF-structuur, zoals die dubbele streammarker, meldt PDFLIB_ERROR_QDF_REPAIR (107); de bestemming werd nooit geopend. Alles na de reparatie, van het oplossen van het pad tot flush en rename, meldt PDFLIB_ERROR_QDF_WRITE (305). De realistische write-storingen landen allemaal op 305: een bestemming die door een andere handle zonder delete sharing vastgehouden wordt, een alleen-lezen bestemming, een ontbrekende bestemmingsmap, plus de DACL- en EFS-weigeringen hierboven. In elk 305-geval blijft geen nieuw of gedeeltelijk doel achter
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
// In-place reparatie: hetzelfde pad is input en output
if Pdf.RepairQDFFile('edited.qdf.pdf', 'edited.qdf.pdf') = 1 then
Log('published; the previous bytes were replaced in one rename')
else
case Pdf.LastErrorCode of
401: Log('could not read the input; it was not modified');
PDFLIB_ERROR_QDF_REPAIR:
Log('QDF structure rejected; the destination was never opened');
PDFLIB_ERROR_QDF_WRITE:
Log('write, flush or replace refused; the destination keeps its old bytes');
end;
finally
Pdf.Free;
end;
end;
Het 305-pad is makkelijk te reproduceren vanuit uw eigen testproject zonder een bestands-API te hooken. De fmShareDenyNone van Delphi opent een bestand met read- en write-sharing maar zonder FILE_SHARE_DELETE, en Windows weigert iets over een bestand dat zo vastgehouden wordt te hernoemen. Houd zo'n handle open tijdens de aanroep en de rename faalt bij de allerlaatste stap, nadat de data is geschreven en geflusht, precies het geval dat u het liefst afgehandeld ziet
var
Before: TBytes;
Holder: TFileStream;
Search: TSearchRec;
begin
Before := TFile.ReadAllBytes(Target); // System.IOUtils
Holder := TFileStream.Create(Target, fmOpenRead or fmShareDenyNone);
try
Assert(Pdf.RepairQDFFile(Source, Target) = 0);
Assert(Pdf.LastErrorCode = PDFLIB_ERROR_QDF_WRITE);
finally
Holder.Free;
end;
// Deze controles zijn suggesties voor uw eigen test, geen library-aanroepen
Assert(BytesEqual(Before, TFile.ReadAllBytes(Target))); // BytesEqual: uw hulpfunctie
// Er mag geen tempbestand achterblijven
if FindFirst(ExtractFilePath(Target) + '.pdflib-qdf-*.tmp',
faAnyFile, Search) = 0 then
try
Assert(False, 'temporary file left behind: ' + Search.Name);
finally
FindClose(Search);
end;
end;
Voor de gewoonte om de code te lezen in plaats van alleen op de 0 te vertrouwen, loopt het artikel over het diagnosticeren van stille fouten hetzelfde patroon langs over de rest van de API
Waar de garantie ophoudt
De belofte dekt storingen die het proces kan zien. Wordt het proces gedood tussen het aanmaken van het tijdelijke bestand en de rename, dan loopt het finally-blok nooit en blijft er een .pdflib-qdf-<GUID>.tmp-bestand in de map staan. De bestemming is nog intact, en dat is wat telt, maar die rommel opruimen is aan u. Stroomuitval is een bestandssysteemvraag: de data is geflusht en de rename is write-through, maar de mapentry wordt op POSIX niet apart gesynct, dus duurzaamheid is wat het bestandssysteem daar bovenop biedt
Een geslaagde rename levert ook een nieuw bestandsobject op, en een paar eigenschappen van het oude reizen niet mee. De DACL wordt bewust overgebracht. Eigendom niet, want er wordt alleen DACL_SECURITY_INFORMATION gelezen, dus het gepubliceerde bestand is van het account dat de reparatie draaide. Het tijdelijke bestand wordt met FILE_ATTRIBUTE_NORMAL aangemaakt, wat betekent dat verborgen-, archief- en soortgelijke attribuutbits fris beginnen, en alternate data streams op het oude bestand zijn weg. Op POSIX houdt het gepubliceerde bestand de 0600-mode van het tijdelijke bestand; roep daarna chmod aan als een groep leestoegang nodig heeft. De attributen en DACL van de bestemming worden gelezen voordat het tijdelijke bestand bestaat, dus een tweede writer op hetzelfde pad coördineren is aan u
SaveQDFToFile en ConvertFileToQDF schrijven direct
RepairQDFFile is het enige QDF-ingangspunt dat via een tijdelijk bestand publiceert. SaveQDFToFile en ConvertFileToQDF openen hun uitvoer met fmCreate en streamen de conversie er rechtstreeks in, net zoals het incrementele pad uit het artikel over het toevoegen van updates aan een stream schrijft naar welke stream u hem ook geeft. De splitsing is bewust. Die twee aanroepen maken van een document dat al is geladen en gevalideerd een nieuw debugging-artefact, dus er is geen onvertrouwde invoer die halverwege kan worden geweigerd, en hun uitvoerpad is normaal een vers bestand. RepairQDFFile neemt een met de hand bewerkt, onvertrouwd bestand aan en wordt routinematig weer naar hetzelfde pad gericht; die combinatie rechtvaardigt de extra machinerie
Wilt u toch atomaire vervanging voor een QDF-dump, dan is een kort eigen hulpje genoeg:
// Eigen hulpfunctie: schrijf de QDF naast het doel en vervang hem in één rename
function SaveQDFReplacing(Pdf: TPDFlib; const FileName: string): Boolean;
var
ID: TGUID;
TempName: string;
begin
CreateGUID(ID);
TempName := ExtractFilePath(ExpandFileName(FileName)) +
'.myapp-qdf-' + GUIDToString(ID) + '.tmp';
try
Result := (Pdf.SaveQDFToFile(TempName) = 1) and
MoveFileExW(PWideChar(WideString(TempName)), PWideChar(WideString(FileName)),
MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH);
finally
if FileExists(TempName) then
DeleteFile(TempName); // alleen aanwezig als iets faalde
end;
end;
Deze versie slaat de DACL-kopie en de expliciete dataflush over die RepairQDFFile uitvoert. Voor QDF-dumps die in een scratch- of debuggingmap belanden, vind ik dat de juiste hoeveelheid machinerie; vervangt de dump een bestand met dichtgezette rechten, draai hem dan door het reparatiepad of kopieer de security descriptor zelf
Atomaire reparatie-output, de QDF-conversieaanroepen en de foutcodes hierboven komen allemaal mee in PDF Library for Delphi, naast de cross-reference-recovery, incrementele updates en versleutelingsondersteuning die in andere berichten hier worden besproken