Teknik Makale

Delphi'de COM IStorage Olmadan OLE2 Compound File Okuma

Delphi ve C++Builder için HotXLS Excel Library, her eski .xls dosyasının arkasındaki Compound File Binary konteynerini saf Object Pascal'da okur ve yazar. TlxCompoundFile sınıfı, [MS-CFB] sürüm 3 düzenini doğrudan bir TStream'e karşı uygular — başlık, DIFAT, FAT zincirleri, MiniFAT ve dizin ağacı — yolda hiçbir yerde ole32.dll ve COM IStorage olmadan

Bu tesisat gibi geliyor ve yirmi yıl boyunca başkasının sahip olduğu bir tesisat oldu. Bir .xls dosyasına dokunan her Delphi kod tabanı StgOpenStorage'a uzandı, bir IStorage geri aldı ve Workbook akışını içinden çekti. Üç satır, sorunsuz çalıştı, kimse bir daha bunu düşünmedi — aynı kodun Windows olmayan bir yerde çalışması gerektiği güne kadar

StgOpenStorage bir sunucuda neden çalışmayı durdurur?

COM structured-storage API'si, dosya biçimiyle hiçbir ilgisi olmayan nedenlerle, modern Delphi kodunun yaşadığı tam olarak dağıtım şekillerinde başarısız olur. StgOpenStorage, ole32.dll içinde bir Win32 giriş noktasıdır: bir dosya sisteminde bir yol ister, çağıran iş parçacığında COM'un başlatılmış olmasını ister ve Windows üzerinde olmasını ister. Yol gereksinimi ilk önce zarar verir, çünkü yüklenen bir çalışma kitabını alan bir REST uç noktası, baytları diskte değil bir tamponda tutar — bu yüzden tamponu bir geçici dosyaya yazarsınız, açarsınız, geri okursunuz, silersiniz ve şimdi yük altında yanlış gidecek bir geçici-dosya yaşam döngüsüne sahip olursunuz. ILockBytes, belgelenmiş kaçış kapısıdır, ama özel bir uygulamayı bir TMemoryStream üzerine kablolamak çoğu ekibin istediğinden daha fazla COM interop'udur. Başlatma gereksinimi ikinci olarak ısırır, genellikle kimsenin CoInitialize çağırmadığı bir servis işçi iş parçacığında, ve platform gereksinimi hedef FPC altında Linux, bir konteyner imajı ya da macOS olduğu anda konuşmayı bitirir. HotXLS bu yüzden StgOpenStorage üzerine kurulu klasik lxOLE yolunu varsayılan olarak tutar, çünkü savaş-testinden geçmiştir ve mevcut çağıranların değişmesi gerekmemelidir; TlxCompoundFile, diğer herkes için opt-in alternatiftir

Başlık ve FAT zincirleri gerçekte ne söylüyor

Bir compound dosyasının ilk 512 baytı, yük baytlarından birini okumadan önce ihtiyacınız olan her yapısal soruyu yanıtlar. [MS-CFB] §2.2, offset 0'daki başlık imzasını sekiz bayt D0 CF 11 E0 A1 B1 1A E1 olarak sabitler ve lxIsCompoundStream tam olarak bunu kontrol eder, ardından çağıranın hiçbir şeyi bozmadan koklayabilmesi için akış konumunu geri yükler. Dört alan daha geometriyi belirler: 0x1C'deki bayt sırası 0xFFFE olmalıdır, bu da ucuz bir ikinci imza kontrolü olarak da işlev görür; 0x1E'deki sektör kayması, sektör boyutunu 1 shl SectorShift olarak verir, bu yüzden sürüm 3, 512 baytlık sektörler için 9 kaymasını kullanır ve sürüm 4, 4096 için 12 kaymasını kullanır; 0x20'deki mini sektör kayması 6'dır, mini sektörleri 64 bayt yapar; ve 0x38'deki mini akış kesim noktası 4096'dır. Bunu izleyen adres aritmetiği, yanlış gitmenin en yaygın yeridir. Sektör 0, başlıktan hemen sonra başlar, bu yüzden sektör N, 512 + N * SectorSize bayt ofsetinde başlar — sabit 512'ye dikkat edin, SectorSize'a değil. Sürüm 3 bir dosyada ikisi aynıdır ve hata sonsuza dek gizli kalır; sürüm 4 bir dosyada ise sessizce yanlış sektörü okur, bu yüzden HotXLS bunu tek bir fonksiyonda, SidToOffset'te tutar

