HotXLS avkodar en BIFF8-XLUnicodeString genom att först läsa cch och flaggan fHigh och sedan välja läsaren som passar kodningen: TXLSBlob.GetWideString med ett byteantal på cch * 2 när fHigh är 1 och TXLSBlob.GetString när fHigh är 0. Para ihop dem åt andra hållet och posten ger en tom eller halvlång sträng, aldrig ett undantag
Det är det som gör den här typen av fel dyr. Ett diagram öppnas, serierna ritas korrekt, axlarna är rätt och en text för en trendlinje är helt enkelt tom. Ingenting i loggen, ingenting i undantagshanteraren och ingen dialog om en skadad fil. Filen var korrekt hela tiden; läsaren bad om fel antal byte och fick exakt det den bad om
Varför kommer en BIFF8-sträng tillbaka tom?
En BIFF8-sträng kommer tillbaka tom eftersom en längdgrind avvisade nyttolasten innan läsningen ens började eller eftersom läsaren stannade vid det första NUL-tecknet den hittade. Båda vägarna är tysta av konstruktionen. I HotXLS är grinden vanligtvis en uttrycklig kontroll av DataLength i posthanteraren, och den måste beräknas per kodning: en 16-bitars nyttolast behöver 8 + cch * 2 byte för en SXViewLink-kropp, men en 8-bitars nyttolast behöver bara 8 + cch. Tillämpa bredteckenaritmetik på en 8-bitars post och varje kort namn misslyckas med kontrollen. NUL-beteendet är den andra fällan, eftersom både TXLSBlob.GetString och TXLSBlob.GetWideString söker efter en terminator i det avkodade resultatet och trunkerar där, och returnerar en tom sträng när terminatorn hamnar på position ett. Läs en 16-bitarskropp med halva byteantalet och du behåller de första cch div 2 tecknen; läs en 8-bitarskropp genom bredläsaren och byteparen bildar godtyckliga kodpunkter. Bara en för lång läsning låter högt: TXLSBlob.EnsureReadable ger Blob read exceeds data size när begäran går utanför blobben. Underläsning har inget sådant larm
GetWideString räknar byte, inte tecken
TXLSBlob.GetWideString(Index, Count) tar Count i byte. Internt gör den ett SetString över en PWideChar med Count div SizeOf(WideChar), så om du skickar ett teckenantal halverar du tyst strängen. BIFF8-postlayouterna uttrycker samtidigt stränglängd i tecken. Varje 16-bitarsanrop måste därför bära med sig omvandlingen * 2, och varje 8-bitarsanrop måste låta bli. Det här är samma kodningsgräns som dyker upp när du skriver ut text igen i stället för att läsa den, vilket är värt att läsa tillsammans med Unicode-säker kalkylbladsexport i Delphi om din pipeline flyttar strängar i båda riktningarna
// 16-bitars XLUnicodeStringNoCch: cch tecken, cch * 2 byte
Name := Data.GetWideString(Start, cch * 2); // korrekt
Name := Data.GetWideString(Start, cch); // halva texten, inget fel
// 8-bitars XLUnicodeStringNoCch: cch tecken, cch byte
Name := WideString(Data.GetString(Start, cch)); // korrekt
Name := Data.GetWideStringWithZero(Start, cch); // fortfarande en bredläsare
Konventionen gäller överallt där byteströmmen gås igenom för hand. När HotXLS fogar ihop en lång String-post ($0207, [MS-XLS] 2.4.268) från dess Continue-poster ($003C) beräknar den breda grenen segCh från segmentlängden och anropar sedan GetWideString(3, segCh * 2), eftersom postkroppen börjar vid offset 3 och antalet fortfarande är byte. Den rika textläsaren gör samma sak från offset 1 på det första Continue-segmentet. [MS-XLS] 2.5.293 garanterar att brytningen ligger på en dubbelbyte-teckengräns när fHighByte är 1, så ingen bokföring av deltecken behövs, men bytearitmetiken är fortfarande din att få rätt
Vad gör GetWideStringWithZero egentligen?
TXLSBlob.GetWideStringWithZero är en bredteckenläsare som behåller inbäddade NUL-tecken. Suffixet WithZero markerar att NUL behålls, inte teckenbredden: internt kör den samma SetString över en PWideChar med Count div SizeOf(WideChar) som GetWideString, minus terminatorsökningen. Motsvarigheten för en byte är TXLSBlob.GetStringWithZero, som returnerar en AnsiString. Ingenting i namnet säger vilken som är vilken, och den tvetydigheten har kostat den här kodbasen riktiga buggar. Den specifika felläsningen är värd att namnge eftersom den ser så rimlig ut: GetWideString behöver cch * 2, så GetWideStringWithZero måste vara den som tar cch direkt. Den accepterar verkligen cch utan klagomål, returnerar verkligen en WideString och kompilatorn är nöjd. Den returnerar också hälften av tecknen, sammansatta från fel bytepar. Den korrekta 8-bitarsvägen är TXLSBlob.GetString med ett vanligt byteantal cch, kastat till WideString vid tilldelningen. HotXLS 2.376.0 rättade exakt denna felanvändning i två diagramavkodare
SXViewLink och längdgrinden per kodning
SXViewLink ($0858, [MS-XLS] 2.4.316) är det renaste genomarbetade exemplet eftersom den packar båda asymmetrierna i ett åttabytehuvud. Layouten är rt(2), unused(2), reserved(2), cch(1), fHigh(1), följt av en XLUnicodeStringNoCch-kropp: fHigh = 1 betyder cch * 2 byte UTF-16, fHigh = 0 betyder cch byte enbyte-tecken och cch är begränsat till 255 eftersom längdfältet är en enda byte. HotXLS skriver posten i diagrammets globala del före Units, bredvid PivotChartBits ($0859, [MS-XLS] 2.4.196), när ett diagramblad länkar till en PivotTable-vy; postnivåbilden av det maskineriet täcks i att skriva BIFF8-PivotTable-poster i Delphi
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// sedan en XLUnicodeStringNoCch - fHigh(1) följt av tecknen
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;
De två grenarna är inte kosmetiska. En tidigare version använde det bredteckenbaserade uttrycket 8 + cch * 2 som grind för båda kodningarna, så ett Excel-skrivet 8-bitarsvynamn misslyckades med kontrollen och avkodaren returnerade ett tomt PivotSourceName medan IsPivotChart förblev falskt. Pivotlänken försvann ur modellen utan en enda diagnostik. Samma misstag fanns i trendlinjeavkodaren ($2050, [MS-XLS] 2.4.328), där namnfältet följer 28 byte numerisk nyttolast med ett tvåbyte-cch på offset 28, fHigh på offset 30 och tecknen på offset 31; trendlinjetexter som Excel hade skrivit med 8-bitars tecken avkodades som tomma strängar. Båda rättades i samma version. Och 8-bitarsfallet är inte en historisk kuriositet begränsad till Excel-filer från 2.0 till 4.0: dagens Excel skriver fortfarande 8-bitars BIFF8-nyttolaster när varje tecken ryms i en byte
Hur avkodar du en ny BIFF8-post säkert i Delphi?
Där fältet verkligen är en standard-XLUnicodeString ska du använda TXLSBlob.GetBiffString i stället för att skriva grenen för hand. Den läser längdfältet, läser alternativbyten, skickar till rätt läsare och flyttar markören förbi kroppen. De två booleska parametrarna är den del som måste läsas noga: is8bit beskriver bredden på längdfältet, inte tecknens bredd, och iswide säger om en alternativbyte fHigh alls finns efter längdfältet. BIFF-versioner under $0600 har inget av detta
var
Offset: LongWord;
begin
Offset := 6; // i SXViewLink börjar cch-byten här
// is8bit = längdfältet är en byte brett
// iswide = en fHigh-alternativbyte följer efter längdfältet
Name := Data.GetBiffString(Offset, True, True);
// Offset pekar nu på den första byten efter strängkroppen
Handskrivna grenar har fortfarande sin plats när hanteraren måste överleva avklippt eller fientlig indata, eftersom GetBiffString förlitar sig på att EnsureReadable ger ett undantag i stället för på en gränskontroll du styr. Därför kontrollerar HotXLS-diagramavkodaren DataLength och returnerar ingenting i stället för att kasta: en felaktig arbetsbok från tredje part bör kosta dig en bildtext, inte hela dokumentet. Avvägningen är avsiktlig, och det är exakt varför den kodningsspecifika grinden måste vara rätt, eftersom det är grinden som omvandlar en dålig läsning till tystnad
En sista processdetalj, lärd på det hårda sättet i samma version. Hävdar mot en sparad och återöppnad arbetsbok, inte mot minnesmodellen du nyss byggde. 2.376.0-batchningen hittade också en SXEx-emitterare ([MS-XLS] 2.4.282) som deklarerade en kropp på 24 byte och bara skrev 22, vilket feljusterade varje post efter PivotTable-vyn, inklusive kalkylbladets EOF och varje följande diagrambladsunderström. De befintliga pivottesterna fångade det aldrig eftersom de alla hävdade mot minnet. Strängavkodning har samma egenskap: en tur och retur genom filen är det enda test som verkligen utövar byteantalen
Om du arbetar med klassiska XLS-interna format i Delphi eller C++Builder och hellre slipper underhålla en egen BIFF8-postläsare är kodningsreglerna ovan redan implementerade och regressionstestade i HotXLS Delphi-kalkylbladskomponenten, som läser och skriver XLS och XLSX utan Excel eller någon OLE-automatisering