Éditez un fichier QDF à la main, glissez un second marqueur de flux pour le même objet, puis lancez une réparation en place avec le code Delphi le plus naturel qui soit. L’étape de réparation remarque le dégât et refuse. Quand vous regardez le fichier ensuite, il fait 0 octet, parce que TFileStream.Create(OutputFileName, fmCreate) a tronqué ce chemin même une ligne avant que le validateur n’ait jamais vu l’entrée. Réparer des octets en mémoire est la moitié facile du travail. Les remettre sur disque sans jamais laisser un fichier de longueur nulle, à moitié écrit ou mal permissionné est l’autre moitié, et elle a ses propres modes de défaillance
Pourquoi une réparation en place vide-t-elle le fichier ?
Une réparation en place vide le fichier parce que l’ordre des opérations est faux : fmCreate tronque à l’ouverture, donc tout refus qui arrive après l’ouverture arrive trop tard. Quand InputFileName et OutputFileName sont le même chemin, une entrée rejetée devient un document perdu. Un analyseur soigneux ne peut pas vous sauver ici. Il peut laisser son flux de sortie intact quand il rejette des marqueurs ambigus, mais l’appelant a déjà détruit la cible un appel plus tôt
// La version évidente : la destination est tronquée avant que quoi que ce soit soit validé
Input := TMemoryStream.Create;
try
Input.LoadFromFile(InputFileName); // même chemin que OutputFileName
Output := TFileStream.Create(OutputFileName, fmCreate); // le fichier fait maintenant 0 octet
try
if not MyRepairQDF(Input, Output) then // votre propre validateur : trop tard pour dire non
raise Exception.Create('QDF structure rejected');
finally
Output.Free;
end;
finally
Input.Free;
end;
La tentative suivante habituelle est de réparer dans un TMemoryStream et de n’ouvrir la sortie qu’après le succès de la réparation. Cela bouche le trou de la mauvaise entrée et rien d’autre. La phase d’écriture reste tronquer-puis-copier, donc un disque plein, une violation de partage à mi-course ou une exception entre la troncature et le dernier WriteBuffer laissent encore la destination endommagée. L’analyse et la publication sont deux frontières séparées et chacune a besoin de sa propre protection
Dans PDF Library for Delphi, TPDFlib.RepairQDFFile(InputFileName, OutputFileName) s’occupe des deux. Il lit l’entrée, la répare entièrement en mémoire, et retourne tôt sans rien avoir ouvert sur la destination si la structure QDF est rejetée. Seule une réparation réussie passe à la publication, et la publication n’ouvre jamais la destination en écriture
Quatre étapes entre les octets réparés et la destination
RepairQDFFile garantit que le chemin de destination contient soit le fichier ancien complet, soit le fichier nouveau complet, pour toute défaillance que le process lui-même peut observer. Il y parvient en quatre étapes, et aucune ne démarre tant que la précédente n’est pas terminée. La destination est résolue en chemin complet avec GetFullPathNameW, appelé une fois pour apprendre la longueur requise et une fois dans un tampon d’exactement cette taille, pour qu’un chemin long ne soit pas coupé à MAX_PATH. Ensuite un fichier temporaire nommé .pdflib-qdf- plus un GUID plus .tmp est créé dans le répertoire de destination avec CreateFileW et CREATE_NEW sous Windows, ou open(2) avec O_CREAT or O_EXCL et le mode 0600 sous POSIX. Les deux drapeaux font échouer la création si le nom existe déjà. Deux process qui tireraient le même GUID ne peuvent pas finir par partager un handle
Le flux réparé est ensuite copié par blocs de 64 Kio via WriteBuffer, qui lève une erreur sur une écriture courte au lieu de renvoyer un compte que personne ne vérifie. FlushFileBuffers ou fsync(2) pousse les données vers le bas, et le handle est fermé. La fermeture ne lève délibérément pas : une fois le vidage réussi, une fermeture en échec ne menace plus les données, et une exception là cacherait la vraie erreur d’écriture ou de vidage à celui qui doit diagnostiquer. La dernière étape est le renommage
Le renommage est l’endroit où la plupart des routines de sauvegarde sûre maison cassent. Sous Windows, le fichier temporaire remplace la cible via MoveFileExW avec MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH, ce qui sur un seul volume est une seule opération du système de fichiers. MOVEFILE_COPY_ALLOWED est laissé de côté à dessein. Un déplacement entre volumes dégénère en silence en copie-puis-suppression, la séquence non atomique exacte que toute la conception évite, et je préfère de loin voir un renommage refusé qu’une copie « réussie » qui était à moitié faite quand la machine est tombée. Comme le fichier temporaire siège dans le répertoire de destination, il est de toute façon sur le volume de destination. L’ancien fichier n’est jamais supprimé d’abord non plus ; un couple suppression-puis-renommage laisse une fenêtre pendant laquelle le chemin n’existe pas, et un crash dedans perd le document. MOVEFILE_WRITE_THROUGH empêche l’appel de revenir avant que le renommage ait atteint le disque. Sous POSIX, rename(2) remplace déjà un nom existant de façon atomique, et le placement dans le même répertoire le tient à l’écart d’EXDEV
Le nettoyage tourne dans un bloc finally sur tous les chemins. Après un renommage réussi, le nom temporaire n’existe plus, donc la suppression ne fait rien ; après un échec, il retire le fichier partiel. Le répertoire finit avec exactement les fichiers qu’il avait avant l’appel
Pourquoi un fichier temporaire assouplit-il les permissions sous Windows ?
Un fichier créé avec un descripteur de sécurité nil hérite sa DACL du répertoire parent ; le fichier qu’il s’apprête à remplacer n’a pas son mot à dire. Pour un document tout neuf, ce défaut est le bon. Pour une réparation en place, il est faux. Imaginez un opérateur qui a verrouillé contract.pdf sur un seul compte avec une DACL protégée et non héritée. Un fichier temporaire ordinaire à côté ramasse les permissions plus larges du répertoire, et après le renommage le contract.pdf publié porte cette DACL large, parce que la sécurité NTFS appartient à l’objet fichier plutôt qu’au nom. Les octets sont corrects et la valeur de retour vaut 1. La restriction d’accès que l’opérateur avait posée a simplement disparu
RepairQDFFile lit donc la DACL de la destination avant de créer le fichier temporaire et la passe à CreateFileW via lpSecurityAttributes, pour que le nouveau fichier naisse avec les permissions de l’ancien fichier et que le renommage ne change rien que l’opérateur remarquerait. La lecture est un GetFileSecurityW avec DACL_SECURITY_INFORMATION, le tampon dimensionné d’après la réponse ERROR_INSUFFICIENT_BUFFER du premier appel. Quand la destination n’existe pas encore, ce qui est le cas normal pour un chemin de sortie séparé, aucun descripteur n’est passé et le nouveau fichier hérite de son répertoire comme tout nouveau fichier
Trois conditions arrêtent la publication au lieu de laisser le code deviner, et chacune rapporte LastErrorCode 305 :
- La DACL ne peut pas être lue, par exemple parce que le compte appelant manque de
READ_CONTROLsur la destination - Le descripteur renvoyé n’a pas
SE_DACL_PRESENTpositionné ; confier un tel descripteur àCreateFileWlaisserait Windows retomber sur la DACL par défaut du process et changer une sémantique d’accès que personne n’a demandé de changer - La destination porte
FILE_ATTRIBUTE_ENCRYPTED; le fichier temporaire serait en clair, et le renommer par-dessus un fichier protégé par EFS publierait un remplacement non chiffré de quelque chose que l’utilisateur avait choisi de chiffrer
EFS est une fonctionnalité du système de fichiers et n’a rien à voir avec les gestionnaires de sécurité PDF, qui sont couverts dans l’article sur le chargement des documents chiffrés. La forme de l’échec est le même genre de déclassement silencieux, pourtant, et le refuser est le bon défaut. Une réparation qui dit parfois non sur un partage EFS est un ticket de support. Une réparation qui déchiffre un fichier que l’utilisateur a chiffré est un incident
Si vous écrivez vous-même un test de préservation de DACL, un détail Windows est facile à rater. Pour construire le fichier de test restreint, appliquez une DACL réservée au propriétaire et positionnez SE_DACL_PROTECTED dans le contrôle du descripteur explicitement ; passer PROTECTED_DACL_SECURITY_INFORMATION à SetFileSecurityW ne transforme pas un descripteur non protégé en descripteur protégé. Ensuite, vérifiez que le fichier publié rapporte toujours le bit protégé et une DACL explicite non nulle, une fois pour un chemin de sortie séparé et une fois pour une réparation par-dessus la source elle-même
Quel LastErrorCode vous dit ce qui a échoué ?
RepairQDFFile renvoie 1 en cas de succès et 0 en cas d’échec, et TPDFlib.LastErrorCode nomme l’étape qui a refusé. Une entrée qui n’existe pas, ne peut pas être ouverte, ou dépasse 2 Go rapporte 401, et une exception pendant la lecture est aussi ramenée à 401 au lieu de fuir dans le code d’écriture. Une structure QDF invalide ou ambiguë, comme ce marqueur de flux dupliqué, rapporte PDFLIB_ERROR_QDF_REPAIR (107) ; la destination n’a jamais été ouverte. Tout ce qui suit la réparation, de la résolution du chemin au vidage et au renommage, rapporte PDFLIB_ERROR_QDF_WRITE (305). Les échecs d’écriture réalistes atterrissent tous sur 305 : une destination tenue ouverte par un autre handle sans partage de suppression, une destination en lecture seule, un répertoire de destination manquant, plus les refus DACL et EFS ci-dessus. Dans chaque cas 305, aucune cible nouvelle ou partielle n’est laissée derrière
var
Pdf: TPDFlib;
begin
Pdf := TPDFlib.Create;
try
// Réparation en place : le même chemin est entrée et sortie
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;
Le chemin 305 est facile à reproduire depuis votre propre projet de test sans hooker aucune API de fichier. Le fmShareDenyNone de Delphi ouvre un fichier avec partage lecture et écriture mais sans FILE_SHARE_DELETE, et Windows refuse de renommer quoi que ce soit par-dessus un fichier tenu ainsi. Gardez un tel handle ouvert pendant l’appel et le renommage échoue à la toute dernière étape, après que les données ont été écrites et vidées, ce qui est le cas que vous voulez le plus voir géré
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;
// Ces vérifications sont des suggestions pour votre propre test, pas des appels de bibliothèque
Assert(BytesEqual(Before, TFile.ReadAllBytes(Target))); // BytesEqual : votre helper
// Aucun fichier temporaire ne doit rester
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;
Pour l’habitude de lire le code au lieu de faire confiance au 0 seul, l’article sur le diagnostic des échecs silencieux parcourt le même schéma sur le reste de l’API
Où la garantie s’arrête
La promesse couvre les défaillances que le process peut voir. Si le process est tué entre la création du fichier temporaire et le renommage, le bloc finally ne tourne jamais et un fichier .pdflib-qdf-<GUID>.tmp reste dans le répertoire. La destination reste intacte, ce qui est ce qui compte, mais balayer ces débris est pour vous. La coupure de courant est une question de système de fichiers : les données sont vidées et le renommage est en write-through, pourtant l’entrée de répertoire n’est pas syncée séparément sous POSIX, donc la durabilité est ce que le système de fichiers fournit par-dessus
Un renommage réussi produit aussi un nouvel objet fichier, et quelques propriétés de l’ancien ne voyagent pas avec. La DACL est transportée délibérément. La propriété ne l’est pas, puisque seul DACL_SECURITY_INFORMATION est lu, donc le fichier publié appartient au compte qui a lancé la réparation. Le fichier temporaire est créé avec FILE_ATTRIBUTE_NORMAL, ce qui veut dire que les bits caché, archive et similaires repartent à zéro, et les flux de données alternatifs de l’ancien fichier sont perdus. Sous POSIX, le fichier publié garde le mode 0600 du fichier temporaire ; appelez chmod ensuite si un groupe a besoin d’un accès en lecture. Les attributs et la DACL de la destination sont lus avant que le fichier temporaire existe, donc un second écrivain sur le même chemin est à coordonner par vous
SaveQDFToFile et ConvertFileToQDF écrivent directement
RepairQDFFile est le seul point d’entrée QDF qui publie via un fichier temporaire. SaveQDFToFile et ConvertFileToQDF ouvrent leur sortie avec fmCreate et y envoient la conversion en flux direct, un peu comme le chemin incrémental de l’article sur l’ajout de mises à jour à un flux écrit dans le flux que vous fournissez. La scission est intentionnelle. Ces deux appels transforment un document déjà chargé et validé en un nouvel artefact de débogage, donc il n’y a aucune entrée non fiable qui pourrait être rejetée à mi-course, et leur chemin de sortie est d’ordinaire un fichier neuf. RepairQDFFile prend un fichier édité à la main, non fiable, et se voit couramment repointé vers le même chemin ; c’est cette combinaison qui justifie la machinerie supplémentaire
Quand vous voulez quand même un remplacement atomique pour un dump QDF, un court wrapper à vous suffit :
// Votre propre helper : écrire le QDF à côté de la cible, puis le remplacer en un renommage
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); // présent seulement quand quelque chose a échoué
end;
end;
Cette version saute la copie de DACL et le vidage explicite des données que RepairQDFFile effectue. Pour des dumps QDF qui atterrissent dans un dossier de travail ou de débogage, je pense que c’est la bonne quantité de machinerie ; si le dump remplace un fichier aux permissions verrouillées, passez par le chemin de réparation ou copiez le descripteur de sécurité vous-même
La sortie de réparation atomique, les appels de conversion QDF et les codes d’erreur ci-dessus sont tous livrés dans PDF Library for Delphi, à côté de la récupération de références croisées, de la mise à jour incrémentale et de la prise en charge du chiffrement traitées dans d’autres articles ici