HotXLS Excel Library for Delphi og C++Builder leser og skriver Compound File Binary-beholderen bak hver eneste eldre .xls-fil i ren Object Pascal. TlxCompoundFile-klassen implementerer [MS-CFB] versjon 3-oppsettet direkte mot en TStream — header, DIFAT, FAT-kjeder, MiniFAT og katalogtreet — uten ole32.dll og uten COM IStorage noe sted i veien
Det høres ut som rørlegging, og i tjue år var det rørlegging noen andre eide. Hver Delphi-kodebase som rørte en .xls-fil grep til StgOpenStorage, fikk tilbake en IStorage, og trakk ut Workbook-strømmen fra den. Tre linjer, fungerte fint, ingen tenkte på det igjen — helt til dagen samme kode måtte kjøre et sted Windows ikke var
Hvorfor slutter StgOpenStorage å fungere på en server?
COM structured-storage-API-et feiler nøyaktig i de utrullingsformene moderne Delphi-kode lever i, av grunner som ikke har noe med filformatet å gjøre. StgOpenStorage er et Win32-inngangspunkt i ole32.dll: den vil ha en sti på et filsystem, den vil ha COM initialisert på den kallende tråden, og den vil være på Windows. Sti-kravet gjør vondt først, fordi et REST-endepunkt som mottar en opplastet arbeidsbok har bytene i en buffer, ikke på disk — så du skriver bufferen til en midlertidig fil, åpner den, leser den tilbake, sletter den, og eier nå en midlertidig-fil-livssyklus å få galt under last. ILockBytes er den dokumenterte rømningsveien, men å koble opp en egendefinert implementasjon over en TMemoryStream er mer COM-interop enn de fleste team vil ha. Initialiseringskravet biter for det andre, vanligvis i en tjeneste-arbeidertråd ingen kalte CoInitialize på, og plattformkravet avslutter samtalen i det øyeblikket målet er Linux under FPC, et containerbilde, eller macOS. HotXLS beholder derfor den klassiske lxOLE-veien bygget på StgOpenStorage som standard, siden den er kamptestet og eksisterende kallere ikke skal måtte endre seg; TlxCompoundFile er det valgfrie alternativet for alle andre
Hva header og FAT-kjedene faktisk sier
De første 512 bytene i en compound-fil svarer på hvert strukturelt spørsmål du trenger før du leser en byte nyttelast. [MS-CFB] §2.2 fester header-signaturen ved offset 0 som de åtte bytene D0 CF 11 E0 A1 B1 1A E1, og lxIsCompoundStream sjekker akkurat det, og gjenoppretter strømposisjonen etterpå slik at en kaller kan snuse uten å forstyrre noe. Fire felter til avgjør geometrien: byterekkefølge ved 0x1C må være 0xFFFE, som fungerer som en billig andre signatursjekk; sektorskift ved 0x1E gir sektorstørrelsen som 1 shl SectorShift, så versjon 3 bruker skift 9 for 512-byte sektorer og versjon 4 bruker skift 12 for 4096; mini-sektorskift ved 0x20 er 6, som gjør mini-sektorer 64 byte; og mini-strøm-terskelen ved 0x38 er 4096. Adressearitmetikken som følger er stedet der det oftest går galt. Sektor 0 begynner umiddelbart etter headeren, så sektor N starter ved byteoffset 512 + N * SectorSize — legg merke til det literale 512, ikke SectorSize. På en versjon 3-fil er de to identiske og feilen gjemmer seg for alltid; på en versjon 4-fil leser den stille feil sektor, og det er derfor HotXLS holder dette i én funksjon, SidToOffset
En compound-fil er et FAT-filsystem inne i en fil, så å lese den betyr å gå gjennom lenkede lister av sektor-ID-er der FAT[n] holder ID-en som følger sektor n. Tre sentinelverdier avslutter eller merker en kjede — ENDOFCHAIN, FATSECT for en sektor som tilhører selve FAT-en, og DIFSECT for en DIFAT-sektor — og alle tre leses som negative signerte 32-bits heltall, noe som holder løkkebetingelsene enkle. Å finne FAT-en trenger én indireksjon til: DIFAT-en er arrayen av sektor-ID-er som sier hvor FAT-sektorene bor, og de første 109 oppføringene sitter i headeren ved offset 0x4C. TlxCompoundFile går gjennom de 109, stopper ved den første negative oppføringen, og sammenkjeder hver FAT-sektor til én flat Integer-array. Det er 109 FAT-sektorer med 128 oppføringer hver på en 512-byte sektor, så 13 952 adresserbare sektorer, så omtrent 6,8 MiB beholder før DIFAT-en må søle over i sin egen kjede
Den andre allokeringstabellen finnes fordi 512-byte sektorer sløser bort det meste av plassen sin på små strømmer. Enhver strøm under 4096-byte-terskelen lagres ikke i sektorer i det hele tatt: den bor inne i mini-strømmen, selv en ordinær strøm som henger av rot-katalogoppføringen, delt opp i 64-byte mini-sektorer og kjedet gjennom en parallell MiniFAT forankret ved header-offset 0x3C. Åpne en ekte .xls, og Workbook-strømmen sitter på den vanlige FAT-en mens sammendrag-informasjon-strømmene sitter nede i mini-sektor-rommet, og det er derfor en implementasjon som bare dekker FAT-veien ser ut til å fungere helt til den trenger dokumentmetadata. Katalogen er den tredje strukturen og den som gjør beholderen navigerbar: hver oppføring er nøyaktig 128 byte, fire per 512-byte sektor, som bærer et UTF-16-navn i de første 64 bytene, byte-lengden sin ved 0x40, objekttypen ved 0x42 (1 = storage, 2 = strøm, 5 = rot), tre-lenker ved 0x44, 0x48 og 0x4C, startsektoren ved 0x74 og 32-bits strømstørrelsen ved 0x78. Den navnelengden teller byte inkludert den avsluttende null-verdien, så tegnantallet er NameLen div 2 - 1, og å få det av med én er hvordan du ender opp med en strøm ved navn Workboo
Å trekke en Workbook-strøm ut av en minnebuffer
TlxCompoundFile.OpenStream skjuler alt over bak ett kall som tar et strømnavn og returnerer en TlxCfbStream som holder de fullt materialiserte bytene. Hele sekvensen — snuse, laste, trekke ut — kjører mot en TBytesStream uten at noe noensinne rører disk
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;
To detaljer der er verdt å trekke frem. LoadFromStream tar et AOwnsStream-flagg som som standard er False, så kalleren beholder ansvaret for kildestrømmen — bevisst, fordi det vanlige tilfellet er en strøm applikasjonen allerede eier. Og OpenStream returnerer en TlxCfbStream som eier sin egen kopi av bytene, eksponert gjennom Data, Size, Read, Seek og CopyTo. Den kopien er en reell kostnad på en stor arbeidsbok, og det er den ærlige prisen for et design der det returnerte objektet forblir gyldig etter at beholderen er frigjort. Når en arbeidsbok er stor nok til at en full kopi i minnet er helt feil form, er den strømmende direkte leseren for oversized regneark det bedre inngangspunktet
Hvorfor ser en kryptert XLSX ut som en XLS-fil?
Fordi den er én, på beholdernivå — og dette er den praktiske gevinsten ved å eie det laget. Åpne en kryptert .xlsx i en heksadesimal-editor, og de første åtte bytene er D0 CF 11 E0 A1 B1 1A E1, byte for byte identisk med en 1997-årgangs .xls, fordi [MS-OFFCRYPTO]-kryptering ikke krypterer ZIP-pakken på stedet: den pakker hele pakken inn i en CFB-beholder som en strøm ved navn EncryptedPackage, ved siden av en EncryptionInfo-strøm som beskriver algoritmen. Signaturen identifiserer derfor beholderen og sier ingenting om nyttelasten. Å skille en BIFF-arbeidsbok fra en kryptert OOXML-pakke betyr å lese katalogen, som etter LoadFromStream er en skanning over EntryCount og Entries, eller et par HasStream-prober
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;
Katalognavn fortjener en advarsel for seg selv: sammendrag-informasjon-strømmene bærer et ledende 0x05-kontrolltegn i navnene sine, så en sammenligning skrevet mot en vanlig visningsstreng vil aldri matche dem, og en naiv loggelinje rendrer dem som søppel. Alt nedstrøms for denne klassifiseringen — å utlede nøkkelen, sjekke passordverifikatoren — er et separat problem, dekket i notatene om hvorfor Excel avviser en arbeidsbok kryptert med feil chiffermodus. Beholderlaget forteller deg bare hvilken dør du står foran
Å skrive en beholder Excel faktisk vil åpne
Skrivesiden av TlxCompoundFile er bevisst smalere enn lesesiden, og å forstå hvorfor sparer deg en krangel med spesifikasjonen. [MS-CFB] tillater et enormt rom av gyldige beholdere: flernivå-storages, korrekt balanserte rød-svarte katalogtrær, mini-strømmer, DIFAT-kjeder. Excel utsteder et lite hjørne av det rommet og leser et noe større. HotXLS skriver et hjørne enda mindre — det minimumet Excel demonstrerbart laster. Hver strøm går på den vanlige FAT-en uten mini-strøm-vei, noe som koster diskplass og kjøper korrekthet: en 300-byte sammendrag-strøm Excel ville pakket inn i fem 64-byte mini-sektorer opptar i stedet en full 512-byte sektor, og for en arbeidsbok er det støy ved siden av å vedlikeholde en andre allokeringstabell, en andre kjedegjennomgang og rot-oppføring-strømmen som støtter den på skrivesiden. Katalogoppføringer danner en flat søskenkjede under roten med hver node farget svart, og utstedelsesrekkefølgen er fast: header-plassholder, strømdata-sektorer, katalogsektorer, FAT-sektorer, deretter en seek tilbake for å skrive om headeren med sektor-ID-ene som først er kjent på slutten. FAT-en dimensjonerer seg selv gjennom en kort fikspunkt-løkke, fordi å legge til FAT-sektorer kan presse sektorantallet høyt nok til å kreve enda en FAT-sektor
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;
Hvor implementasjonen stopper
Tre grenser er verdt å si rett ut, fordi en beholderleser som stille feilhåndterer et grensetilfelle er verre enn en som varsler. TlxCompoundFile leser de 109 DIFAT-oppføringene som bor i headeren og følger ikke DIFAT-kjeden ved 0x44 utover dem, noe som setter et lesbart-beholder-tak på omtrent 6,8 MiB på 512-byte sektorer — komfortabelt over de ekte .xls-filene HotXLS møter i praksis, men et hardt tak likevel, og skriveren håndhever samme grense eksplisitt fremfor å utstede en beholder den ikke kan beskrive. For det andre er versjon 4-beholdere med 4096-byte sektorer akkommodert av sektorstørrelse-aritmetikken, men er ikke det koden er innstilt for, og 64-bits strømstørrelsen konsulteres ikke: HotXLS leser de lave 32 bitene ved offset 0x78 og lar den øvre halvdelen være i fred, noe som er korrekt for versjon 3 og bare for versjon 3. For det tredje er oppføringsoppslag en flat skanning etter navn på tvers av kataloglisten fremfor en gjennomgang nedover det rød-svarte treet fra en foreldre-storage, så nøstede storages løses opp etter navnekollisjon fremfor etter sti — hver strøm en .xls-fil trenger sitter på toppnivå, som er det som gjør det enklere designet forsvarlig, men kode som forventer å adressere SomeStorage/SomeStream vil ikke finne den
Ingenting av det endrer hva enheten er til for. Å eie beholderlaget gjør .xls-håndtering om til ordinær Object Pascal: parsbar fra en bytearray, testbar uten filsystem, portabel til hvilken plattform kompilatoren enn måtte målrette, og fri for et COM-apartment. Det pensjonerer også snuse-snarveiene, fordi å identifisere en arbeidsbok nå betyr å lese katalogen dens fremfor de første åtte bytene — samme disiplin bak å liste arknavn uten å åpne hele arbeidsboken
TlxCompoundFile leveres som en del av HotXLS Excel Component for Delphi og C++Builder, sammen med BIFF- og OOXML-lagene som sitter oppå det; produktsiden inneholder den fullstendige enhetsreferansen og den støttede kompilatormatrisen