HotXLS apre cartelle di lavoro scritte da Excel 2.0, 3.0 e 4.0 direttamente da Delphi e C++Builder. Questi file precedono il contenitore di documento composito OLE che ogni .xls successivo usa, quindi sono stream di record BIFF grezzi senza alcun wrapper di storage, e un lettore costruito per BIFF8 non troverà al loro interno una singola struttura riconoscibile. Aprirne uno usa la stessa chiamata Open di qualsiasi altra cartella di lavoro; il lettore rileva il formato e cambia percorso
Questi file continuano a comparire, ed è l'unico motivo per cui tutto questo conta. Archivi ingegneristici, conservazione di documenti governativi, dati di laboratorio provenienti da strumenti il cui software di controllo fu scritto nel 1993, e sistemi contabili di lunga durata hanno tutti lasciato dietro di sé cartelle di lavoro BIFF2 e BIFF4. L'Excel moderno rifiuta di aprirne diversi del tutto, avendo rimosso i convertitori legacy per motivi di sicurezza, il che lascia un insieme di dati che nessuno può leggere con uno strumento che qualcuno possiede
Cosa rende diversa una cartella di lavoro pre-OLE?
Ogni .xls da Excel 5.0 in poi è un file composito OLE2, un piccolo file system dentro un file, con la cartella di lavoro che risiede in uno stream chiamato Workbook o Book. L'analisi di uno di essi inizia analizzando quel contenitore, come descritto in il formato binario dei file compositi in Pascal
Da BIFF2 a BIFF4 non esiste alcun contenitore. Il file inizia immediatamente con un record BOF, e il numero di record di quel BOF codifica la generazione: $0009 per BIFF2, $0209 per BIFF3 e $0409 per BIFF4. HotXLS convalida la lunghezza del corpo del BOF, compresa tra quattro e sei byte, e il tipo di sottostream, $0010 per un foglio di lavoro, $0020 per un grafico e $0040 per un foglio macro, prima di impegnarsi nel percorso grezzo. Questa convalida è ciò che impedisce a un file corrotto o identificato erroneamente di essere interpretato come una cartella di lavoro molto vecchia
Tre generazioni, tre layout di record
I record di cella sono il punto in cui le generazioni divergono più visibilmente. BIFF2 occupa un blocco contiguo di numeri di record bassi, da $0001 a $0005 per celle vuote, intere, numeriche, etichetta e booleane-o-errore, e ogni corpo porta un campo di attributo di tre byte dove le versioni successive mettono un indice di formato esteso. BIFF3 e BIFF4 abbandonano questo schema e riutilizzano i numeri di record e i layout di BIFF5, $0201, $0203, $0204 e $0205, con un indice XF di due byte
Quest'ultimo dettaglio causa un fallimento specifico e facilmente diagnosticato in modo errato. Un record LABEL BIFF3 o BIFF4 è strutturalmente identico alla sua controparte BIFF5, riga e colonna seguite dall'indice di formato e poi dal conteggio dei caratteri. Scrivi un lettore che presume il layout BIFF2 e leggerà due byte in meno, poi finirà oltre la fine del record e interpreterà male tutto ciò che segue. Il sintomo non è un'eccezione; è una cartella di lavoro che si legge con dati spazzatura plausibili al suo interno
I record delle formule occupano una numerazione parallela su tutte e tre le generazioni, $0006, $0206 e $0406. Quando una formula produce un risultato stringa, quella stringa arriva in un record separato successivo, $0007 o $0207, e la forma BIFF2 di esso usa un prefisso di lunghezza a byte singolo anziché quello a due byte usato successivamente
Perché le formule tornano come valori, non come testo
HotXLS legge il risultato memorizzato nella cache di una formula in questi file e non tenta di ricostruire l'espressione della formula. Questo è un confine deliberato, non una lacuna in attesa di essere colmata
L'espressione analizzata in BIFF2 fino a BIFF4 usa una codifica di token che differisce da BIFF5 e successivi in modi che vanno oltre l'aspetto estetico: le lunghezze dei token sono prefissate diversamente, i token di riferimento hanno dimensioni diverse, e le tabelle degli indici di funzione sono state rinumerate tra le generazioni. Far passare quei byte attraverso un traduttore di espressioni BIFF8 non produce una formula sbagliata, ne produce una casuale. Leggere il valore memorizzato nella cache fornisce il numero o la stringa che Excel ha calcolato l'ultima volta, che è ciò di cui una migrazione di archivio ha davvero bisogno
Il valore memorizzato nella cache risiede a un offset dipendente dalla generazione all'interno del record: byte 7 per BIFF2 e byte 6 per BIFF3 e BIFF4. I valori speciali, stringhe, booleani, errori e celle vuote, sono codificati in una word marcatore di $FFFF con un discriminatore, la stessa convenzione mantenuta dalle generazioni BIFF successive
Aprirne uno
Il codice chiamante non ha nulla di particolare, ed è proprio questo il punto. Il rilevamento avviene dentro Open:
uses
lxHandle;
var
Book: TXLSWorkbook;
Sheet: TXLSWorksheet;
R, C: Integer;
V: Variant;
begin
Book := TXLSWorkbook.Create;
try
if Book.Open('archive\1993-inventory.xls') <> 1 then
begin
Writeln('unreadable - quarantine for manual review');
Exit;
end;
Sheet := Book.Sheets[1]; // Sheets[] è a base 1
for R := Sheet.UsedRange.FirstRow + 1 to Sheet.UsedRange.LastRow + 1 do
for C := Sheet.UsedRange.FirstCol + 1 to Sheet.UsedRange.LastCol + 1 do
begin
V := Sheet.Cells[R, C].Value;
if not VarIsEmpty(V) then
Writeln(Format('R%dC%d = %s', [R, C, VarToStr(V)]));
end;
finally
Book.Free;
end;
end;
Nota l'aritmetica degli indici in quel ciclo. I limiti di UsedRange sono a base zero mentre sia la collezione dei fogli sia l'accesso alle celle sono a base uno, un'incoerenza che precede l'API attuale ed è preservata per compatibilità. Dimenticare l'aggiustamento controlla il rettangolo sbagliato e non riporta nulla di insolito mentre lo fa. I controlli preliminari economici che evitano del tutto di caricare un file sono trattati in ispezione leggera delle cartelle di lavoro
Cosa non si ottiene, e cosa fare al riguardo
La formattazione non viene interpretata. HotXLS non analizza i record XF e FONT di queste generazioni, quindi font, colori, bordi e formati numerici non sono disponibili, e le celle che Excel un tempo mostrava come date tornano come i loro numeri seriali grezzi
Quest'ultimo punto richiede una gestione nel proprio codice piuttosto che nel lettore, e il motivo è onesto: i formati numerici in BIFF2 fino a BIFF4 non sono abbastanza affidabili da guidare una decisione automatica sulle date. Una colonna di numeri a cinque cifre potrebbe rappresentare date, o potrebbe rappresentare codici articolo. Converti deliberatamente, usando il sistema di date della cartella di lavoro, le cui regole sono descritte in numeri seriali di data, il sistema 1904 e i formati numerici:
// Decidi per colonna, mai per valore: un numero a cinque cifre può essere una
// data o un codice articolo, e il formato legacy non te lo dirà
if ColumnHoldsDates(C) then
begin
// I due sistemi di date distano 1462 giorni, quindi lo stesso seriale
// indica due date distanti quattro anni. Leggi il sistema dalla
// cartella di lavoro anziché presumerne uno
if Book.Date1904 then
Writeln(DateToStr(SerialToDate1904(V)))
else
Writeln(DateToStr(SerialToDate1900(V)));
end
else
Writeln(VarToStr(V));
Due note strutturali completano il quadro. I record di protezione tramite password e di code page compaiono dentro l'unico stream del foglio di lavoro anziché in uno stream a livello di cartella di lavoro, perché non esiste uno stream a livello di cartella di lavoro in cui metterli, quindi devono essere riconosciuti nel contesto del foglio di lavoro. E un file da BIFF2 a BIFF4 contiene esattamente un sottostream di foglio; le cartelle di lavoro multi-foglio non esistevano finché il formato non ha acquisito il proprio contenitore
Il percorso di migrazione pragmatico è quindi in due fasi: leggere il file legacy per i suoi valori, poi scrivere una cartella di lavoro moderna che porti quei valori con la formattazione applicata da chi scrive il codice. Lettura legacy, scrittura moderna e tutto ciò che sta in mezzo funzionano in un'unica libreria per Delphi e C++Builder, descritta nella pagina del componente foglio di calcolo Delphi HotXLS