Ouvrez un vieux xls, enregistrez-le à nouveau, et la formule add-in qui appelait une bibliothèque d’analyse enregistrée pointe désormais vers une référence vide à l’intérieur du classeur lui-même. HotXLS remonte cette corruption silencieuse jusqu’à une mauvaise hypothèse : qu’un enregistrement SupBook du BIFF est soit self, soit un fichier externe. [MS-XLS] définit sept types, pas deux
Pourquoi un classeur enregistré perd-il ses liens add-in ?
Parce que le test de classification était structurel plutôt que typé. Le raccourci traditionnel lit un enregistrement SupBook ($01AE), vérifie s’il porte le marqueur self, et sinon traite la chaîne qui suit comme une URL de document. Tout enregistrement qui n’est ni l’un ni l’autre tombe dans une branche par défaut, et la branche par défaut est presque toujours « c’est le classeur lui-même ». Un lien de conteneur add-in, un lien même feuille, un emplacement inutilisé et un enregistrement tronqué se retrouvent tous affublés de la même mauvaise étiquette. Rien ne lève pendant que cela arrive : l’enregistrement s’analyse, la formule se recompile, le fichier s’enregistre sans avertissement, et le défaut refait surface trois semaines plus tard quand quelqu’un remarque une colonne de zéros là où se trouvait une conversion de devises. [MS-XLS] §2.4.271 décrit un enregistrement qui peut être une auto-référence, une référence même feuille, un conteneur de fonctions add-in, un classeur externe avec un chemin virtuel et une table de noms de feuilles, une liaison de données DDE ou OLE, ou un espace réservé inutilisé — et un septième état qui n’est pas dans la spécification mais existe sur de vrais disques, l’enregistrement qui ne s’analyse pas. La correction n’est pas une meilleure heuristique ; c’est refuser d’avoir une heuristique du tout
Les sept types qu’un enregistrement SupBook peut porter
HotXLS déclare la taxonomie des liens de support comme une énumération fermée dans lxExternSheet.pas, et toute décision en aval commute dessus. Neuf valeurs d’énumération couvrent les sept catégories, car le cas DDE et OLE a besoin d’un état provisoire avant de pouvoir être résolu :
type
TXLSSupportingLinkKind = (
slkUnknown, // échec d'analyse, ou octets de queue restants
slkSelf, // ce classeur
slkSameSheet, // marqueur U+0000
slkAddIn, // conteneur de fonctions add-in
slkExternalWorkbook, // chemin virtuel + table de noms de feuilles
slkDde, // résolu depuis les drapeaux ExternName
slkOle, // résolu depuis les drapeaux ExternName
slkDdeOrOle, // l'un des deux, on ne sait pas encore lequel
slkUnused); // espace réservé d'une seule espace
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // base zéro, tel que stocké dans ExternSheet.rgXTI
ExternID : Integer; // base un, la convention interne
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
La répartition est pilotée par sentinelles, pas par chaînes. Une valeur de champ $0401 marque l’enregistrement self. Un nombre de feuilles de un apparié à $3A01 marque un conteneur add-in. Seule une valeur dans la plage 1 à $00FF signifie qu’un chemin virtuel encodé suit, et seulement alors HotXLS décode une chaîne. Tout ce qui est hors de ces trois formes reste slkUnknown, et un enregistrement dont la table de noms de feuilles ne consomme pas exactement le corps de l’enregistrement est rétrogradé en slkUnknown même quand la tête paraissait plausible
Pourquoi le marqueur même feuille se décode-t-il en chaîne vide ?
Parce que le lecteur de chaînes BIFF à usage général détruit l’octet dont la classification dépend. Le lien de support même feuille est une chaîne d’un caractère dont l’unique caractère est U+0000, et TXLSBlob.GetBiffString rend cela comme un WideString vide, indiscernable d’un chemin réellement vide — qui est exactement l’entrée à laquelle une heuristique d’auto-référence répond « self ». HotXLS lit donc le premier point de code brut hors du corps de l’enregistrement plutôt que de faire confiance à la valeur décodée :
StringOffset := offset;
FDocUrl := Data.GetBiffString(offset, False, True);
FirstChar := $FFFF;
if val = 1 then
begin
StringOptions := Data.GetByte(StringOffset + 2);
if (StringOptions and $01) = 0 then
FirstChar := Data.GetByte(StringOffset + 3) // compressé, un octet
else
FirstChar := Data.GetWord(StringOffset + 3); // étendu, deux octets
end;
if FirstChar = 0 then
FKind := slkSameSheet
else if (Length(FDocUrl) = 1) and (FDocUrl[1] = WideChar(#32)) then
FKind := slkUnused
else if Pos(WideChar(#3), FDocUrl) > 0 then
FKind := slkDdeOrOle
else if FDocUrl <> '' then
FKind := slkExternalWorkbook;
Notez la branche compressé contre étendu. L’octet d’options se trouve à un décalage fixe de l’en-tête de chaîne et le premier point de code fait un octet ou deux selon le bit 0, si bien que le lire comme un octet sans condition marche sur la plupart des fichiers et échoue sur ceux écrits par des builds localisés — la pire distribution possible pour un bogue. L’espace réservé inutilisé est attrapé de la même manière, par sa charge utile d’une seule espace littérale, et le cas DDE ou OLE par le séparateur U+0003 enchâssé dans le chemin encodé
Pourquoi DDE et OLE ne peuvent-ils pas être séparés au moment du SupBook ?
Parce que l’enregistrement SupBook ne porte pas les bits discriminants. Il vous dit que le lien est l’un des deux ; les drapeaux fOle et fOleLink qui décident lequel vivent dans l’enregistrement ExternName ($0023) arrivant plus tard dans le flux. HotXLS enregistre slkDdeOrOle au moment de l’analyse et le resserre dans ParseExternalName, et si aucun ExternName n’arrive jamais le type reste provisoire pour toujours — ce qui est correct, car le fichier ne dit réellement rien. Tout consommateur en aval traite cette valeur provisoire comme une vraie valeur plutôt que comme une absence, si bien qu’aucun appelant n’a à inventer un départage. Deviner « probablement DDE » ici achèterait une énumération plus rangée et une classe de réponses fausses que personne ne pourrait retracer :
if FKind = slkDdeOrOle then
begin
if Data.DataLength < 2 then
Exit;
Flags := Data.GetWord(0);
if (Flags and $0010) <> 0 then
FKind := slkOle
else if (Flags and $0008) <> 0 then
FKind := slkDde;
end;
Les index XTI sont en base zéro sur disque et en base un à l’intérieur
HotXLS effectue la conversion de décalage de un exactement une fois, au point où un jeton entre dans l’arbre syntaxique interne, et nulle part ailleurs. PtgNameX.ixti ([MS-XLS] §2.5.198.85) est un index en base zéro dans le tableau rgXTI de l’enregistrement ExternSheet ($0017, §2.4.106), tandis que la convention ExternID interne de la bibliothèque est en base un avec zéro réservé à « aucune feuille externe ». Le chemin de lecture BIFF8 fait FExternID := wValue + 1 quand il décode un jeton tNameX et le chemin d’écriture émet StoreExternID - 1, laissant la vue brute des jetons et la sémantique sur disque intactes. Se tromper là-dessus est inhabituellement difficile à attraper : les noms définis externes se résolvent vers l’entrée voisine, et dans un fichier avec une unique entrée XTI l’index 0 devient index 1, manque, et le nom dégénère en silence. Une régression qui n’exerce que du texte de formules recompilé ne la voit jamais, car la recompilation ne touche jamais l’index sur disque — le même piège qui rend les noms définis à cheval sur des feuilles et des classeurs dignes d’être testés contre de vrais flux d’octets. La résolution est bornée aux deux bouts : TlxExternSheetSheet.TryResolveXti renvoie False pour un index négatif ou une entrée manquante, TXLSSupBook.TryGetKind renvoie False pour un index SupBook hors du tableau, et ClassifyXti projette alors slkSelf et slkSameSheet vers frcInternal, slkExternalWorkbook vers frcExternalWorkbook, et slkAddIn, slkDde, slkOle et slkDdeOrOle vers frcExternalOther. Tout le reste, chaque chemin hors plage inclus, atterrit sur frcUnknownOrMalformed
Classer une formule avant de la figer
TXLSCompiledFormula.ClassifyReferences balaye directement le flux de jetons BIFF préservé au lieu de décompiler la formule et de chercher des crochets. La chasse aux crochets dans le texte de formule est une heuristique de texte portant un manteau d’analyseur : elle apparie les littéraux de chaîne, elle apparie les références structurées, et elle rate complètement les noms définis externes, puisque ceux-ci ne portent aucun crochet sous forme décompilée. Le balayage de jetons ne regarde que PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d et PtgAreaErr3d, en retombant sur un parcours de l’arbre syntaxique quand aucun flux BIFF ne survit. La fusion est délibérément pessimiste — la priorité fixée est frcUnknownOrMalformed, puis frcExternalWorkbook, puis frcExternalOther, puis frcInternal — si bien qu’un seul jeton illisible empoisonne toute la formule. Pour un nom défini externe l’index de nom est validé lui aussi : en base un, dans la plage, et adossé à un enregistrement ExternName retenu
var
Wb : TXLSWorkbook;
Sheet: TXLSWorksheet;
i : Integer;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Open('quarterly.xls');
for i := 1 to Wb.Sheets.Count do // Sheets est en base un
begin
Sheet := Wb.Sheets[i];
// ne fige QUE les formules classées frcExternalWorkbook ;
// les références internes, add-in, DDE/OLE et malformées restent des formules
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
Le paramètre OnlyExternal est l’endroit où la taxonomie se paie elle-même. Figer une formule est irréversible, si bien que l’opération doit prouver qu’une référence est un classeur externe plutôt que de le soupçonner seulement. Les appels add-in survivent, les liens DDE et OLE survivent, et tout ce que l’analyseur n’a pas su comprendre entièrement survit, car l’issue sûre de l’incertitude est de ne rien changer. La même discipline gouverne la re-liaison des formules copiées entre classeurs, où une référence mal classée se re-lie au mauvais classeur au lieu d’échouer bruyamment
Les enregistrements qui ne s’analysent pas sont réécrits intacts
HotXLS conserve la charge utile SupBook d’origine et la réémet octet pour octet quand l’enregistrement n’a jamais été édité. Un échec d’analyse met slkUnknown et efface l’état dérivé, mais le corps capturé reste dans FRawData et le chemin de stockage le préfère à toute reconstruction tant que l’élément n’est pas sale et n’est pas l’enregistrement self. L’alternative — normaliser un enregistrement non analysé en auto-référence pour que l’écrivain ait quelque chose de bien formé à émettre — transforme un enregistrement que vous n’avez pas compris en un enregistrement définitivement faux. Ce principe est le même contrat appliqué aux projets VBA et à leurs références externes à travers un cycle chargement-enregistrement, et c’est la différence entre une bibliothèque qui fait l’aller-retour sur des fichiers réels et une qui fait l’aller-retour sur les fichiers que sa suite de tests contient par hasard. Un classeur qui a traversé quinze ans de versions d’Excel, un générateur de rapports et deux outils de migration contiendra des enregistrements que personne de vivant aujourd’hui n’a conçus. Réécrivez-les comme vous les avez trouvés
La classification typée des enregistrements SupBook et XTI est livrée dans HotXLS 2.361.2 à 2.361.4, avec la résolution XTI bornée et le chemin ConvertFormulasToValues plus sûr décrit ici. Si vous maintenez du code Delphi ou C++Builder qui lit des fichiers xls patrimoniaux portant des appels add-in, des liens DDE ou OLE, ou des noms définis externes, le composant tableur HotXLS pour Delphi gère toute la taxonomie nativement, sans installation d’Excel et sans automatisation OLE sur la machine qui fait le travail