Abra un xls antiguo, guárdelo de nuevo, y la fórmula de complemento que llamaba a una biblioteca de análisis registrada apunta ahora a una referencia vacía dentro del propio libro. HotXLS rastrea esa corrupción silenciosa hasta una suposición errónea: que un registro BIFF SupBook es self o un archivo externo. [MS-XLS] define siete tipos, no dos
¿Por qué un libro guardado pierde sus enlaces de complemento?
Porque la prueba de clasificación era estructural y no de tipos. El atajo tradicional lee un registro SupBook ($01AE), comprueba si lleva el marcador self y, si no, trata la cadena que sigue como una URL de documento. Todo registro que no sea ninguna de esas dos cosas cae en una rama por defecto, y la rama por defecto casi siempre dice "este es el propio libro". Un enlace de soporte a complemento, un enlace de la misma hoja, una ranura sin usar y un registro truncado acaban todos con la misma etiqueta equivocada. Nada lanza una excepción mientras ocurre: el registro se parseó, la fórmula se recompiló, el archivo se guardó sin advertencia, y el defecto aparece tres semanas después cuando alguien nota una columna de ceros donde había una conversión de moneda. [MS-XLS] §2.4.271 describe un registro que puede ser una autorreferencia, una referencia de la misma hoja, un contenedor de funciones de complemento, un libro externo con ruta virtual y tabla de nombres de hoja, un enlace de datos DDE u OLE, o un marcador de posición sin usar — y un séptimo estado que no está en la especificación pero existe en discos reales: el registro que no se parsea. La solución no es una heurística mejor; es negarse a tener una heurística
Los siete tipos que puede llevar un registro SupBook
HotXLS declara la taxonomía de enlaces de soporte como una enumeración cerrada en lxExternSheet.pas, y toda decisión aguas abajo conmuta sobre ella. Nueve valores de enumeración cubren las siete categorías, porque el caso DDE y OLE necesita un estado provisional antes de poder resolverse:
type
TXLSSupportingLinkKind = (
slkUnknown, // no se parseó, o quedaron bytes al final
slkSelf, // este libro
slkSameSheet, // marcador U+0000
slkAddIn, // contenedor de funciones de complemento
slkExternalWorkbook, // ruta virtual + tabla de nombres de hoja
slkDde, // se resuelve con los flags de ExternName
slkOle, // se resuelve con los flags de ExternName
slkDdeOrOle, // uno de los dos, aún no se sabe cuál
slkUnused); // marcador de un solo espacio
TXLSFormulaReferenceClass = (
frcInternal,
frcExternalWorkbook,
frcExternalOther,
frcUnknownOrMalformed);
TXLSXtiInfo = record
XtiIndex : Integer; // base cero, tal como se guarda en ExternSheet.rgXTI
ExternID : Integer; // base uno, la convención interna
SupBookIndex: Integer;
Sheet1Index : Integer;
Sheet2Index : Integer;
LinkKind : TXLSSupportingLinkKind;
end;
El despacho se guía por centinelas, no por cadenas. Un valor de campo $0401 marca el registro self. Un conteo de hojas de uno emparejado con $3A01 marca un contenedor de complemento. Solo un valor en el rango de 1 a $00FF significa que sigue una ruta virtual codificada, y solo entonces HotXLS decodifica una cadena. Cualquier cosa fuera de esas tres formas permanece slkUnknown, y un registro cuya tabla de nombres de hoja no consume el cuerpo del registro exactamente se degrada de nuevo a slkUnknown incluso cuando el encabezado parecía plausible
¿Por qué el marcador de la misma hoja se decodifica como cadena vacía?
Porque el lector de cadenas BIFF de propósito general destruye el byte del que depende la clasificación. El enlace de soporte de la misma hoja es una cadena de un carácter cuyo único carácter es U+0000, y TXLSBlob.GetBiffString lo devuelve como un WideString vacío, indistinguible de una ruta genuinamente vacía — que es exactamente la entrada ante la que una heurística de autorreferencia responde "self". HotXLS lee por eso el primer punto de código crudo del cuerpo del registro en lugar de fiarse del valor decodificado:
StringOffset := offset;
FDocUrl := Data.GetBiffString(offset, False, True);
FirstChar := $FFFF;
if val = 1 then
begin
StringOptions := Data.GetByte(StringOffset + 2);
if (StringOptions and $01) = 0 then
FirstChar := Data.GetByte(StringOffset + 3) // comprimido, un byte
else
FirstChar := Data.GetWord(StringOffset + 3); // ancho, dos bytes
end;
if FirstChar = 0 then
FKind := slkSameSheet
else if (Length(FDocUrl) = 1) and (FDocUrl[1] = WideChar(#32)) then
FKind := slkUnused
else if Pos(WideChar(#3), FDocUrl) > 0 then
FKind := slkDdeOrOle
else if FDocUrl <> '' then
FKind := slkExternalWorkbook;
Fíjese en la rama comprimido contra ancho. El byte de opciones está a un offset fijo del encabezado de la cadena y el primer punto de código ocupa uno o dos bytes según el bit 0, así que leerlo como un byte sin condiciones funciona en la mayoría de los archivos y falla en los escritos por builds localizadas — la peor distribución posible para un bug. El marcador de posición sin usar se detecta igual, por su payload literal de un solo espacio, y el caso DDE u OLE por el separador U+0003 incrustado en la ruta codificada
¿Por qué DDE y OLE no se pueden separar en el momento del SupBook?
Porque el registro SupBook no lleva los bits distintivos. Le dice que el enlace es uno de los dos; los flags fOle y fOleLink que deciden cuál viven en el registro ExternName ($0023) que llega después en el flujo. HotXLS registra slkDdeOrOle al parsear y lo acota en ParseExternalName, y si ningún ExternName llega nunca, el tipo permanece provisional para siempre — lo cual es correcto, porque el archivo genuinamente no lo dice. Todo consumidor aguas abajo trata ese valor provisional como un valor real y no como uno faltante, así que ningún llamador tiene que inventar un desempate. Adivinar "probablemente DDE" aquí compraría una enumeración más ordenada y una clase de respuestas incorrectas que nadie podría rastrear:
if FKind = slkDdeOrOle then
begin
if Data.DataLength < 2 then
Exit;
Flags := Data.GetWord(0);
if (Flags and $0010) <> 0 then
FKind := slkOle
else if (Flags and $0008) <> 0 then
FKind := slkDde;
end;
Los índices XTI son base cero en disco y base uno por dentro
HotXLS realiza la conversión del desplazamiento de uno exactamente una vez, en el punto donde un token entra al árbol de sintaxis interno, y en ningún otro lugar. PtgNameX.ixti ([MS-XLS] §2.5.198.85) es un índice base cero dentro del arreglo rgXTI del registro ExternSheet ($0017, §2.4.106), mientras que la convención interna de ExternID de la biblioteca es base uno con cero reservado para "sin hoja externa". La ruta de lectura BIFF8 hace FExternID := wValue + 1 al decodificar un token tNameX y la ruta de escritura emite StoreExternID - 1, dejando intactas la vista cruda de tokens y la semántica en disco. Equivocarse aquí es inusualmente difícil de detectar: los nombres definidos externos resuelven a la entrada vecina, y en un archivo con una sola entrada XTI el índice 0 se vuelve 1, falla, y el nombre se degrada en silencio. Una regresión que solo ejercita texto de fórmulas recompilado jamás lo ve, porque la recompilación nunca toca el índice en disco — la misma trampa que hace que los nombres definidos que cruzan hojas y libros valgan la pena probarse contra flujos de bytes reales. La resolución está acotada en ambos extremos: TlxExternSheetSheet.TryResolveXti devuelve False para un índice negativo o una entrada faltante, TXLSSupBook.TryGetKind devuelve False para un índice de SupBook fuera del arreglo, y ClassifyXti mapea luego slkSelf y slkSameSheet a frcInternal, slkExternalWorkbook a frcExternalWorkbook, y slkAddIn, slkDde, slkOle y slkDdeOrOle a frcExternalOther. Todo lo demás, incluida cada ruta fuera de rango, cae en frcUnknownOrMalformed
Clasificar una fórmula antes de congelarla
TXLSCompiledFormula.ClassifyReferences escanea directamente el flujo de tokens BIFF preservado en lugar de decompilar la fórmula y buscar corchetes. Cazar corchetes en el texto de la fórmula es una heurística de texto con abrigo de parser: coincide con literales de cadena, coincide con referencias estructuradas, y pasa por alto por completo los nombres definidos externos, ya que estos no llevan corchetes en forma decompilada. El escaneo de tokens mira solo PtgNameX, PtgRef3d, PtgArea3d, PtgRefErr3d y PtgAreaErr3d, con repliegue a un recorrido del árbol de sintaxis cuando no sobrevive ningún flujo BIFF. La fusión es deliberadamente pesimista — la prioridad fija es frcUnknownOrMalformed, luego frcExternalWorkbook, luego frcExternalOther, luego frcInternal — así que un solo token ilegible envenena toda la fórmula. Para un nombre definido externo también se valida el índice del nombre: base uno, dentro del rango, y respaldado por un registro ExternName retenido
var
Wb : TXLSWorkbook;
Sheet: TXLSWorksheet;
i : Integer;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Open('quarterly.xls');
for i := 1 to Wb.Sheets.Count do // Sheets es base uno
begin
Sheet := Wb.Sheets[i];
// congela SOLO las fórmulas clasificadas frcExternalWorkbook;
// las referencias internas, de complemento, DDE/OLE y malformadas
// siguen siendo fórmulas
Sheet.ConvertFormulasToValues(True);
end;
Wb.SaveAs('quarterly-detached.xls');
finally
Wb.Free;
end;
end;
El parámetro OnlyExternal es donde la taxonomía se paga a sí misma. Congelar una fórmula es irreversible, así que la operación tiene que probar que una referencia es un libro externo en lugar de solo sospecharlo. Las llamadas a complemento sobreviven, los enlaces DDE y OLE sobreviven, y cualquier cosa que el parser no pudo entender del todo sobrevive, porque el desenlace seguro de la incertidumbre es no cambiar nada. La misma disciplina gobierna re-vincular fórmulas copiadas entre libros, donde una referencia mal clasificada se re-vincula al libro equivocado en lugar de fallar estridentemente
Los registros que no se parsean se reescriben intactos
HotXLS conserva el payload original del SupBook y lo reemite byte por byte cuando el registro nunca fue editado. Un fallo de parseo establece slkUnknown y limpia el estado derivado, pero el cuerpo capturado permanece en FRawData y la ruta de guardado lo prefiere sobre cualquier reconstrucción mientras el elemento no esté sucio y no sea el registro self. La alternativa — normalizar un registro sin parsear en una autorreferencia para que el escritor tenga algo bien formado que emitir — convierte un registro que usted no entendió en un registro definitivamente incorrecto. Ese principio es el mismo contrato aplicado a los proyectos VBA y sus referencias externas a lo largo de un ciclo de cargar y guardar, y es la diferencia entre una biblioteca que hace round-trip de archivos reales y una que hace round-trip de los archivos que su suite de pruebas casualmente contiene. Un libro que ha pasado por quince años de versiones de Excel, un generador de reportes y dos herramientas de migración contendrá registros que nadie vivo hoy diseñó. Escríbalos de vuelta tal como los encontró
La clasificación con tipos de los registros SupBook y XTI llegó en HotXLS 2.361.2 a 2.361.4, junto con la resolución acotada de XTI y la ruta más segura de ConvertFormulasToValues descrita aquí. Si mantiene código Delphi o C++Builder que lee archivos xls antiguos con llamadas a complemento, enlaces DDE u OLE, o nombres definidos externos, el componente de hojas de cálculo HotXLS para Delphi maneja toda la taxonomía de forma nativa, sin instalación de Excel y sin automatización OLE en la máquina que hace el trabajo