Article technique

Décoder XLUnicodeString BIFF8 dans Delphi : cch et fHigh

HotXLS décode une XLUnicodeString BIFF8 en lisant d’abord cch et le flag fHigh, puis en choisissant le lecteur correspondant à l’encodage : TXLSBlob.GetWideString avec un nombre d’octets égal à cch * 2 lorsque fHigh vaut 1, et TXLSBlob.GetString lorsque fHigh vaut 0. Associez-les dans l’autre sens et l’enregistrement fournit une chaîne vide ou de longueur réduite de moitié, sans jamais lever d’exception

C’est ce qui rend cette catégorie de bug coûteuse. Un graphique s’ouvre, les séries se dessinent correctement, les axes sont justes et une légende de courbe de tendance est simplement vide. Rien dans le journal, rien dans le gestionnaire d’exceptions, aucune boîte de dialogue de fichier corrompu. Le fichier était correct tout le temps ; le lecteur a demandé le mauvais nombre d’octets et a obtenu exactement ce qu’il demandait

Pourquoi une chaîne BIFF8 revient-elle vide ?

Une chaîne BIFF8 revient vide parce qu’un garde de longueur a rejeté la charge avant même la lecture, ou parce que le lecteur s’est arrêté au premier NUL rencontré. Les deux chemins sont silencieux par construction. Dans HotXLS, le garde est généralement un contrôle explicite de DataLength dans le gestionnaire d’enregistrement, et il doit être calculé par encodage : une charge 16 bits nécessite 8 + cch * 2 octets pour un corps SXViewLink, tandis qu’une charge 8 bits n’en nécessite que 8 + cch. Appliquez l’arithmétique des caractères larges à un enregistrement 8 bits et chaque nom court échoue au garde. Le comportement NUL est le deuxième piège, car TXLSBlob.GetString et TXLSBlob.GetWideString recherchent tous deux un terminateur dans le résultat décodé et tronquent à cet endroit, en renvoyant une chaîne vide lorsque le terminateur tombe en position un. Lisez un corps 16 bits avec la moitié du nombre d’octets et vous conservez les cch div 2 premiers caractères ; lisez un corps 8 bits via le lecteur wide et les paires d’octets forment des points de code arbitraires. Seule une lecture trop longue est bruyante : TXLSBlob.EnsureReadable lève Blob read exceeds data size lorsque la demande dépasse la taille du blob. Une lecture trop courte ne déclenche aucune alarme de ce type

GetWideString compte les octets, pas les caractères

TXLSBlob.GetWideString(Index, Count) prend Count en octets. En interne, il effectue un SetString sur un PWideChar avec Count div SizeOf(WideChar), de sorte que passer un nombre de caractères réduit silencieusement la chaîne de moitié. Les dispositions d’enregistrement BIFF8 expriment quant à elles la longueur en caractères. Chaque point d’appel 16 bits doit donc porter lui-même la conversion * 2, et aucun point d’appel 8 bits ne doit la porter. C’est la même frontière d’encodage que celle rencontrée lorsque vous réécrivez le texte plutôt que de le lire, ce qui mérite une lecture parallèle de l’export tableur Unicode fiable dans Delphi si votre pipeline déplace les chaînes dans les deux sens

// XLUnicodeStringNoCch 16 bits : cch caractères, cch * 2 octets
Name := Data.GetWideString(Start, cch * 2);        // correct
Name := Data.GetWideString(Start, cch);            // moitié du texte, sans erreur

// XLUnicodeStringNoCch 8 bits : cch caractères, cch octets
Name := WideString(Data.GetString(Start, cch));    // correct
Name := Data.GetWideStringWithZero(Start, cch);    // toujours un lecteur wide

La convention tient partout où le flux d’octets est parcouru à la main. Lorsque HotXLS reconstitue un long enregistrement String ($0207, [MS-XLS] 2.4.268) à partir de ses enregistrements Continue ($003C), la branche wide calcule segCh à partir de la longueur du segment puis appelle GetWideString(3, segCh * 2), car le corps de l’enregistrement commence à l’offset 3 et le compte reste exprimé en octets. Le lecteur de texte enrichi fait la même chose à partir de l’offset 1 sur le premier segment Continue. [MS-XLS] 2.5.293 garantit que la coupure tombe sur une frontière de caractère double-octet lorsque fHighByte vaut 1 ; aucun suivi de caractère partiel n’est donc nécessaire, mais l’arithmétique des octets reste à votre charge

Que fait réellement GetWideStringWithZero ?

TXLSBlob.GetWideStringWithZero est un lecteur de caractères larges qui conserve les NUL intégrés. Le suffixe WithZero indique la conservation des NUL, pas la largeur des caractères : en interne, il exécute le même SetString sur un PWideChar avec Count div SizeOf(WideChar) que GetWideString, mais sans rechercher le terminateur. Son équivalent sur un octet est TXLSBlob.GetStringWithZero, qui renvoie un AnsiString. Rien dans le nom n’indique lequel est lequel, et cette ambiguïté a causé de vrais bugs dans ce code. L’erreur précise mérite d’être nommée parce qu’elle semble si plausible : GetWideString nécessite cch * 2, donc GetWideStringWithZero doit être celui qui prend directement cch. Il accepte effectivement cch sans protester, renvoie bien une WideString et le compilateur est satisfait. Il renvoie aussi la moitié des caractères, assemblés à partir des mauvaises paires d’octets. Le bon chemin 8 bits est TXLSBlob.GetString avec un simple compte d’octets cch, converti en WideString lors de l’affectation. HotXLS 2.376.0 a corrigé précisément cette mauvaise utilisation dans deux décodeurs de graphiques

