feat(parser): support genuine BIFF8 XLS workbooks (#20573)

This commit is contained in:
Ricardo-M-L
2026-10-10 10:17:46 +08:00
committed by GitHub
parent 8f5d32e1ba
commit df3af45fb9
4 changed files with 274 additions and 3 deletions
+1
View File
@@ -48,6 +48,7 @@ require (
github.com/infiniflow/infinity-go-sdk v0.0.0-00010101000000-000000000000
github.com/infiniflow/onnxruntime_go v1.29.0
github.com/jackc/pgx/v5 v5.10.0
github.com/jloor/go-excel-reader v0.3.0
github.com/kaptinlin/jsonrepair v0.4.8
github.com/larksuite/oapi-sdk-go/v3 v3.9.9
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728
+2
View File
@@ -375,6 +375,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jloor/go-excel-reader v0.3.0 h1:CczAwTDGIZgMu7wP718Im3yONlOTN+asdb+cp7yk7As=
github.com/jloor/go-excel-reader v0.3.0/go.mod h1:A3hRAFOOAy82/j+pVmX/f/dVs4XEfDCT3aMVw+HNnGw=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+99 -3
View File
@@ -17,10 +17,17 @@
package parser
import (
"bytes"
"context"
"fmt"
"strings"
"time"
excel "github.com/jloor/go-excel-reader"
)
var compoundDocumentSignature = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1}
type XLSParser struct {
ParseMethod string
OutputFormat string
@@ -85,15 +92,25 @@ func (p *XLSParser) ParseWithResult(ctx context.Context, filename string, data [
}
}
mediaBudget := newEmbeddedMediaBudget()
items, warnings, sheetsCount, err := parseXLSXBytes(data, mediaBudget)
var (
items []map[string]any
warnings []string
sheetsCount int
err error
)
if bytes.HasPrefix(data, compoundDocumentSignature) {
items, sheetsCount, err = parseBIFF8Bytes(ctx, data)
} else {
mediaBudget := newEmbeddedMediaBudget()
items, warnings, sheetsCount, err = parseXLSXBytes(data, mediaBudget)
warnings = append(warnings, mediaBudget.warnings()...)
}
if err := ctx.Err(); err != nil {
return ParseResult{Err: err}
}
if err != nil {
return ParseResult{Err: fmt.Errorf("xls parse: %w", err)}
}
warnings = append(warnings, mediaBudget.warnings()...)
return ParseResult{
OutputFormat: spreadsheetOutputFormat,
@@ -102,3 +119,82 @@ func (p *XLSParser) ParseWithResult(ctx context.Context, filename string, data [
Warnings: warnings,
}
}
func parseBIFF8Bytes(ctx context.Context, data []byte) ([]map[string]any, int, error) {
reader, err := excel.OpenXLS(bytes.NewReader(data), excel.WithErrorValues(true))
if err != nil {
return nil, 0, fmt.Errorf("open BIFF workbook: %w", err)
}
defer reader.Close()
sheetCount := reader.SheetCount()
items := make([]map[string]any, 0, sheetCount)
for sheetIndex := 0; reader.NextSheet(); sheetIndex++ {
if err := ctx.Err(); err != nil {
return nil, sheetCount, err
}
records, dataRows, headerRow := readBIFF8Records(ctx, reader)
if err := ctx.Err(); err != nil {
return nil, sheetCount, err
}
items = append(items, buildSheetItems(records, reader.SheetName(), sheetIndex+1, headerRow, dataRows, nil)...)
}
if err := reader.Err(); err != nil {
return nil, sheetCount, fmt.Errorf("read BIFF workbook: %w", err)
}
return items, sheetCount, nil
}
func readBIFF8Records(ctx context.Context, reader excel.Reader) ([][]string, []int, int) {
type numberedRow struct {
number int
cells []string
}
rows := make([]numberedRow, 0)
for reader.Read() {
if ctx.Err() != nil {
return nil, nil, 0
}
cells := make([]string, reader.FieldCount())
for col := range cells {
cells[col] = spreadsheetCellString(reader.GetValue(col))
}
if rowIsEmpty(cells) {
continue
}
rows = append(rows, numberedRow{number: reader.RowIndex() + 1, cells: cells})
}
if len(rows) == 0 {
return nil, nil, 0
}
records := make([][]string, 0, len(rows))
dataRows := make([]int, 0, len(rows)-1)
records = append(records, rows[0].cells)
for _, row := range rows[1:] {
records = append(records, row.cells)
dataRows = append(dataRows, row.number)
}
return cleanIllegalControlChars(records), dataRows, rows[0].number
}
func spreadsheetCellString(value any) string {
switch value := value.(type) {
case nil:
return ""
case time.Time:
return value.Format(time.RFC3339)
default:
return fmt.Sprint(value)
}
}
func rowIsEmpty(row []string) bool {
for _, cell := range row {
if strings.TrimSpace(cell) != "" {
return false
}
}
return true
}
+172
View File
@@ -2,11 +2,14 @@ package parser
import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"errors"
"image"
"image/png"
"io"
"slices"
"strings"
"testing"
@@ -33,6 +36,175 @@ func createTestExcelBytes(t *testing.T) []byte {
return buf.Bytes()
}
// genuineBIFF8FixtureBase64 was generated with xlwt 1.3.0. It contains two
// worksheets and is an OLE compound document with a BIFF8 Workbook stream,
// rather than OOXML bytes renamed with an .xls extension.
const genuineBIFF8FixtureBase64 = `
0M8R4KGxGuEAAAAAAAAAAAAAAAAAAAAAPgADAP7/CQAGAAAAAAAAAAAAAAABAAAACQAAAAAAAAAAEAAA/v///wAAAAD+////AAAAAAgAAAD/////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////8JCBAAAAYFALsNzAcAAAAABgAAAOEAAgCwBMEA
AgAAAOIAAABcAHAATm9uZSAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIEIAAgCwBGEBAgAAAD0BBAABAAIAnAACAA4AGQACAAAAEgACAAAAYwACAAAAEwACAAAArwECAAAA
vAECAAAAQAACAAAAjQACAAAAPQASAOABWgDPP04qOAAAAAAAAQBYAiIAAgAAAA4AAgABALcBAgAAANoAAgAAADEAFQDIAAAA/3+QAQAAAAABAAUAQXJpYWwx
ABUAyAAAAP9/kAEAAAAAAQAFAEFyaWFsMQAVAMgAAAD/f5ABAAAAAAEABQBBcmlhbDEAFQDIAAAA/3+QAQAAAAABAAUAQXJpYWwxABUAyAAAAP9/kAEAAAAA
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
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAP7////9/////v//////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////9SAG8AbwB0ACAA
RQBuAHQAcgB5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFgAFAf//////////AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAP7///8AAAAAAAAAAFcAbwByAGsAYgBvAG8AawAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAASAAIB////////////////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH///////////////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAD+////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf//////////
/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP7///8AAAAAAAAAAA==
`
// formulaErrorBIFF8FixtureGzipBase64 was generated with xlwt 1.3.0 and
// patched to carry the cached BIFF8 error value for the formula 1/0. It is
// compressed to keep the mostly empty OLE container compact in source.
const formulaErrorBIFF8FixtureGzipBase64 = `
H4sIAAAAAAAC/+1YPWgUQRT+ZnP/JJe9eBES8TgCRhPTiI1NslGIqQzRRhFBL+YKSbiT1UYbo/FKQbBSbAJpbKIW/qGFdhZCRAtBEO60tBIULJLbvPl2Vy6JRQ40GJlvmTfv3ps3827mzZudfbuYqc497K5hDYbQgrqXRKxBpqQkwx82RO95mg3rhBTPYEshmZCFjEXxvO1NXK+hXu8aLDyIvBIKfJZyCucxVi4V85uIQ/ShoLQPg0IV7ookjS561UF6lnQb6X22fEE6TMkN0kFpW1UnseiM9R8IoviE1UNdGrrfJ7T5SMk+dOK1juIrN5XfNoqD7rnC9L+pyEVaMQ9Zt9FiqegWpqvIygLO44eXB76HO/Vl3sg3V64g8p+r5fHfyG9ZEWAG3hkGeEUC8lGLvwlHXLfsXliSLKuCxBuFL0zpJMxNa6/atG0M5lahk2gnn2FI25KWl+59e3dkYtw5TckME7Xf6y7tATxc1RZinKbG+qXvJ7+X9Bp73UG+mzQrXkndO94ZMIdn2eY6tb0yzn7ivbO7gd8jfOXr0ae5yhenT/iF0drl7MIHZw49crxMir1+ZjGgBtSd2xrPnLBWwdb/RNq1Lg0kLDvw3QvOrHYsI0U2Q6pbqDUtYthJmZ6xOA8xXxVHTiEn8ztkdeAxZ2W44TxMwcDAwMDAwMDAwGBrQQWv+/reEfGvGbxOxIPvOstS6uYzyX+LYyjLc1EupiMoSe3iUlPxsx1RFfalNmgTfi/UOC6ju5jCBP2Yajp+5TqoGv/Phg3tP7eFmh2/3oyff3n8FQEt9k4AFgAA
`
func TestXLSParser_PreservesFormulaErrors(t *testing.T) {
compressed, err := base64.StdEncoding.DecodeString(strings.TrimSpace(formulaErrorBIFF8FixtureGzipBase64))
if err != nil {
t.Fatalf("decode formula-error fixture: %v", err)
}
zr, err := gzip.NewReader(bytes.NewReader(compressed))
if err != nil {
t.Fatalf("open formula-error fixture: %v", err)
}
data, err := io.ReadAll(zr)
if err != nil {
t.Fatalf("decompress formula-error fixture: %v", err)
}
if err := zr.Close(); err != nil {
t.Fatalf("close formula-error fixture: %v", err)
}
p, err := NewXLSParser("excelize")
if err != nil {
t.Fatalf("NewXLSParser failed: %v", err)
}
res := p.ParseWithResult(t.Context(), "formula-error.xls", data)
if res.Err != nil {
t.Fatalf("ParseWithResult failed: %v", res.Err)
}
if len(res.JSON) != 1 {
t.Fatalf("res.JSON len = %d, want 1", len(res.JSON))
}
text, _ := res.JSON[0]["text"].(string)
if !strings.Contains(text, "<tr><td>#DIV/0!</td></tr>") {
t.Fatalf("table markup = %q, want formula error row", text)
}
}
func TestXLSParser_GenuineBIFF8SpreadsheetJSONOutput(t *testing.T) {
data, err := base64.StdEncoding.DecodeString(genuineBIFF8FixtureBase64)
if err != nil {
t.Fatalf("decode BIFF8 fixture: %v", err)
}
p, err := NewXLSParser("excelize")
if err != nil {
t.Fatalf("NewXLSParser failed: %v", err)
}
res := p.ParseWithResult(t.Context(), "inventory.xls", data)
if res.Err != nil {
t.Fatalf("ParseWithResult failed: %v", res.Err)
}
if res.OutputFormat != spreadsheetOutputFormat {
t.Fatalf("res.OutputFormat = %q, want %q", res.OutputFormat, spreadsheetOutputFormat)
}
if got := res.File["format"]; got != "xls" {
t.Fatalf("res.File[format] = %v, want xls", got)
}
if got := res.File["sheets"]; got != 2 {
t.Fatalf("res.File[sheets] = %v, want 2", got)
}
if len(res.JSON) != 2 {
t.Fatalf("res.JSON len = %d, want one table for each sheet", len(res.JSON))
}
first := res.JSON[0]
text, _ := first["text"].(string)
if !strings.Contains(text, "<caption>Inventory</caption>") ||
!strings.Contains(text, "<tr><th>Name</th><th>Count</th></tr>") ||
!strings.Contains(text, "<tr><td>Apples</td><td>3</td></tr>") {
t.Fatalf("first table markup = %q, want BIFF8 sheet data", text)
}
if first["sheet_index"] != 1 {
t.Fatalf("first sheet_index = %v, want 1", first["sheet_index"])
}
positions, ok := first["positions"].([][]float64)
if !ok || len(positions) != 3 {
t.Fatalf("first positions = %#v, want header plus two data rows", first["positions"])
}
if got, want := positions[1], []float64{1, 2, 2, 1, 2}; !slices.Equal(got, want) {
t.Fatalf("first data row position = %v, want %v", got, want)
}
if got, want := positions[2], []float64{1, 3, 3, 1, 2}; !slices.Equal(got, want) {
t.Fatalf("last row position = %v, want %v", got, want)
}
second := res.JSON[1]
secondText, _ := second["text"].(string)
if !strings.Contains(secondText, "<caption>Notes</caption>") ||
!strings.Contains(secondText, "<tr><td>Legacy</td><td>BIFF8</td></tr>") {
t.Fatalf("second table markup = %q, want second BIFF8 sheet data", secondText)
}
if second["sheet_index"] != 2 {
t.Fatalf("second sheet_index = %v, want 2", second["sheet_index"])
}
}
func TestXLSParser_SpreadsheetJSONOutput(t *testing.T) {
data := createTestExcelBytes(t)
p, err := NewXLSParser("excelize")