Bir compound dosyası, bir dosya içindeki bir FAT dosya sistemidir, bu yüzden onu okumak, FAT[n]'in sektör n'yi takip eden ID'yi tuttuğu bağlı sektör ID listelerini gezmek anlamına gelir. Üç sentinel bir zinciri sonlandırır ya da açıklar — ENDOFCHAIN, FAT'ın kendisine ait bir sektör için FATSECT ve bir DIFAT sektörü için DIFSECT — ve üçü de negatif işaretli 32-bit tamsayı olarak okunur, bu da döngü koşullarını basit tutar. FAT'ı bulmak bir dolaylılık daha gerektirir: DIFAT, FAT sektörlerinin nerede yaşadığını söyleyen sektör ID'leri dizisidir ve ilk 109 girdisi başlıkta 0x4C ofsetindedir. TlxCompoundFile bu 109'u gezer, ilk negatif girdide durur ve her FAT sektörünü tek düz bir Integer dizisinde birleştirir. Bu, 512-baytlık bir sektörde girdi başına 128 olmak üzere 109 FAT sektörüdür, yani 13.952 adreslenebilir sektör, yani DIFAT'ın kendi zincirine dökülmek zorunda kalmasından önce kabaca 6,8 MiB konteyner

İkinci tahsis tablosu, 512-baytlık sektörlerin çoğu alanlarını küçük akışlarda israf etmesinden dolayı var olur. 4096-bayt kesim noktasının altındaki herhangi bir akış hiç sektörlerde depolanmaz: mini akış içinde yaşar; bu, kök dizin girdisinden sarkan sıradan bir akıştır, 64-baytlık mini sektörlere bölünmüştür ve başlık ofseti 0x3C'de kökleşmiş paralel bir MiniFAT üzerinden zincirlenmiştir. Gerçek bir .xls açın ve Workbook akışı normal FAT üzerinde otururken özet-bilgi akışları mini-sektör uzayında aşağıda oturur; bu, yalnızca FAT yolunu kapsayan bir uygulamanın doküman meta verisine ihtiyaç duyana kadar doğru çalışıyormuş gibi görünmesinin nedenidir. Dizin üçüncü yapıdır ve konteyneri gezilebilir yapan yapıdır: her girdi tam olarak 128 bayttır, 512-baytlık sektör başına dört tane, ilk 64 baytta bir UTF-16 isim taşır, 0x40'ta bayt uzunluğu, 0x42'de nesne türü (1 = storage, 2 = akış, 5 = kök), 0x44, 0x48 ve 0x4C'de ağaç bağlantıları, 0x74'te başlangıç sektörü ve 0x78'de 32-bit akış boyutu. O isim uzunluğu, sonlandıran null dahil baytları sayar, bu yüzden karakter sayısı NameLen div 2 - 1'dir ve bunu bir eksik yapmak, Workboo adlı bir akışla sonuçlanmanın yoludur

Bir bellek tamponundan bir Workbook akışı çekmek

TlxCompoundFile.OpenStream, yukarıdakilerin hepsini, bir akış ismi alan ve tamamen somutlaştırılmış baytları tutan bir TlxCfbStream döndüren tek bir çağrının arkasına gizler. Tüm sıra — kokla, yükle, çıkar — hiçbir şey diske dokunmadan bir TBytesStream'e karşı çalışır

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;

Orada iki ayrıntı öne çıkarılmaya değer. LoadFromStream, varsayılan olarak False olan bir AOwnsStream bayrağı alır, bu yüzden çağıran kaynak akıştan sorumlu kalır — kasıtlıdır, çünkü yaygın durum uygulamanın zaten sahip olduğu bir akıştır. Ve OpenStream, Data, Size, Read, Seek ve CopyTo üzerinden açığa çıkan, baytlarının kendi kopyasına sahip bir TlxCfbStream döndürür. Bu kopya, büyük bir çalışma kitabında gerçek bir maliyettir ve döndürülen nesnenin konteyner serbest bırakıldıktan sonra geçerli kalmasının tasarım gereği dürüst bedelidir. Bir çalışma kitabı, tam bellek-içi kopyanın tamamen yanlış şekil olacağı kadar büyük olduğunda, büyük boyutlu elektronik tablolar için streaming direct reader daha iyi giriş noktasıdır

Şifrelenmiş bir XLSX neden bir XLS dosyası gibi görünür?

Çünkü konteyner seviyesinde öyledir — ve bu, o katmana sahip olmanın pratik getirisidir. Şifrelenmiş bir .xlsx'i bir onaltılık düzenleyicide açın, ilk sekiz bayt D0 CF 11 E0 A1 B1 1A E1'dir, bayt bayt 1997-model bir .xls ile aynıdır, çünkü [MS-OFFCRYPTO] şifrelemesi ZIP paketini yerinde şifrelemez: tüm paketi, algoritmayı açıklayan bir EncryptionInfo akışının yanında EncryptedPackage adlı bir akış olarak bir CFB konteynerinin içine sarar. İmza bu yüzden konteyneri tanımlar ve yük hakkında hiçbir şey söylemez. Bir BIFF çalışma kitabını şifrelenmiş bir OOXML paketinden ayırt etmek, dizini okumak anlamına gelir; bu, LoadFromStream'den sonra EntryCount ve Entries üzerinde bir tarama ya da bir çift HasStream yoklamasıdır

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;

