HotXLS Excel Library pour Delphi et C++Builder lit et écrit le conteneur Compound File Binary derrière chaque fichier .xls historique, en Object Pascal pur. La classe TlxCompoundFile implémente directement la disposition [MS-CFB] version 3 sur un TStream — en-tête, DIFAT, chaînes FAT, MiniFAT et arbre de répertoire — sans ole32.dll ni COM IStorage nulle part dans le chemin
Cela ressemble à de la plomberie, et pendant vingt ans, ce fut une plomberie appartenant à quelqu'un d'autre. Chaque base de code Delphi qui touchait un fichier .xls appelait StgOpenStorage, récupérait un IStorage, et en extrayait le flux Workbook. Trois lignes, ça marchait bien, personne n'y repensait — jusqu'au jour où ce même code a dû tourner là où Windows n'était pas
Pourquoi StgOpenStorage cesse-t-il de fonctionner sur un serveur ?
L'API COM de stockage structuré échoue exactement dans les formes de déploiement où vit le code Delphi moderne, pour des raisons qui n'ont rien à voir avec le format de fichier. StgOpenStorage est un point d'entrée Win32 dans ole32.dll : il veut un chemin sur un système de fichiers, il veut COM initialisé sur le thread appelant, et il veut être sous Windows. L'exigence de chemin fait mal en premier, car un point de terminaison REST recevant un classeur téléversé a les octets dans un tampon, pas sur disque — on écrit alors le tampon dans un fichier temporaire, on l'ouvre, on le relit, on le supprime, et on se retrouve à devoir gérer correctement un cycle de vie de fichier temporaire sous charge. ILockBytes est l'échappatoire documentée, mais câbler une implémentation personnalisée par-dessus un TMemoryStream demande plus d'interopérabilité COM que la plupart des équipes ne le souhaitent. L'exigence d'initialisation mord en second, généralement dans un thread de travail de service où personne n'a appelé CoInitialize, et l'exigence de plateforme met fin à la conversation dès que la cible est Linux sous FPC, une image de conteneur, ou macOS. HotXLS conserve donc le chemin classique lxOLE construit sur StgOpenStorage comme comportement par défaut, car il est éprouvé et les appelants existants ne devraient pas avoir à changer ; TlxCompoundFile est l'alternative optionnelle pour tous les autres
Ce que disent réellement l'en-tête et les chaînes FAT
Les 512 premiers octets d'un fichier composé répondent à chaque question structurelle nécessaire avant de lire le moindre octet de charge utile. [MS-CFB] §2.2 fixe la signature d'en-tête à l'offset 0 comme les huit octets D0 CF 11 E0 A1 B1 1A E1, et lxIsCompoundStream vérifie exactement cela, restaurant ensuite la position du flux afin qu'un appelant puisse renifler sans rien perturber. Quatre autres champs déterminent la géométrie : l'ordre des octets à 0x1C doit être 0xFFFE, ce qui sert de second contrôle de signature bon marché ; le décalage de secteur à 0x1E donne la taille de secteur comme 1 shl SectorShift, si bien que la version 3 utilise un décalage de 9 pour des secteurs de 512 octets et la version 4 un décalage de 12 pour 4096 ; le décalage de mini-secteur à 0x20 vaut 6, ce qui fait des mini-secteurs de 64 octets ; et le seuil de flux mini à 0x38 est 4096. L'arithmétique d'adresse qui suit est l'endroit le plus courant où se tromper. Le secteur 0 commence immédiatement après l'en-tête, si bien que le secteur N commence à l'offset d'octet 512 + N * SectorSize — notez le 512 littéral, pas SectorSize. Sur un fichier version 3, les deux sont identiques et le bogue se cache pour toujours ; sur un fichier version 4, il lit silencieusement le mauvais secteur, ce qui explique pourquoi HotXLS regroupe cela dans une seule fonction, SidToOffset
Un fichier composé est un système de fichiers FAT à l'intérieur d'un fichier, si bien que le lire signifie parcourir des listes chaînées d'identifiants de secteur où FAT[n] contient l'identifiant suivant le secteur n. Trois sentinelles terminent ou annotent une chaîne — ENDOFCHAIN, FATSECT pour un secteur appartenant à la FAT elle-même, et DIFSECT pour un secteur DIFAT — et les trois se lisent comme des entiers signés 32 bits négatifs, ce qui garde les conditions de boucle simples. Trouver la FAT nécessite une indirection de plus : le DIFAT est le tableau d'identifiants de secteur indiquant où se trouvent les secteurs FAT, et ses 109 premières entrées siègent dans l'en-tête à l'offset 0x4C. TlxCompoundFile parcourt ces 109 entrées, s'arrête à la première entrée négative, et concatène chaque secteur FAT en un unique tableau plat d'Integer. Cela fait 109 secteurs FAT à 128 entrées chacun sur un secteur de 512 octets, soit 13 952 secteurs adressables, soit environ 6,8 Mio de conteneur avant que le DIFAT ne doive déborder dans sa propre chaîne
La seconde table d'allocation existe parce que les secteurs de 512 octets gaspillent la majeure partie de leur espace sur de petits flux. Tout flux en dessous du seuil de 4096 octets n'est pas stocké dans des secteurs du tout : il vit à l'intérieur du mini flux, lui-même un flux ordinaire suspendu à l'entrée de répertoire racine, subdivisé en mini-secteurs de 64 octets et chaîné via une MiniFAT parallèle enracinée à l'offset d'en-tête 0x3C. Ouvrez un vrai fichier .xls et le flux Workbook siège sur la FAT normale tandis que les flux d'informations sommaires siègent dans l'espace des mini-secteurs, ce qui explique pourquoi une implémentation ne couvrant que le chemin FAT semble fonctionner correctement jusqu'au moment où elle a besoin des métadonnées du document. Le répertoire est la troisième structure et celle qui rend le conteneur navigable : chaque entrée fait exactement 128 octets, quatre par secteur de 512 octets, portant un nom UTF-16 dans les 64 premiers octets, sa longueur en octets à 0x40, le type d'objet à 0x42 (1 = storage, 2 = stream, 5 = root), des liens d'arbre à 0x44, 0x48 et 0x4C, le secteur de début à 0x74 et la taille de flux 32 bits à 0x78. Cette longueur de nom compte les octets y compris le null terminal, si bien que le nombre de caractères est NameLen div 2 - 1, et se tromper de un dans ce calcul est comment on finit avec un flux nommé Workboo
Extraire un flux Workbook d'un tampon mémoire
TlxCompoundFile.OpenStream cache tout ce qui précède derrière un seul appel qui prend un nom de flux et retourne un TlxCfbStream contenant les octets entièrement matérialisés. Toute la séquence — reniflage, chargement, extraction — s'exécute sur un TBytesStream sans que rien ne touche jamais le disque
uses
Classes, SysUtils, lxCompoundFile;
function ExtractBiffPayload(const Blob: TBytes): TBytes;
var
Src: TBytesStream;
Cfb: TlxCompoundFile;
Wb: TlxCfbStream;
begin
SetLength(Result, 0);
Src:= TBytesStream.Create(Blob);
try
if not lxIsCompoundStream(Src) then
Exit; // not a CFB container at all
Cfb:= TlxCompoundFile.Create;
try
Cfb.LoadFromStream(Src); // header, FAT, directory, MiniFAT
Wb:= Cfb.OpenStream('Workbook'); // BIFF8
if Wb = nil then
Wb:= Cfb.OpenStream('Book'); // BIFF5 / BIFF7
if Wb <> nil then
try
Result:= Wb.Data;
finally
Wb.Free;
end;
finally
Cfb.Free;
end;
finally
Src.Free;
end;
end;
Deux détails méritent d'être signalés ici. LoadFromStream prend un indicateur AOwnsStream valant False par défaut, si bien que l'appelant garde la responsabilité du flux source — délibéré, car le cas courant est un flux que l'application possède déjà. Et OpenStream retourne un TlxCfbStream possédant sa propre copie des octets, exposée via Data, Size, Read, Seek et CopyTo. Cette copie est un coût réel sur un gros classeur, et c'est le prix honnête d'une conception où l'objet retourné reste valide après que le conteneur a été libéré. Quand un classeur est assez volumineux pour qu'une copie complète en mémoire soit tout simplement la mauvaise approche, le lecteur direct en flux pour feuilles de calcul surdimensionnées est le meilleur point d'entrée
Pourquoi un fichier XLSX chiffré ressemble-t-il à un fichier XLS ?
Parce que c'en est un, au niveau du conteneur — et c'est là le bénéfice pratique de posséder cette couche. Ouvrez un .xlsx chiffré dans un éditeur hexadécimal, et les huit premiers octets sont D0 CF 11 E0 A1 B1 1A E1, identiques octet pour octet à un .xls millésime 1997, car le chiffrement [MS-OFFCRYPTO] ne chiffre pas le paquet ZIP sur place : il enveloppe le paquet entier dans un conteneur CFB sous forme d'un flux nommé EncryptedPackage, à côté d'un flux EncryptionInfo décrivant l'algorithme. La signature identifie donc le conteneur et ne dit rien sur la charge utile. Distinguer un classeur BIFF d'un paquet OOXML chiffré signifie lire le répertoire, ce qui après LoadFromStream est un parcours sur EntryCount et Entries, ou une paire de sondes HasStream
type
TCfbPayload = (cpUnknown, cpBiffWorkbook, cpEncryptedOoxml);
function ClassifyContainer(AStream: TStream): TCfbPayload;
var
Cfb: TlxCompoundFile;
E: TlxCfbEntry;
I: Integer;
begin
Result:= cpUnknown;
Cfb:= TlxCompoundFile.Create;
try
Cfb.LoadFromStream(AStream);
for I:= 0 to Cfb.EntryCount - 1 do
begin
E:= Cfb.Entries(I);
if E.EntryType <> cfbStream then
Continue;
if E.Name = 'EncryptedPackage' then
Result:= cpEncryptedOoxml
else if (E.Name = 'Workbook') or (E.Name = 'Book') then
Result:= cpBiffWorkbook;
end;
finally
Cfb.Free;
end;
end;
Les noms de répertoire méritent leur propre avertissement : les flux d'informations sommaires portent un caractère de contrôle 0x05 en tête de leur nom, si bien qu'une comparaison écrite contre une simple chaîne d'affichage ne les fera jamais correspondre, et une ligne de journal naïve les affiche comme du charabia. Tout ce qui se trouve en aval de cette classification — dériver la clé, vérifier le témoin de mot de passe — est un problème séparé, couvert dans les notes sur pourquoi Excel rejette un classeur chiffré avec le mauvais mode de chiffrement. La couche conteneur vous dit seulement devant quelle porte vous vous trouvez
Écrire un conteneur qu'Excel ouvrira réellement
Le côté écriture de TlxCompoundFile est délibérément plus étroit que le côté lecture, et comprendre pourquoi évite une dispute avec la spécification. [MS-CFB] permet un espace énorme de conteneurs valides : stockages multi-niveaux, arbres de répertoire rouge-noir correctement équilibrés, mini flux, chaînes DIFAT. Excel émet un petit coin de cet espace et en lit un un peu plus large. HotXLS écrit un coin plus petit encore — le minimum qu'Excel charge de façon démontrée. Chaque flux va sur la FAT normale sans chemin de mini flux, ce qui coûte de l'espace disque et achète la correction : un flux sommaire de 300 octets qu'Excel aurait empaqueté dans cinq mini-secteurs de 64 octets occupe à la place un secteur complet de 512 octets, et pour un classeur, c'est un bruit négligeable face au fait de maintenir une seconde table d'allocation, un second parcours de chaîne et le flux d'entrée racine qui les soutient sur le chemin d'écriture. Les entrées de répertoire forment une chaîne de frères plate sous la racine, chaque nœud étant coloré en noir, et l'ordre d'émission est fixe : espace réservé pour l'en-tête, secteurs de données de flux, secteurs de répertoire, secteurs FAT, puis un retour en arrière pour réécrire l'en-tête avec les identifiants de secteur qui ne sont connus qu'à la fin. La FAT se dimensionne elle-même via une courte boucle à point fixe, car ajouter des secteurs FAT peut faire grimper le nombre de secteurs suffisamment haut pour exiger un secteur FAT supplémentaire
procedure SaveAsCompoundFile(const Dest: string; const BiffBytes: TBytes);
var
FS: TFileStream;
Cfb: TlxCompoundFile;
begin
FS:= TFileStream.Create(Dest, fmCreate);
try
Cfb:= TlxCompoundFile.Create;
try
Cfb.CreateNew(FS); // v3 header, 512-byte sectors
Cfb.AddStream('Workbook', BiffBytes);
Cfb.Save; // data -> dir -> FAT -> header
finally
Cfb.Free;
end;
finally
FS.Free;
end;
end;
Où s'arrête l'implémentation
Trois limites méritent d'être énoncées clairement, car un lecteur de conteneur qui gère mal un cas limite en silence est pire qu'un lecteur qui lève une exception. TlxCompoundFile lit les 109 entrées DIFAT résidentes dans l'en-tête et ne suit pas la chaîne DIFAT à 0x44 au-delà, plafonnant un conteneur lisible à environ 6,8 Mio sur des secteurs de 512 octets — confortablement au-dessus des vrais fichiers .xls que HotXLS rencontre sur le terrain, mais un plafond dur néanmoins, et l'écrivain impose explicitement la même limite plutôt que d'émettre un conteneur qu'il ne pourrait pas décrire. Deuxièmement, les conteneurs version 4 avec des secteurs de 4096 octets sont pris en charge par l'arithmétique de taille de secteur mais ne sont pas ce pour quoi le code est optimisé, et la taille de flux 64 bits n'est pas consultée : HotXLS lit les 32 bits de poids faible à l'offset 0x78 et laisse la moitié haute de côté, ce qui est correct pour la version 3 et seulement pour elle. Troisièmement, la recherche d'entrée est un parcours plat par nom à travers la liste de répertoire plutôt qu'une descente dans l'arbre rouge-noir depuis un stockage parent, si bien que les stockages imbriqués se résolvent par collision de nom plutôt que par chemin — chaque flux dont un fichier .xls a besoin siège au niveau supérieur, ce qui rend la conception plus simple défendable, mais du code s'attendant à adresser SomeStorage/SomeStream ne le trouvera pas
Rien de tout cela ne change à quoi sert l'unité. Posséder la couche conteneur transforme la gestion des .xls en Object Pascal ordinaire : analysable depuis un tableau d'octets, testable sans système de fichiers, portable vers quelle que soit la plateforme visée par le compilateur, et libéré de tout appartement COM. Cela retire aussi les raccourcis de reniflage, car identifier un classeur signifie désormais lire son répertoire plutôt que ses huit premiers octets — la même discipline derrière le listage des noms de feuilles sans ouvrir tout le classeur
TlxCompoundFile fait partie du composant HotXLS Excel pour Delphi et C++Builder, aux côtés des couches BIFF et OOXML qui reposent dessus ; la page produit propose la référence complète de l'unité et la matrice des compilateurs pris en charge