HotXLS decodifica una XLUnicodeString de BIFF8 leyendo primero cch y el flag fHigh, y después elige el lector que corresponde a la codificación: TXLSBlob.GetWideString con un recuento de bytes de cch * 2 cuando fHigh es 1, y TXLSBlob.GetString cuando fHigh es 0. Si los empareja al revés, el registro devuelve una string vacía o de media longitud, nunca una excepción
Eso es lo que hace costosa esta clase de bug. Un gráfico se abre, las series se dibujan correctamente, los ejes están bien y una leyenda de trendline aparece sencillamente vacía. Nada en el log, nada en el handler de excepciones ni un diálogo de archivo corrupto. El archivo estaba bien todo el tiempo; el lector pidió el número equivocado de bytes y obtuvo exactamente lo que pidió
¿Por qué una string BIFF8 vuelve vacía?
Una string BIFF8 vuelve vacía porque una protección de longitud rechazó el payload antes de que se produjera la lectura, o porque el lector se detuvo ante el primer NUL que encontró. Ambas rutas son silenciosas por construcción. En HotXLS, la protección suele ser una comprobación explícita de DataLength en el handler del registro y tiene que calcularse según la codificación: un payload de 16 bits necesita 8 + cch * 2 bytes para un cuerpo SXViewLink, mientras que uno de 8 bits solo necesita 8 + cch. Aplique la aritmética de caracteres anchos a un registro de 8 bits y todos los nombres cortos fallarán la protección. El comportamiento con NUL es la segunda trampa, porque TXLSBlob.GetString y TXLSBlob.GetWideString recorren el resultado decodificado hasta encontrar un terminador y truncan ahí, devolviendo una string vacía cuando el terminador cae en la posición uno. Lea un cuerpo de 16 bits con la mitad del recuento de bytes y conservará los primeros cch div 2 caracteres; lea un cuerpo de 8 bits mediante el lector wide y las parejas de bytes formarán puntos de código arbitrarios. Solo una lectura demasiado larga hace ruido: TXLSBlob.EnsureReadable genera Blob read exceeds data size cuando la petición supera el tamaño del blob. Una lectura demasiado corta no tiene esa alarma
GetWideString cuenta bytes, no caracteres
TXLSBlob.GetWideString(Index, Count) recibe Count en bytes. Internamente hace un SetString sobre un PWideChar con Count div SizeOf(WideChar), de modo que pasar un recuento de caracteres reduce la string a la mitad silenciosamente. Mientras tanto, los layouts de registros BIFF8 expresan la longitud de la string en caracteres. Por eso cada punto de llamada de 16 bits tiene que llevar por sí mismo la conversión * 2, y cada punto de llamada de 8 bits no debe llevarla. Es el mismo límite de codificación que aparece al volver a escribir texto en lugar de leerlo, algo que conviene leer junto a la exportación de hojas de cálculo Unicode-safe en Delphi si su pipeline mueve strings en ambas direcciones
// XLUnicodeStringNoCch de 16 bits: cch caracteres, cch * 2 bytes
Name := Data.GetWideString(Start, cch * 2); // correcto
Name := Data.GetWideString(Start, cch); // la mitad del texto, sin error
// XLUnicodeStringNoCch de 8 bits: cch caracteres, cch bytes
Name := WideString(Data.GetString(Start, cch)); // correcto
Name := Data.GetWideStringWithZero(Start, cch); // sigue siendo un lector wide
La convención se mantiene en cualquier lugar donde se recorra el stream de bytes a mano. Cuando HotXLS recompone un registro String largo ($0207, [MS-XLS] 2.4.268) a partir de sus registros Continue ($003C), la rama wide calcula segCh a partir de la longitud del segmento y después llama a GetWideString(3, segCh * 2), porque el cuerpo del registro comienza en el offset 3 y el recuento sigue estando en bytes. El lector de rich text hace lo mismo desde el offset 1 en el primer segmento Continue. [MS-XLS] 2.5.293 garantiza que el corte cae en un límite de carácter de doble byte cuando fHighByte es 1, así que no hace falta mantener caracteres parciales, pero la aritmética de bytes sigue siendo responsabilidad suya
¿Qué hace realmente GetWideStringWithZero?
TXLSBlob.GetWideStringWithZero es un lector de caracteres wide que conserva los NUL incrustados. El sufijo WithZero indica que conserva los NUL, no la anchura de los caracteres: internamente ejecuta el mismo SetString sobre un PWideChar con Count div SizeOf(WideChar) que GetWideString, pero sin recorrer el terminador. La contrapartida de un byte es TXLSBlob.GetStringWithZero, que devuelve un AnsiString. El nombre no dice cuál es cuál, y esa ambigüedad ha costado bugs reales a este código. Conviene nombrar la lectura incorrecta concreta porque parece muy plausible: GetWideString necesita cch * 2, así que GetWideStringWithZero debe ser el que recibe cch directamente. Recibe cch sin quejarse, devuelve una WideString y el compilador está satisfecho. También devuelve la mitad de los caracteres, montados a partir de parejas de bytes incorrectas. La ruta correcta de 8 bits es TXLSBlob.GetString con un recuento de bytes cch normal, convertido a WideString en la asignación. HotXLS 2.376.0 corrigió exactamente ese uso incorrecto en dos decoders de gráficos
SXViewLink y la protección de longitud por codificación
SXViewLink ($0858, [MS-XLS] 2.4.316) es el ejemplo trabajado más claro porque empaqueta ambas asimetrías en una cabecera de ocho bytes. El layout es rt(2), unused(2), reserved(2), cch(1), fHigh(1), seguido por un cuerpo XLUnicodeStringNoCch: fHigh = 1 significa cch * 2 bytes de UTF-16, fHigh = 0 significa cch bytes de caracteres de un byte, y cch está limitado a 255 porque el campo de longitud ocupa un solo byte. HotXLS escribe el registro dentro de los chart globals, antes de Units y junto a PivotChartBits ($0859, [MS-XLS] 2.4.196), cuando una hoja de gráfico enlaza con una vista PivotTable; la visión a nivel de registro de esa maquinaria se trata en la escritura de registros PivotTable BIFF8 en Delphi
// SXViewLink ([MS-XLS] 2.4.316): rt(2) unused(2) reserved(2) cch(1)
// después una XLUnicodeStringNoCch: fHigh(1) seguido por los caracteres
PivCch := Item.FData.GetByte(6);
if (PivCch > 0) and (Item.FData.GetByte(7) <> 0) and
(Item.FData.DataLength >= LongWord(8 + PivCch * 2)) then
begin
Result.PivotSourceName := Item.FData.GetWideString(8, PivCch * 2);
Result.IsPivotChart := True;
end
else if (PivCch > 0) and (Item.FData.GetByte(7) = 0) and
(Item.FData.DataLength >= LongWord(8 + PivCch)) then
begin
Result.PivotSourceName := WideString(Item.FData.GetString(8, PivCch));
Result.IsPivotChart := True;
end;
Las dos ramas no son decorativas. Una versión anterior protegía ambas codificaciones mediante la expresión wide 8 + cch * 2, por lo que un nombre de vista de 8 bits escrito por Excel no superaba la protección y el decoder devolvía una PivotSourceName vacía con IsPivotChart a false. El enlace al pivot desaparecía del modelo sin un solo diagnóstico. El mismo error estaba en el decoder de Trendline ($2050, [MS-XLS] 2.4.328), donde el campo de nombre sigue a 28 bytes de payload numérico con un cch de dos bytes en el offset 28, fHigh en el offset 30 y los caracteres en el offset 31; las leyendas de trendline que Excel había escrito con caracteres de 8 bits se decodificaban como strings vacías. Ambos se corrigieron en la misma release. Y el caso de 8 bits no es una curiosidad heredada confinada a los archivos de Excel 2.0 a 4.0: Excel actual sigue escribiendo payloads BIFF8 de 8 bits cuando cada carácter cabe en un byte
¿Cómo se decodifica con seguridad un registro BIFF8 nuevo en Delphi?
Cuando el campo es realmente una XLUnicodeString estándar, use TXLSBlob.GetBiffString en lugar de escribir la rama a mano. Lee el campo de longitud, lee el byte de opciones, envía al lector correspondiente y avanza el cursor más allá del cuerpo. Los dos parámetros booleanos son la parte que conviene leer con cuidado: is8bit describe la anchura del campo de longitud, no la anchura de los caracteres, y iswide indica si hay un byte de opciones fHigh después del campo de longitud. Las versiones BIFF inferiores a $0600 no tienen ninguno de los dos
var
Offset: LongWord;
begin
Offset := 6; // en SXViewLink el byte cch comienza aquí
// is8bit = el campo de longitud ocupa un byte
// iswide = después del campo de longitud aparece un byte de opciones fHigh
Name := Data.GetBiffString(Offset, True, True);
// Offset apunta ahora al primer byte después del cuerpo de la string
Las ramas escritas a mano siguen estando justificadas cuando el handler tiene que sobrevivir a entradas truncadas u hostiles, porque GetBiffString se apoya en que EnsureReadable genere una excepción en lugar de en una comprobación de límites que usted pueda controlar. Por eso el decoder de gráficos de HotXLS comprueba DataLength y no devuelve nada en lugar de lanzar una excepción: un workbook de terceros malformado debería costarle una leyenda, no el documento entero. El compromiso es deliberado y precisamente por eso la protección específica de la codificación tiene que ser correcta, ya que esa protección es lo que convierte una lectura mala en silencio
Un último detalle de proceso, aprendido a las malas en la misma release. Haga aserciones sobre un workbook guardado y vuelto a abrir, no sobre el modelo en memoria que acaba de construir. El batch 2.376.0 también sacó a la luz un emisor SXEx ([MS-XLS] 2.4.282) que declaraba un cuerpo de 24 bytes y escribía solo 22, desalineando todos los registros posteriores a la vista PivotTable, incluido el EOF del worksheet y cualquier substream de hoja de gráfico que viniera después. Las pruebas de pivot existentes no lo detectaron porque todas comprobaban la memoria. La decodificación de strings tiene la misma propiedad: un round-trip a través del archivo es la única prueba que realmente ejercita los recuentos de bytes
Si trabaja con internals XLS clásicos en Delphi o C++Builder y prefiere no mantener su propio lector de registros BIFF8, las reglas de codificación anteriores ya están implementadas y cubiertas por regresiones en el componente de hojas de cálculo Delphi HotXLS, que lee y escribe XLS y XLSX sin Excel ni automatización OLE