SXViewLink et le garde de longueur par encodage

SXViewLink ($0858, [MS-XLS] 2.4.316) est l’exemple idéal, car il réunit les deux asymétries dans un en-tête de huit octets. La disposition est rt(2), unused(2), reserved(2), cch(1), fHigh(1), suivie d’un corps XLUnicodeStringNoCch : fHigh = 1 signifie cch * 2 octets d’UTF-16, fHigh = 0 signifie cch octets de caractères sur un octet et cch est plafonné à 255, car le champ de longueur tient sur un seul octet. HotXLS écrit l’enregistrement dans les globales du graphique avant Units, à côté de PivotChartBits ($0859, [MS-XLS] 2.4.196), lorsqu’une feuille de graphique est liée à une vue PivotTable ; la vue au niveau des enregistrements de ce mécanisme est couverte dans l’écriture des enregistrements PivotTable BIFF8 dans Delphi

// SXViewLink ([MS-XLS] 2.4.316) : rt(2) unused(2) reserved(2) cch(1)
// puis un XLUnicodeStringNoCch - fHigh(1) suivi des caractères
PivCch := Item.FData.GetByte(6);
if (PivCch > 0) and (Item.FData.GetByte(7) <> 0) and
   (Item.FData.DataLength >= LongWord(8 + PivCch * 2)) then
begin
  Result.PivotSourceName := Item.FData.GetWideString(8, PivCch * 2);
  Result.IsPivotChart := True;
end
else if (PivCch > 0) and (Item.FData.GetByte(7) = 0) and
   (Item.FData.DataLength >= LongWord(8 + PivCch)) then
begin
  Result.PivotSourceName := WideString(Item.FData.GetString(8, PivCch));
  Result.IsPivotChart := True;
end;

Les deux branches ne sont pas décoratives. Une version antérieure plaçait les deux encodages derrière l’expression de caractères larges 8 + cch * 2, si bien qu’un nom de vue 8 bits écrit par Excel échouait au garde et que le décodeur renvoyait un PivotSourceName vide avec IsPivotChart laissé à false. Le lien du pivot disparaissait du modèle sans un seul diagnostic. La même erreur se trouvait dans le décodeur Trendline ($2050, [MS-XLS] 2.4.328), où le champ de nom suit 28 octets de charge numérique, avec un cch de deux octets à l’offset 28, fHigh à l’offset 30 et les caractères à l’offset 31 ; les légendes de courbe de tendance écrites par Excel avec des caractères 8 bits étaient décodées en chaînes vides. Les deux corrections sont arrivées dans la même version. Et le cas 8 bits n’est pas une curiosité héritée limitée aux fichiers Excel 2.0 à 4.0 : Excel actuel écrit encore des charges utiles BIFF8 sur un octet dès que chaque caractère tient sur un octet

Comment décoder sans risque un nouvel enregistrement BIFF8 dans Delphi ?

Lorsque le champ est réellement une XLUnicodeString standard, utilisez TXLSBlob.GetBiffString plutôt que de réécrire la branche à la main. La méthode lit le champ de longueur, lit l’octet d’option, appelle le lecteur correspondant et avance le curseur au-delà du corps. Les deux paramètres booléens sont ceux qu’il faut lire attentivement : is8bit décrit la largeur du champ de longueur, pas celle des caractères, et iswide indique si un octet d’option fHigh est présent ou non. Les versions BIFF antérieures à $0600 n’ont ni l’un ni l’autre

var
  Offset: LongWord;
begin
  Offset := 6;  // dans SXViewLink, l’octet cch commence ici
  // is8bit = le champ de longueur fait un octet
  // iswide = un octet d’option fHigh suit le champ de longueur
  Name := Data.GetBiffString(Offset, True, True);
  // Offset pointe maintenant sur le premier octet qui suit le corps de la chaîne

Les branches écrites à la main gardent leur place lorsque le gestionnaire doit résister à une entrée tronquée ou hostile, car GetBiffString s’appuie sur EnsureReadable pour lever une exception plutôt que sur un contrôle de limites que vous maîtriseriez. C’est pourquoi le décodeur de graphiques HotXLS contrôle DataLength et ne renvoie rien au lieu de lever une exception : un classeur tiers malformé doit vous coûter une légende, pas le document entier. Le compromis est délibéré, et c’est précisément pourquoi le garde spécifique à l’encodage doit être juste, puisqu’il transforme une mauvaise lecture en silence

Dernier élément de méthode, appris à la dure dans la même version. Faites une assertion sur un classeur sauvegardé puis rouvert, pas sur le modèle en mémoire que vous venez de construire. Le lot 2.376.0 a également révélé un émetteur SXEx ([MS-XLS] 2.4.282) qui déclarait un corps de 24 octets et n’en écrivait que 22, désalignant chaque enregistrement situé après la vue PivotTable, y compris l’EOF de la feuille et tout sous-flux de feuille de graphique suivant. Les tests de pivot existants ne l’avaient jamais détecté parce qu’ils vérifiaient tous la mémoire. Le décodage des chaînes possède la même propriété : seul un aller-retour par fichier exerce réellement les comptes d’octets

Si vous travaillez avec les entrailles XLS classiques dans Delphi ou C++Builder et préférez ne pas maintenir votre propre lecteur d’enregistrements BIFF8, les règles d’encodage ci-dessus sont déjà implémentées et couvertes par des régressions dans le composant tableur HotXLS pour Delphi, qui lit et écrit XLS et XLSX sans Excel ni automatisation OLE