Techninis straipsnis

OLE2 Compound Files skaitymas Delphi be COM IStorage

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