Öppna en gammal xls, spara den igen, och tilläggsformeln som anropade ett registrerat analysbibliotek pekar nu på en tom referens inuti själva arbetsboken. HotXLS spårar den tysta korruptionen till ett enda dåligt antagande: att en BIFF SupBook-post antingen är self eller en extern fil. [MS-XLS] definierar sju sorter, inte två
Varför tappar en sparad arbetsbok sina tilläggslänkar?
Eftersom klassificeringstestet var strukturellt i stället för typat. Den traditionella genvägen läser en SupBook-post ($01AE), kontrollerar om den bär self-markören, och behandlar i annat fall vilken sträng som följer som en dokument-URL. Varje post som är inget av de två faller igenom till en default-gren, och default-grenen är nästan alltid ”detta är arbetsboken själv”. En tilläggslänk, en samma-ark-länk, en oanvänd plats och en trunkerad post hamnar alla med samma felaktiga etikett. Inget kastar medan detta pågår: posten parsades, formeln kompilerades om, filen sparades utan varning, och defekten dyker upp tre veckor senare när någon märker en kolumn av nollor där en valutaomvandling brukade vara. [MS-XLS] §2.4.271 beskriver en post som kan vara en självreferens, en samma-ark-referens, en tilläggsfunktionsbehållare, en extern arbetsbok med en virtuell sökväg och en arknamnstabell, en DDE- eller OLE-datalänk, eller en oanvänd platshållare — och ett sjunde tillstånd som inte finns i specifikationen men finns på riktiga diskar, posten som inte går att parsa. Fixen är inte en bättre heuristik; det är att vägra ha en heuristik alls
De sju sorter en SupBook-post kan bära
HotXLS deklarerar taxonomin för stödlänkar som en sluten uppräkning i lxExternSheet.pas, och varje nedströms beslut växlar på den. Nio uppräkningsvärden täcker de sju kategorierna, eftersom DDE- och OLE-fallet behöver ett preliminärt tillstånd innan det kan lösas:
type
TXLSSupportingLinkKind = (
slkUnknown, // misslyckades att parsas, eller efterföljande byte kvarstod
slkSelf, // denna arbetsbok
slkSameSheet, // U+0000-markör
slkAddIn, // tilläggsfunktionsbehållare
slkExternalWorkbook, // virtuell sökväg + arknamnstabell
slkDde, // löst från ExternName-flaggor
slkOle, // löst från ExternName-flaggor
slkDdeOrOle, // en av de två, ännu inte känt vilken
slkUnused); // platshållare med ett enda blanksteg
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // nollbaserat, som lagrat i ExternSheet.rgXTI
ExternID : Integer; // ettbaserat, den interna konventionen
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
Omdirigeringen är sentinelstyrd, inte strängstyrd. Ett fältvärde på $0401 markerar self-posten. Ett arkantal på ett parat med $3A01 markerar en tilläggsbehållare. Bara ett värde i intervallet 1 till $00FF betyder att en kodad virtuell sökväg följer, och först då avkodar HotXLS en sträng alls. Allt utanför dessa tre former förblir slkUnknown, och en post vars arknamnstabell inte konsumerar postkroppen exakt nedgraderas tillbaka till slkUnknown även när huvudet såg trovärdigt ut
Varför avkodas samma-ark-markören som en tom sträng?
Eftersom den generella BIFF-strängläsaren förstör den byte som klassificeringen är beroende av. Samma-ark-stödlänken är en sträng med ett enda tecken vars tecken är U+0000, och TXLSBlob.GetBiffString lämnar tillbaka det som en tom WideString, oskiljbar från en genuint tom sökväg — vilket är exakt den indata en självreferensheuristik svarar ”self” på. HotXLS läser därför den råa första kodpunkten ur postkroppen i stället för att lita på det avkodade värdet:
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) // komprimerad, en byte
else
FirstChar := Data.GetWord(StringOffset + 3); // bred, två byte
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;
Notera grenen komprimerad kontra bred. Optionsbyten sitter på en fast offset från strängheadern och den första kodpunkten är en byte eller två beroende på bit 0, så att läsa den som en byte ovillkorligt fungerar på de flesta filer och misslyckas på de som skrivits av lokaliserade byggen — den värsta tänkbara fördelningen för en bugg. Den oanvända platshållaren fångas på samma sätt, via sin bokstavliga nyttolast med ett enda blanksteg, och DDE- eller OLE-fallet via U+0003-separatorn inbäddad i den kodade sökvägen
Varför kan DDE och OLE inte skiljas åt vid SupBook-tillfället?
Eftersom SupBook-posten inte bär de särskiljande bitarna. Den talar om att länken är en av de två; flaggorna fOle och fOleLink som avgör vilken finns i ExternName-posten ($0023) som anländer senare i strömmen. HotXLS registrerar slkDdeOrOle vid parstillfället och preciserar det i ParseExternalName, och om inget ExternName någonsin anländer förblir sorten preliminär för evigt — vilket är korrekt, eftersom filen genuint inte säger det. Varje konsument nedströms behandlar det preliminära värdet som ett riktigt värde snarare än ett saknat, så ingen anropare behöver hitta på en skiljelinje. Att gissa ”förmodligen DDE” här skulle köpa en snyggare uppräkning och en klass av felaktiga svar som ingen kunde spåra tillbaka:
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;
XTI-index är nollbaserade på disk och ettbaserade internt
HotXLS utför off-by-one-konverteringen exakt en gång, i punkten där en token kommer in i det interna syntaxträdet, och ingen annanstans. PtgNameX.ixti ([MS-XLS] §2.5.198.85) är ett nollbaserat index i rgXTI-arrayen i ExternSheet-posten ($0017, §2.4.106), medan bibliotekets interna ExternID-konvention är ettbaserad med noll reserverad för ”inget externt ark”. BIFF8-läsvägen gör FExternID := wValue + 1 när den avkodar en tNameX-token och skrivvägen skriver ut StoreExternID - 1, vilket lämnar den råa tokenvyn och semantiken på disk orörd. Att få detta fel är ovanligt svårt att fånga: externa definierade namn löser till den intilliggande posten, och i en fil med en enda XTI-post blir index 0 till index 1, missar, och namnet degraderas tyst. En regression som bara tränar omkompilerad formeltext ser det aldrig, eftersom omkompilering aldrig rör diskindexet alls — samma fälla som gör definierade namn som spänner ark och arbetsböcker värda att testa mot riktiga byteströmmar. Upplösningen är avgränsad i båda ändarna: TlxExternSheetSheet.TryResolveXti returnerar False för ett negativt index eller en saknad post, TXLSSupBook.TryGetKind returnerar False för ett SupBook-index utanför arrayen, och ClassifyXti avbildar sedan slkSelf och slkSameSheet på frcInternal, slkExternalWorkbook på frcExternalWorkbook, och slkAddIn, slkDde, slkOle och slkDdeOrOle på frcExternalOther. Allt annat, inklusive varje väg utanför intervallet, landar på frcUnknownOrMalformed
Klassificera en formel innan den fryses
TXLSCompiledFormula.ClassifyReferences skannar den bevarade BIFF-tokenströmmen direkt i stället för att dekompilera formeln och söka efter hakparenteser. Hakparentesjakt i formeltext är en textheuristik i en parsers kappa: den matchar stränglitteraler, den matchar strukturerade referenser, och den missar externa definierade namn helt, eftersom dessa inte bär några hakparenteser i dekompilerad form. Tokenskanningen tittar bara på PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d och PtgAreaErr3d, och faller tillbaka på en syntaxträdsgenomgång när ingen BIFF-ström överlever. Sammanslagningen är avsiktligt pessimistisk — den fasta prioriteten är frcUnknownOrMalformed, sedan frcExternalWorkbook, sedan frcExternalOther, sedan frcInternal — så en enda oläsbar token förgiftar hela formeln. För ett externt definierat namn valideras också namnindexet: ettbaserat, inom intervallet, och backat av en bevarad ExternName-post
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 är ettbaserad
begin
Sheet := Wb.Sheets[i];
// fryser ENDAST formler klassificerade frcExternalWorkbook;
// interna, tilläggs-, DDE/OLE- och malformed-referenser förblir formler
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
Parametern OnlyExternal är där taxonomin tjänar in sig själv. Att frysa en formel är irreversibelt, så operationen måste bevisa att en referens är en extern arbetsbok snarare än att bara misstänka det. Tilläggsanrop överlever, DDE- och OLE-länkar överlever, och allt som parsern inte fullständigt kunde förstå överlever, eftersom det säkra utfallet av osäkerhet är att ändra ingenting. Samma disciplin styr omkoppling av formler kopierade mellan arbetsböcker, där en felklassificerad referens kopplas om till fel bok i stället för att misslyckas högljutt
Poster som inte går att parsa skrivs tillbaka orörda
HotXLS behåller den ursprungliga SupBook-nyttolasten och skriver ut den byte för byte när posten aldrig redigerades. Ett parsfel sätter slkUnknown och rensar det härledda tillståndet, men den fångade kroppen ligger kvar i FRawData och lagringsvägen föredrar den framför varje rekonstruktion så länge posten inte är smutsig och inte är self-posten. Alternativet — att normalisera en oparserad post till en självreferens så att skrivaren har något välformat att skriva ut — förvandlar en post du inte förstod till en post som definitivt är fel. Den principen är samma kontrakt som tillämpas på VBA-projekt och deras externa referenser över en ladda-och-spara-cykel, och det är skillnaden mellan ett bibliotek som round-trippar verkliga filer och ett som round-trippar de filer dess testsvit råkar innehålla. En arbetsbok som passerat genom femton år av Excel-versioner, en rapportgenerator och två migrationsverktyg kommer att innehålla poster som ingen nu levande designade. Skriv tillbaka dem som du hittade dem
Typad klassificering av SupBook- och XTI-poster skeppades i HotXLS 2.361.2 till 2.361.4, tillsammans med avgränsad XTI-upplösning och den säkrare ConvertFormulasToValues-väg som beskrivs här. Om du underhåller Delphi- eller C++Builder-kod som läser äldre xls-filer med tilläggsanrop, DDE- eller OLE-länkar eller externa definierade namn, hanterar HotXLS Delphi-kalkylarkskomponent hela taxonomin nativt, utan Excel-installation och utan OLE-automation på maskinen som utför arbetet