package sberhealth import ( "bytes" "encoding/binary" "fmt" "io" "math" "strconv" "unicode/utf16" "github.com/extrame/ole2" ) // Минимальный читатель книг Excel 97–2003 (BIFF8). Нужен потому, что общая таблица строк (SST) // у больших выгрузок разбивается на записи CONTINUE посреди строки, и библиотека extrame/xls на // этих границах смещает значения: часть ячеек получала чужие данные, а все строки ниже сдвигались. // Здесь строки собираются строго по спецификации: у продолжения строки первым байтом идёт флаг // кодировки (1 или 2 байта на символ), а блоки форматирования и фонетики могут пересекать границу. // // Поддерживаются только ячейки, которые встречаются в выгрузках: LABELSST, LABEL, NUMBER, RK, MULRK // (и пустые BLANK/MULBLANK). Формулы, ошибки и прочее — явная ошибка: данные нельзя тихо терять. const ( recEOF = 0x000A recFormula = 0x0006 recBoundSheet = 0x0085 recMulRK = 0x00BD recMulBlank = 0x00BE recRString = 0x00D6 recSST = 0x00FC recLabelSST = 0x00FD recContinue = 0x003C recBlank = 0x0201 recNumber = 0x0203 recLabel = 0x0204 recBoolErr = 0x0205 recString = 0x0207 recRK = 0x027E recBOF = 0x0809 biff8Version = 0x0600 maxSheetRows = 50000 maxSheetCols = 100 maxSSTItems = 1_000_000 ) type record struct { id uint16 data []byte off int } type biffSheet struct { name string rows [][]string } // workbookStream достаёт из контейнера OLE2 поток Workbook (или Book) целиком. func workbookStream(data []byte) ([]byte, error) { o, err := ole2.Open(bytes.NewReader(data), "utf-8") if err != nil || o == nil { return nil, fmt.Errorf("контейнер XLS не открылся") } dir, err := o.ListDir() if err != nil { return nil, fmt.Errorf("каталог XLS не прочитан") } var book, root *ole2.File for _, f := range dir { switch f.Name() { case "Workbook", "Book": book = f case "Root Entry": root = f } } if book == nil { return nil, fmt.Errorf("в XLS нет потока Workbook") } stream, err := io.ReadAll(io.LimitReader(o.OpenFile(book, root), maxDownload+1)) if err != nil { return nil, fmt.Errorf("поток Workbook не дочитался") } if len(stream) > maxDownload { return nil, fmt.Errorf("поток Workbook слишком велик") } return stream, nil } func readRecords(stream []byte) ([]record, error) { var recs []record for pos := 0; pos < len(stream); { if pos+4 > len(stream) { if allZero(stream[pos:]) { break } return nil, fmt.Errorf("оборванная запись в конце книги") } id := binary.LittleEndian.Uint16(stream[pos:]) n := int(binary.LittleEndian.Uint16(stream[pos+2:])) if id == 0 && n == 0 && allZero(stream[pos:]) { break // выравнивание нулями после последней записи } if pos+4+n > len(stream) { return nil, fmt.Errorf("запись выходит за пределы книги") } recs = append(recs, record{id: id, data: stream[pos+4 : pos+4+n], off: pos}) pos += 4 + n if len(recs) > 2_000_000 { return nil, fmt.Errorf("слишком много записей в книге") } } return recs, nil } func allZero(b []byte) bool { for _, v := range b { if v != 0 { return false } } return true } // chunkReader читает данные, разбитые на записи SST и CONTINUE. type chunkReader struct { chunks [][]byte ci int pos int } func (r *chunkReader) avail() int { if r.ci >= len(r.chunks) { return 0 } return len(r.chunks[r.ci]) - r.pos } // advance переходит к следующему непустому фрагменту; false — данные кончились. func (r *chunkReader) advance() bool { for { r.ci++ r.pos = 0 if r.ci >= len(r.chunks) { return false } if len(r.chunks[r.ci]) > 0 { return true } } } // take отдаёт n непрерывных байт из текущего фрагмента (заголовки полей через границу не переходят). func (r *chunkReader) take(n int) ([]byte, error) { if r.avail() == 0 && !r.advance() { return nil, fmt.Errorf("таблица строк оборвана") } if r.avail() < n { return nil, fmt.Errorf("поле таблицы строк разорвано границей записи") } b := r.chunks[r.ci][r.pos : r.pos+n] r.pos += n return b, nil } // skip пропускает n байт, переходя между фрагментами без флага кодировки. func (r *chunkReader) skip(n int) error { for n > 0 { if r.avail() == 0 && !r.advance() { return fmt.Errorf("таблица строк оборвана при пропуске форматирования") } k := min(n, r.avail()) r.pos += k n -= k } return nil } // readString читает одну строку Unicode (cch, флаги, символы, блоки форматирования/фонетики). func (r *chunkReader) readString() (string, error) { head, err := r.take(3) if err != nil { return "", err } cch := int(binary.LittleEndian.Uint16(head)) flags := head[2] high := flags&0x01 != 0 runs, ext := 0, 0 if flags&0x08 != 0 { b, err := r.take(2) if err != nil { return "", err } runs = int(binary.LittleEndian.Uint16(b)) } if flags&0x04 != 0 { b, err := r.take(4) if err != nil { return "", err } ext = int(binary.LittleEndian.Uint32(b)) } units := make([]uint16, 0, cch) for remaining := cch; remaining > 0; { if r.avail() == 0 { // Продолжение строки: первый байт нового фрагмента — флаг кодировки. if !r.advance() { return "", fmt.Errorf("таблица строк оборвана внутри строки") } b, err := r.take(1) if err != nil { return "", err } high = b[0]&0x01 != 0 } width := 1 if high { width = 2 } n := min(remaining, r.avail()/width) if n == 0 { return "", fmt.Errorf("символ разорван границей записи") } raw, _ := r.take(n * width) for i := 0; i < n; i++ { if high { units = append(units, binary.LittleEndian.Uint16(raw[i*2:])) } else { units = append(units, uint16(raw[i])) } } remaining -= n } if err := r.skip(runs*4 + ext); err != nil { return "", err } return string(utf16.Decode(units)), nil } // parseSST разбирает общую таблицу строк, начиная с записи SST (i) и её CONTINUE. func parseSST(recs []record, i int) ([]string, error) { data := recs[i].data if len(data) < 8 { return nil, fmt.Errorf("слишком короткая таблица строк") } unique := int(binary.LittleEndian.Uint32(data[4:])) if unique < 0 || unique > maxSSTItems { return nil, fmt.Errorf("неверный размер таблицы строк") } chunks := [][]byte{data[8:]} for j := i + 1; j < len(recs) && recs[j].id == recContinue; j++ { chunks = append(chunks, recs[j].data) } r := &chunkReader{chunks: chunks} out := make([]string, 0, unique) for k := 0; k < unique; k++ { s, err := r.readString() if err != nil { return nil, fmt.Errorf("строка %d из %d: %w", k+1, unique, err) } out = append(out, s) } return out, nil } func decodeRK(v uint32) float64 { var f float64 if v&2 != 0 { f = float64(int32(v) >> 2) } else { f = math.Float64frombits(uint64(v&0xFFFFFFFC) << 32) } if v&1 != 0 { f /= 100 } return f } func numberString(f float64) string { if f == math.Trunc(f) && math.Abs(f) < 1e15 { return strconv.FormatInt(int64(f), 10) } return strconv.FormatFloat(f, 'f', -1, 64) } type boundSheet struct { name string offset int kind byte } func parseBoundSheet(d []byte) (boundSheet, error) { if len(d) < 8 { return boundSheet{}, fmt.Errorf("повреждённая запись листа") } cch, flags := int(d[6]), d[7] width := 1 if flags&1 != 0 { width = 2 } if len(d) < 8+cch*width { return boundSheet{}, fmt.Errorf("повреждённое имя листа") } units := make([]uint16, cch) for i := range units { if width == 2 { units[i] = binary.LittleEndian.Uint16(d[8+i*2:]) } else { units[i] = uint16(d[8+i]) } } return boundSheet{name: string(utf16.Decode(units)), offset: int(binary.LittleEndian.Uint32(d)), kind: d[5]}, nil } // parseBIFF8 читает все листы книги из потока Workbook. func parseBIFF8(stream []byte) ([]biffSheet, error) { recs, err := readRecords(stream) if err != nil { return nil, err } if len(recs) == 0 || recs[0].id != recBOF || len(recs[0].data) < 4 || binary.LittleEndian.Uint16(recs[0].data) != biff8Version { return nil, fmt.Errorf("поддерживается только формат Excel 97–2003 (BIFF8)") } byOffset := make(map[int]int, len(recs)) for i, rec := range recs { byOffset[rec.off] = i } var sst []string var bounds []boundSheet for i, rec := range recs { if i > 0 && rec.id == recBOF { break // началась первая страница: общая часть книги закончилась } switch rec.id { case recSST: if sst, err = parseSST(recs, i); err != nil { return nil, err } case recBoundSheet: b, err := parseBoundSheet(rec.data) if err != nil { return nil, err } bounds = append(bounds, b) } } sheets := make([]biffSheet, 0, len(bounds)) for _, b := range bounds { start, ok := byOffset[b.offset] if !ok || recs[start].id != recBOF { return nil, fmt.Errorf("лист %q: не найдено начало данных", b.name) } rows, err := readSheet(recs, start, sst) if err != nil { return nil, fmt.Errorf("лист %q: %w", b.name, err) } sheets = append(sheets, biffSheet{name: b.name, rows: rows}) } return sheets, nil } type cellValue struct { row, col int text string } func readSheet(recs []record, start int, sst []string) ([][]string, error) { var cells []cellValue maxRow, maxCol := -1, -1 put := func(row, col int, text string) error { if row >= maxSheetRows || col >= maxSheetCols { return fmt.Errorf("лист слишком велик (строка %d, столбец %d)", row+1, col+1) } cells = append(cells, cellValue{row, col, text}) maxRow, maxCol = max(maxRow, row), max(maxCol, col) return nil } depth := 0 for i := start; i < len(recs); i++ { rec := recs[i] d := rec.data switch rec.id { case recBOF: depth++ continue case recEOF: depth-- if depth == 0 { i = len(recs) } continue } if depth != 1 { continue } switch rec.id { case recLabelSST: if len(d) < 10 { return nil, fmt.Errorf("повреждённая ячейка LABELSST") } idx := int(binary.LittleEndian.Uint32(d[6:])) if idx < 0 || idx >= len(sst) { return nil, fmt.Errorf("ячейка ссылается на несуществующую строку %d", idx) } if err := put(int(binary.LittleEndian.Uint16(d)), int(binary.LittleEndian.Uint16(d[2:])), sst[idx]); err != nil { return nil, err } case recLabel: if len(d) < 8 { return nil, fmt.Errorf("повреждённая ячейка LABEL") } r := &chunkReader{chunks: [][]byte{d[6:]}} s, err := r.readString() if err != nil { return nil, err } if err := put(int(binary.LittleEndian.Uint16(d)), int(binary.LittleEndian.Uint16(d[2:])), s); err != nil { return nil, err } case recNumber: if len(d) < 14 { return nil, fmt.Errorf("повреждённая ячейка NUMBER") } f := math.Float64frombits(binary.LittleEndian.Uint64(d[6:])) if err := put(int(binary.LittleEndian.Uint16(d)), int(binary.LittleEndian.Uint16(d[2:])), numberString(f)); err != nil { return nil, err } case recRK: if len(d) < 10 { return nil, fmt.Errorf("повреждённая ячейка RK") } if err := put(int(binary.LittleEndian.Uint16(d)), int(binary.LittleEndian.Uint16(d[2:])), numberString(decodeRK(binary.LittleEndian.Uint32(d[6:])))); err != nil { return nil, err } case recMulRK: if len(d) < 6 { return nil, fmt.Errorf("повреждённая запись MULRK") } row, first := int(binary.LittleEndian.Uint16(d)), int(binary.LittleEndian.Uint16(d[2:])) n := (len(d) - 6) / 6 for k := 0; k < n; k++ { v := binary.LittleEndian.Uint32(d[4+k*6+2:]) if err := put(row, first+k, numberString(decodeRK(v))); err != nil { return nil, err } } case recBlank, recMulBlank, recString: // пустые ячейки значения не несут case recFormula, recBoolErr, recRString: return nil, fmt.Errorf("неподдерживаемый тип ячейки 0x%04X: выгрузка не должна содержать формул и ошибок", rec.id) } } rows := make([][]string, maxRow+1) width := make([]int, maxRow+1) for _, c := range cells { width[c.row] = max(width[c.row], c.col+1) } for r := range rows { if width[r] > 0 { rows[r] = make([]string, width[r]) } } for _, c := range cells { rows[c.row][c.col] = c.text } return rows, nil }