HotXLS Excel Library Delphi ir C++Builder aplinkoms skaito ir rašo Compound File Binary konteinerį, esantį už kiekvieno senojo .xls failo, grynoje Object Pascal kalboje. TlxCompoundFile klasė realizuoja [MS-CFB] versijos 3 išdėstymą tiesiogiai prieš TStream — antraštė, DIFAT, FAT grandinės, MiniFAT ir katalogų medis — be jokio ole32.dll ir be jokio COM IStorage kelyje
Tai skamba kaip sanitarinė instaliacija, ir dvidešimt metų tai buvo kažkieno kito sanitarinė instaliacija. Kiekviena Delphi kodo bazė, palietusi .xls failą, siekdavo StgOpenStorage, gaudavo IStorage ir ištraukdavo Workbook srautą iš jo. Trys eilutės, veikė gerai, niekas apie tai daugiau negalvodavo — kol vieną dieną tam pačiam kodui teko veikti ten, kur nebuvo Windows
Kodėl StgOpenStorage nustoja veikti serveryje?
COM struktūrinės saugyklos API žlunga būtent tose diegimo formose, kuriose gyvena šiuolaikinis Delphi kodas, dėl priežasčių, neturinčių nieko bendra su failo formatu. StgOpenStorage yra Win32 įėjimo taškas ole32.dll viduje: jis nori kelio failų sistemoje, jis nori COM inicializuoto iškvietimo gijoje, ir jis nori būti Windows aplinkoje. Kelio reikalavimas skauda pirmiausia, nes REST galinis taškas, gaunantis įkeltą darbaknygę, turi baitus buferyje, ne diske — todėl rašote buferį į laikiną failą, jį atidarote, nuskaitote atgal, ištrinate, ir dabar turite laikino failo gyvavimo ciklą, kurį reikia klaidingai sutvarkyti apkrovoje. ILockBytes yra dokumentuota išeitis, tačiau pasirinktinės realizacijos sujungimas su TMemoryStream yra daugiau COM sąveikos, nei dauguma komandų nori. Inicializavimo reikalavimas kanda antra, dažniausiai paslaugos darbo gijoje, kurioje niekas nekvietė CoInitialize, o platformos reikalavimas baigia pokalbį akimirksniu, kai tikslas yra Linux po FPC, konteinerio atvaizdas ar macOS. HotXLS todėl palieka klasikinį lxOLE kelią, sukurtą ant StgOpenStorage, kaip numatytąjį, nes jis kovoje išbandytas, o esami iškvietėjai neturėtų turėti keistis; TlxCompoundFile yra pasirenkama alternatyva visiems kitiems
Ką iš tikrųjų sako antraštė ir FAT grandinės
Pirmieji 512 compound failo baitų atsako į kiekvieną struktūrinį klausimą, kurio reikia prieš skaitant baitą duomenų. [MS-CFB] §2.2 fiksuoja antraštės signatūrą poslinkyje 0 kaip aštuonis baitus D0 CF 11 E0 A1 B1 1A E1, o lxIsCompoundStream patikrina lygiai tai, atkurdama srauto poziciją po to, kad iškvietėjas galėtų uostyti, netrikdydamas nieko. Dar keturi laukai nulemia geometriją: baitų tvarka ties 0x1C turi būti 0xFFFE, kas dubliuoja pigų antrą signatūros patikrinimą; sektoriaus poslinkis ties 0x1E duoda sektoriaus dydį kaip 1 shl SectorShift, todėl versija 3 naudoja poslinkį 9 512 baitų sektoriams, o versija 4 naudoja poslinkį 12 4096 baitams; mini sektoriaus poslinkis ties 0x20 yra 6, todėl mini sektoriai yra 64 baitai; o mini srauto riba ties 0x38 yra 4096. Adreso aritmetika, sekanti toliau, yra dažniausia vieta, kur suklystama. Sektorius 0 prasideda iškart po antraštės, todėl sektorius N prasideda baito poslinkyje 512 + N * SectorSize — atkreipkite dėmesį į literalią 512, ne SectorSize. Versijos 3 faile abu identiški, ir klaida amžinai slepiasi; versijos 4 faile ji tyliai skaito neteisingą sektorių, todėl HotXLS tai laiko vienoje funkcijoje, SidToOffset
Compound failas yra FAT failų sistema failo viduje, todėl jo skaitymas reiškia surištų sektorių ID sąrašų vaikščiojimą, kur FAT[n] laiko ID, sekantį po sektoriaus n. Trys sentinelai baigia arba anotuoja grandinę — ENDOFCHAIN, FATSECT sektoriui, priklausančiam pačiai FAT, ir DIFSECT DIFAT sektoriui — ir visi trys skaitomi kaip neigiami ženklinti 32 bitų sveikieji skaičiai, kas išlaiko ciklo sąlygas paprastas. FAT radimui reikia dar vieno netiesiogiškumo: DIFAT yra sektorių ID masyvas, sakantis, kur gyvena FAT sektoriai, o pirmieji jo 109 įrašai sėdi antraštėje poslinkyje 0x4C. TlxCompoundFile eina per tuos 109, sustoja ties pirmu neigiamu įrašu ir sujungia kiekvieną FAT sektorių į vieną plokščią Integer masyvą. Tai 109 FAT sektoriai po 128 įrašų kiekvienas 512 baitų sektoriuje, taigi maždaug 13 952 adresuojami sektoriai, taigi apytiksliai 6,8 MiB konteinerio, prieš DIFAT turintis persipilti į savo grandinę
Antra paskirstymo lentelė egzistuoja, nes 512 baitų sektoriai iššvaisto didžiąją dalį savo vietos smulkiems srautams. Bet koks srautas žemiau 4096 baitų ribos apskritai nesaugomas sektoriuose: jis gyvena mini srauto viduje, pačiame paprastame sraute, kabančiame nuo šaknies katalogo įrašo, subdalintame į 64 baitų mini sektorius ir surištame per lygiagrečią MiniFAT, įsišaknijusią antraštės poslinkyje 0x3C. Atidarykite tikrą .xls, ir Workbook srautas sėdi ant įprastos FAT, o suvestinės informacijos srautai sėdi žemai mini-sektoriaus erdvėje, ir būtent todėl realizacija, apimanti tik FAT kelią, atrodo veikianti tol, kol jai prireikia dokumento metaduomenų. Katalogas yra trečia struktūra ir ta, kuri padaro konteinerį naršomu: kiekvienas įrašas yra lygiai 128 baitai, keturi 512 baitų sektoriuje, nešantis UTF-16 vardą pirmuose 64 baituose, jo baitų ilgį ties 0x40, objekto tipą ties 0x42 (1 = saugykla, 2 = srautas, 5 = šaknis), medžio nuorodas ties 0x44, 0x48 ir 0x4C, pradžios sektorių ties 0x74 ir 32 bitų srauto dydį ties 0x78. Tas vardo ilgis skaičiuoja baitus, įskaitant baigiamąjį nulį, todėl simbolių skaičius yra NameLen div 2 - 1, ir suklydus vienetu gaunate srautą, pavadintą Workboo
Workbook srauto ištraukimas iš atminties buferio
TlxCompoundFile.OpenStream paslepia visą aukščiau minėtą po vienu iškvietimu, kuris ima srauto vardą ir grąžina TlxCfbStream, laikantį pilnai materializuotus baitus. Visa seka — uostyti, įkelti, ištraukti — vykdoma prieš TBytesStream, niekam neliečiant disko
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;
Dvi ten esančios smulkmenos vertos paminėti. LoadFromStream ima AOwnsStream vėliavėlę, pagal numatymą False, todėl iškvietėjas išlaiko atsakomybę už šaltinio srautą — tyčia, nes įprastas atvejis yra srautas, kurį programa jau turi. O OpenStream grąžina TlxCfbStream, turintį savo baitų kopiją, atskleistą per Data, Size, Read, Seek ir CopyTo. Ta kopija yra reali kaina didelėje darbaknygėje, ir tai yra sąžininga dizaino kaina, kur grąžintas objektas lieka galiojantis po to, kai konteineris atlaisvinamas. Kai darbaknygė pakankamai didelė, kad pilna kopija atmintyje būtų visiškai neteisinga forma, geresnis įėjimo taškas yra srautinis tiesioginis skaitytuvas per didelėms skaičiuoklėms
Kodėl šifruotas XLSX atrodo kaip XLS failas?
Todėl, kad jis toks yra, konteinerio lygmenyje — ir tai yra praktinis atlygis už tos lygmens valdymą. Atidarykite šifruotą .xlsx šešioliktainiame redaktoriuje, ir pirmieji aštuoni baitai yra D0 CF 11 E0 A1 B1 1A E1, baitas baitan identiški 1997 metų .xls failui, nes [MS-OFFCRYPTO] šifravimas nešifruoja ZIP paketo vietoje: jis apvynioja visą paketą compound file konteineriu kaip srautą, pavadintą EncryptedPackage, šalia EncryptionInfo srauto, aprašančio algoritmą. Signatūra todėl identifikuoja konteinerį ir nieko nesako apie turinį. Atskirti BIFF darbaknygę nuo šifruoto OOXML paketo reiškia katalogo skaitymą, kas po LoadFromStream yra EntryCount ir Entries perėjimas, arba pora HasStream tyrimų
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;
Katalogo vardai nusipelno savo įspėjimo: suvestinės informacijos srautai neša pirmaujantį 0x05 valdymo simbolį savo varduose, todėl palyginimas, parašytas prieš paprastą rodomą eilutę, niekada su jais nesutaps, o naivi žurnalo eilutė juos atvaizduos kaip šiukšles. Viskas žemiau šios klasifikacijos — rakto išvedimas, slaptažodžio patikrintojo tikrinimas — yra atskira problema, aprašyta pastabose apie kodėl Excel atmeta darbaknygę, šifruotą neteisingu šifro režimu. Konteinerio lygmuo tik pasako, prie kurių durų stovite
Konteinerio rašymas, kurį Excel iš tikrųjų atidarys
Rašymo pusė TlxCompoundFile yra sąmoningai siauresnė nei skaitymo pusė, ir supratimas kodėl išgelbsti ginčą su specifikacija. [MS-CFB] leidžia didžiulę galiojančių konteinerių erdvę: kelių lygių saugyklas, teisingai subalansuotus raudonai-juodus katalogų medžius, mini srautus, DIFAT grandines. Excel išleidžia mažą tos erdvės kampą ir perskaito šiek tiek didesnę. HotXLS rašo dar mažesnį kampą — minimumą, kurį Excel demonstruojamai įkelia. Kiekvienas srautas eina ant įprastos FAT be jokio mini-srauto kelio, kas kainuoja disko vietą, bet perka teisingumą: 300 baitų suvestinės srautas, kurį Excel būtų supakavęs į penkis 64 baitų mini sektorius, vietoj to užima pilną 512 baitų sektorių, ir darbaknygei tai yra triukšmas, palyginti su antros paskirstymo lentelės, antros grandinės vaikščiojimo ir šaknies įrašo srauto palaikymu rašymo kelyje. Katalogo įrašai sudaro plokščią broli-sesers grandinę po šaknimi, kiekvienam mazgui nudažytam juodai, o išleidimo tvarka fiksuota: antraštės vietos rezervavimas, srauto duomenų sektoriai, katalogo sektoriai, FAT sektoriai, tada peršokimas atgal, kad perrašytų antraštę su sektorių ID, kurie žinomi tik pabaigoje. FAT nustato savo dydį per trumpą fiksuoto taško ciklą, nes FAT sektorių pridėjimas gali pastumti sektorių skaičių pakankamai aukštai, kad reikėtų dar vieno FAT sektoriaus
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;
Kur realizacija sustoja
Trys ribas verta aiškiai išsakyti, nes konteinerio skaitytuvas, tyliai netvarkantis kraštutinio atvejo, yra blogiau nei toks, kuris kelia klaidą. TlxCompoundFile skaito 109 DIFAT įrašus, esančius antraštėje, ir nesivelka į DIFAT grandinę ties 0x44 už jų ribų, apribodama skaitomą konteinerį apytiksliai 6,8 MiB su 512 baitų sektoriais — patogiai virš realių .xls failų, su kuriais HotXLS susiduria praktikoje, tačiau vis tiek griežta lubos, o rašytuvas užtikrina tą pačią ribą aiškiai, o ne išleidžia konteinerį, kurio negali aprašyti. Antra, versijos 4 konteineriai su 4096 baitų sektoriais yra priimami sektoriaus dydžio aritmetika, bet ne tai, kam kodas suderintas, o 64 bitų srauto dydis nekonsultuojamas: HotXLS skaito žemesnius 32 bitus poslinkyje 0x78 ir palieka viršutinę pusę ramybėje, kas teisinga versijai 3 ir tik jai. Trečia, įrašo paieška yra plokščia paieška pagal vardą per katalogo sąrašą, o ne vaikščiojimas žemyn raudonai-juodu medžiu nuo tėvinės saugyklos, todėl įdėtos saugyklos išsprendžiamos vardo susidūrimu, o ne keliu — kiekvienas srautas, kurio reikia .xls failui, sėdi viršutiniame lygmenyje, kas ir padaro paprastesnį dizainą pateisinamą, tačiau kodas, tikintis pasiekti SomeStorage/SomeStream, jo neras
Nė kas iš to nekeičia, kam vienetas skirtas. Konteinerio lygmens valdymas paverčia .xls tvarkymą įprastu Object Pascal: nagrinėjamu iš baitų masyvo, testuojamu be failų sistemos, perkeliamu į bet kurią platformą, kurią kompiliatorius taikosi, ir be COM apartamento. Tai taip pat pensijoje palieka uostymo trumpuosius kelius, nes darbaknygės identifikavimas dabar reiškia jos katalogo, o ne pirmų aštuonių baitų skaitymą — ta pati drausmė, kaip ir lapų vardų išvardinimas neatidarant visos darbaknygės
TlxCompoundFile pristatomas kaip HotXLS Excel Component dalis, skirta Delphi ir C++Builder, kartu su BIFF ir OOXML sluoksniais, esančiais virš jo; produkto puslapyje pateikiama pilna vieneto nuoroda ir palaikomų kompiliatorių matrica