Dizin isimleri kendi başlarına bir uyarı hak eder: özet-bilgi akışları, isimlerinde baştaki bir 0x05 kontrol karakteri taşır, bu yüzden düz görüntü dizesine karşı yazılmış bir karşılaştırma onlarla asla eşleşmez ve saf bir günlük satırı onları çöp olarak görüntüler. Bu sınıflandırmanın aşağısındaki her şey — anahtarı türetmek, parola doğrulayıcısını kontrol etmek — ayrı bir sorundur ve Excel'in yanlış şifre modu ile şifrelenmiş bir çalışma kitabını neden reddettiği üzerine notlarda ele alınmıştır. Konteyner katmanı size yalnızca hangi kapının önünde durduğunuzu söyler

Excel'in gerçekten açacağı bir konteyner yazmak

TlxCompoundFile'ın yazma tarafı, okuma tarafından kasıtlı olarak daha dardır ve nedenini anlamak, spek ile bir tartışmadan kurtarır. [MS-CFB], muazzam bir geçerli konteyner alanına izin verir: çok seviyeli storage'lar, düzgün dengelenmiş kırmızı-siyah dizin ağaçları, mini akışlar, DIFAT zincirleri. Excel, o alanın küçük bir köşesini yayınlar ve biraz daha büyük bir bölümünü okur. HotXLS, daha da küçük bir köşe yazar — Excel'in kanıtlanabilir şekilde yüklediği minimum. Her akış, mini-akış yolu olmadan normal FAT üzerine gider; bu, disk alanına mal olur ve doğruluk satın alır: Excel'in beş 64-baytlık mini sektöre paketleyeceği 300-baytlık bir özet akışı bunun yerine tam bir 512-baytlık sektör kaplar ve bir çalışma kitabı için bu, ikinci bir tahsis tablosunu, ikinci bir zincir gezintisini ve yazma yolunda onu destekleyen kök-girdi akışını sürdürmenin yanında gürültüdür. Dizin girdileri, her düğüm siyah renklendirilmiş şekilde kök altında düz bir kardeş zinciri oluşturur ve yayınlama sırası sabittir: başlık yer tutucu, akış veri sektörleri, dizin sektörleri, FAT sektörleri, ardından yalnızca sonda bilinen sektör ID'leriyle başlığı yeniden yazmak için geriye bir arama. FAT, kısa bir sabit-nokta döngüsüyle kendini boyutlandırır, çünkü FAT sektörleri eklemek, sektör sayısını başka bir FAT sektörü gerektirecek kadar yükseltebilir

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;

Uygulamanın durduğu yer

Üç sınırın açıkça belirtilmesi gerekir, çünkü bir kenar durumu sessizce yanlış işleyen bir konteyner okuyucusu, hata fırlatan bir okuyucudan daha kötüdür. TlxCompoundFile, başlıkta yerleşik 109 DIFAT girdisini okur ve DIFAT zincirini 0x44'te onların ötesinde takip etmez, okunabilir bir konteyneri 512-bayt sektörlerde kabaca 6,8 MiB'de sınırlar — HotXLS'in sahada karşılaştığı gerçek .xls dosyalarının rahatça üzerinde, ama yine de sert bir tavan, ve yazıcı aynı sınırı, tanımlayamayacağı bir konteyner yayınlamak yerine açıkça uygular. İkinci olarak, 4096-baytlık sektörlere sahip sürüm 4 konteynerleri sektör-boyutu aritmetiği tarafından karşılanır ama kodun ayarlandığı şey değildir ve 64-bit akış boyutuna başvurulmaz: HotXLS, 0x78 ofsetindeki alt 32 biti okur ve üst yarıyı bırakır; bu sürüm 3 için doğrudur ve yalnızca sürüm 3 için doğrudur. Üçüncü olarak, girdi araması, bir üst storage'dan kırmızı-siyah ağaçta bir gezinme yerine dizin listesi boyunca isme göre düz bir taramadır, bu yüzden iç içe storage'lar yola göre değil isim çakışmasına göre çözülür — bir .xls dosyasının ihtiyaç duyduğu her akış en üst seviyede oturur, ki bu daha basit tasarımı savunulabilir kılan şeydir, ama SomeStorage/SomeStream'i adreslemeyi bekleyen kod onu bulamayacaktır

Bunların hiçbiri birimin ne için olduğunu değiştirmez. Konteyner katmanına sahip olmak, .xls işlemeyi sıradan Object Pascal'a dönüştürür: bir bayt dizisinden ayrıştırılabilir, dosya sistemi olmadan test edilebilir, derleyicinin hedeflediği herhangi bir platforma taşınabilir ve bir COM apartment'tan bağımsız. Ayrıca koklama kısayollarını emekliye ayırır, çünkü bir çalışma kitabını tanımlamak artık ilk sekiz baytını değil dizinini okumak anlamına gelir — tüm çalışma kitabını açmadan sayfa isimlerini listelemenin arkasındaki aynı disiplin

TlxCompoundFile, üzerine oturan BIFF ve OOXML katmanlarının yanı sıra Delphi ve C++Builder için HotXLS Excel Component'in bir parçası olarak sunulur; ürün sayfası tam birim referansını ve desteklenen derleyici matrisini içerir