HotXLS Excel Library untuk Delphi dan C++Builder membaca dan menulis container Compound File Binary di balik setiap berkas .xls lawas dalam Object Pascal murni. Kelas TlxCompoundFile mengimplementasikan layout [MS-CFB] versi 3 langsung terhadap sebuah TStream — header, DIFAT, rantai FAT, MiniFAT, dan directory tree — tanpa ole32.dll dan tanpa COM IStorage di mana pun dalam jalurnya
Itu terdengar seperti plumbing, dan selama dua puluh tahun memang plumbing milik orang lain. Setiap codebase Delphi yang menyentuh berkas .xls memanggil StgOpenStorage, mendapatkan kembali sebuah IStorage, dan menarik keluar stream Workbook darinya. Tiga baris, bekerja dengan baik, tidak ada yang memikirkannya lagi — sampai suatu hari kode yang sama harus berjalan di suatu tempat yang bukan Windows
Kenapa StgOpenStorage berhenti bekerja di server?
API COM structured-storage gagal persis pada bentuk deployment yang menjadi tempat hidup kode Delphi modern, karena alasan yang sama sekali tidak berkaitan dengan format berkasnya. StgOpenStorage adalah sebuah entry point Win32 di ole32.dll: ia menginginkan sebuah path pada filesystem, ia menginginkan COM sudah diinisialisasi pada thread pemanggil, dan ia menginginkan berada di Windows. Syarat path menyakitkan lebih dulu, karena sebuah REST endpoint yang menerima workbook yang di-upload memiliki byte-nya dalam sebuah buffer, bukan di disk — sehingga Anda menulis buffer itu ke temp file, membukanya, membacanya kembali, menghapusnya, dan kini memiliki siklus hidup temp-file yang bisa salah di bawah beban. ILockBytes adalah jalan keluar yang terdokumentasi, tetapi menyambungkan implementasi kustom di atas sebuah TMemoryStream adalah interop COM yang lebih rumit daripada yang diinginkan kebanyakan tim. Syarat inisialisasi menggigit kedua, biasanya pada sebuah thread worker layanan yang tidak pernah dipanggil CoInitialize-nya, dan syarat platform mengakhiri pembicaraan begitu target-nya adalah Linux di bawah FPC, sebuah image container, atau macOS. HotXLS karena itu mempertahankan jalur klasik lxOLE yang dibangun di atas StgOpenStorage sebagai default, karena sudah teruji medan dan caller yang sudah ada tidak perlu berubah; TlxCompoundFile adalah alternatif opt-in untuk semua orang lainnya
Apa yang sebenarnya dikatakan header dan rantai FAT
512 byte pertama dari sebuah compound file menjawab setiap pertanyaan struktural yang Anda butuhkan sebelum membaca satu byte payload pun. [MS-CFB] §2.2 mematok signature header pada offset 0 sebagai delapan byte D0 CF 11 E0 A1 B1 1A E1, dan lxIsCompoundStream memeriksa persis itu, mengembalikan posisi stream sesudahnya sehingga caller bisa mengendus tanpa mengganggu apa pun. Empat field lagi menentukan geometrinya: byte order pada 0x1C harus 0xFFFE, yang sekaligus berfungsi sebagai pemeriksaan signature kedua yang murah; sector shift pada 0x1E memberi ukuran sector sebagai 1 shl SectorShift, sehingga versi 3 memakai shift 9 untuk sector 512-byte dan versi 4 memakai shift 12 untuk 4096; mini sector shift pada 0x20 adalah 6, membuat mini sector berukuran 64 byte; dan mini stream cutoff pada 0x38 adalah 4096. Aritmatika alamat yang mengikuti adalah tempat paling umum untuk salah. Sector 0 dimulai persis setelah header, sehingga sector N dimulai pada byte offset 512 + N * SectorSize — perhatikan angka literal 512, bukan SectorSize. Pada berkas versi 3 keduanya identik dan bug-nya bersembunyi selamanya; pada berkas versi 4 ia diam-diam membaca sector yang salah, dan itulah sebabnya HotXLS menjaga ini dalam satu fungsi, SidToOffset
Sebuah compound file adalah filesystem FAT di dalam sebuah berkas, sehingga membacanya berarti menelusuri linked list dari sector ID tempat FAT[n] menyimpan ID yang mengikuti sector n. Tiga sentinel mengakhiri atau menandai sebuah rantai — ENDOFCHAIN, FATSECT untuk sector yang menjadi milik FAT itu sendiri, dan DIFSECT untuk sebuah sector DIFAT — dan ketiganya terbaca sebagai integer signed 32-bit negatif, yang membuat kondisi loop tetap sederhana. Menemukan FAT membutuhkan satu indireksi lagi: DIFAT adalah array sector ID yang menyatakan di mana sector FAT berada, dan 109 entri pertamanya berada di header pada offset 0x4C. TlxCompoundFile menelusuri 109 entri itu, berhenti pada entri negatif pertama, dan menggabungkan setiap sector FAT menjadi satu array Integer datar. Itu berarti 109 sector FAT dengan 128 entri masing-masing pada sector 512-byte, sehingga 13.952 sector yang bisa dialamati, sehingga kira-kira 6,8 MiB container sebelum DIFAT harus meluber ke rantainya sendiri
Tabel alokasi kedua ini ada karena sector 512-byte menyia-nyiakan sebagian besar ruangnya pada stream kecil. Stream mana pun di bawah cutoff 4096-byte sama sekali tidak disimpan dalam sector: ia hidup di dalam mini stream, yang sendiri adalah sebuah stream biasa yang menggantung pada entri root directory, dibagi lagi menjadi mini sector 64-byte dan dirantai lewat sebuah MiniFAT paralel yang berakar pada header offset 0x3C. Buka sebuah .xls sungguhan dan stream Workbook berada pada FAT normal sementara stream summary-information berada di ruang mini-sector, dan itulah sebabnya sebuah implementasi yang hanya mencakup jalur FAT tampak bekerja sampai ia membutuhkan metadata dokumen. Directory adalah struktur ketiga dan yang membuat container ini bisa dinavigasi: setiap entri persis 128 byte, empat per sector 512-byte, membawa sebuah nama UTF-16 dalam 64 byte pertama, panjang byte-nya pada 0x40, tipe objek pada 0x42 (1 = storage, 2 = stream, 5 = root), tautan tree pada 0x44, 0x48, dan 0x4C, start sector pada 0x74, dan ukuran stream 32-bit pada 0x78. Panjang nama itu menghitung byte termasuk null penutup, sehingga jumlah karakternya adalah NameLen div 2 - 1, dan meleset satu di situ adalah cara Anda berakhir dengan sebuah stream bernama Workboo
Menarik keluar stream Workbook dari sebuah memory buffer
TlxCompoundFile.OpenStream menyembunyikan semua hal di atas di balik satu pemanggilan yang mengambil sebuah nama stream dan mengembalikan sebuah TlxCfbStream yang membawa byte yang sudah sepenuhnya dimaterialisasi. Seluruh urutannya — mengendus, memuat, mengekstrak — berjalan terhadap sebuah TBytesStream tanpa apa pun yang pernah menyentuh 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;
Ada dua detail yang layak disebutkan di sini. LoadFromStream mengambil sebuah flag AOwnsStream yang defaultnya False, sehingga caller tetap memegang tanggung jawab atas stream sumber — ini disengaja, karena kasus umumnya adalah sebuah stream yang sudah dimiliki aplikasi. Dan OpenStream mengembalikan sebuah TlxCfbStream yang memiliki salinannya sendiri dari byte-nya, diekspos lewat Data, Size, Read, Seek, dan CopyTo. Salinan itu adalah biaya nyata pada sebuah workbook besar, dan itu adalah harga jujur dari sebuah desain tempat objek yang dikembalikan tetap valid setelah container-nya dibebaskan. Ketika sebuah workbook cukup besar sehingga salinan penuh in-memory sama sekali bukan bentuk yang tepat, streaming direct reader untuk spreadsheet berukuran besar adalah entry point yang lebih baik
Kenapa XLSX yang terenkripsi terlihat seperti berkas XLS?
Karena memang begitulah adanya, pada level container — dan inilah keuntungan praktis dari memiliki lapisan itu. Buka sebuah .xlsx terenkripsi dalam hex editor dan delapan byte pertamanya adalah D0 CF 11 E0 A1 B1 1A E1, identik byte demi byte dengan sebuah .xls vintage 1997, karena enkripsi [MS-OFFCRYPTO] tidak mengenkripsi paket ZIP di tempat: ia membungkus seluruh paket di dalam sebuah container CFB sebagai sebuah stream bernama EncryptedPackage, di samping sebuah stream EncryptionInfo yang mendeskripsikan algoritmanya. Signature-nya karena itu mengidentifikasi container-nya dan tidak mengatakan apa pun soal payload-nya. Membedakan sebuah workbook BIFF dari sebuah paket OOXML terenkripsi berarti membaca directory-nya, yang setelah LoadFromStream adalah sebuah scan atas EntryCount dan Entries, atau sepasang probe HasStream
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;
Nama directory layak mendapat peringatan tersendiri: stream summary-information membawa sebuah karakter kontrol 0x05 di depan namanya, sehingga sebuah perbandingan yang ditulis terhadap string tampilan biasa tidak akan pernah cocok dengan mereka, dan sebuah log line yang naif merendernya sebagai sampah. Segala sesuatu di hilir klasifikasi ini — menurunkan key, memeriksa password verifier — adalah masalah terpisah, dibahas dalam catatan tentang kenapa Excel menolak workbook yang dienkripsi dengan cipher mode yang salah. Lapisan container hanya memberi tahu Anda pintu mana yang sedang Anda hadapi
Menulis container yang benar-benar akan dibuka Excel
Sisi penulisan dari TlxCompoundFile sengaja dibuat lebih sempit daripada sisi pembacaannya, dan memahami alasannya menghindarkan Anda dari perdebatan dengan spesifikasinya. [MS-CFB] mengizinkan ruang container valid yang sangat luas: storage multi-level, directory tree red-black yang seimbang dengan benar, mini stream, rantai DIFAT. Excel menerbitkan sebuah sudut kecil dari ruang itu dan membaca sudut yang agak lebih besar. HotXLS menulis sudut yang lebih kecil lagi — minimum yang terbukti bisa dimuat Excel. Setiap stream masuk ke FAT normal tanpa jalur mini-stream, yang memakan ruang disk dan membeli kebenaran: sebuah stream summary 300-byte yang tadinya akan dipadatkan Excel ke dalam lima mini sector 64-byte sebagai gantinya menempati satu sector 512-byte penuh, dan untuk sebuah workbook itu hanyalah noise dibandingkan dengan memelihara tabel alokasi kedua, sebuah chain walk kedua, dan stream root-entry yang mendukungnya pada jalur penulisan. Entri directory membentuk sebuah rantai sibling datar di bawah root dengan setiap node diwarnai hitam, dan urutan penerbitannya tetap: placeholder header, sector data stream, sector directory, sector FAT, lalu sebuah seek kembali untuk menulis ulang header dengan sector ID yang baru diketahui di akhir. FAT-nya mengukur dirinya sendiri lewat sebuah loop fixed-point pendek, karena menambahkan sector FAT bisa mendorong jumlah sector cukup tinggi hingga membutuhkan sector FAT lain
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;
Di mana implementasi ini berhenti
Ada tiga batasan yang layak dinyatakan dengan jelas, karena sebuah container reader yang diam-diam salah menangani sebuah edge case lebih buruk daripada satu yang memunculkan error. TlxCompoundFile membaca 109 entri DIFAT yang berada di header dan tidak mengikuti rantai DIFAT pada 0x44 melampaui itu, membatasi container yang bisa dibaca pada kira-kira 6,8 MiB pada sector 512-byte — jauh di atas berkas .xls sungguhan yang ditemui HotXLS di lapangan, tapi tetap sebuah batas keras, dan writer-nya menegakkan batas yang sama secara eksplisit alih-alih menerbitkan sebuah container yang tidak bisa dideskripsikannya. Kedua, container versi 4 dengan sector 4096-byte diakomodasi oleh aritmatika ukuran sector tapi bukan yang menjadi tujuan penyetelan kode ini, dan ukuran stream 64-bit tidak dikonsultasikan: HotXLS membaca 32 bit rendah pada offset 0x78 dan membiarkan paruh atasnya, yang benar untuk versi 3 dan hanya untuk versi 3. Ketiga, pencarian entri adalah sebuah scan datar berdasarkan nama di seluruh daftar directory, bukan sebuah penelusuran menuruni red-black tree dari sebuah storage induk, sehingga storage bersarang diresolusi lewat tabrakan nama, bukan lewat path — setiap stream yang dibutuhkan sebuah berkas .xls berada di level teratas, dan itulah yang membuat desain yang lebih sederhana ini bisa dipertahankan, tapi kode yang mengharapkan untuk mengalamati SomeStorage/SomeStream tidak akan menemukannya
Tidak satu pun dari itu mengubah untuk apa unit ini ada. Memiliki lapisan container mengubah penanganan .xls menjadi Object Pascal biasa: bisa diurai dari sebuah byte array, bisa diuji tanpa filesystem, portabel ke platform apa pun yang menjadi target kompiler, dan bebas dari COM apartment. Ini juga memensiunkan jalan pintas sniffing, karena mengidentifikasi sebuah workbook kini berarti membaca directory-nya, bukan delapan byte pertamanya — disiplin yang sama di balik mendaftar nama sheet tanpa membuka seluruh workbook
TlxCompoundFile hadir sebagai bagian dari HotXLS Excel Component untuk Delphi dan C++Builder, berdampingan dengan lapisan BIFF dan OOXML yang berada di atasnya; halaman produk memuat referensi unit lengkap dan matriks kompiler yang didukung