package sberhealth import ( "encoding/binary" "fmt" "math" "math/rand" "reflect" "strings" "testing" "unicode/utf16" ) // ---- сборка синтетической книги BIFF8: без персональных данных ---- func biffRec(id uint16, data []byte) []byte { out := make([]byte, 4, 4+len(data)) binary.LittleEndian.PutUint16(out, id) binary.LittleEndian.PutUint16(out[2:], uint16(len(data))) return append(out, data...) } func le16(v int) []byte { b := make([]byte, 2); binary.LittleEndian.PutUint16(b, uint16(v)); return b } func le32(v uint32) []byte { b := make([]byte, 4); binary.LittleEndian.PutUint32(b, v); return b } func latin1(s string) bool { for _, r := range s { if r > 0xFF { return false } } return true } type testString struct { text string runs int // блоки форматирования (по 4 байта) ext int // байты фонетики } // sstRecords раскладывает строки по записям SST и CONTINUE так же, как это делает Excel: // строка может прерваться на любой границе, а у продолжения первым байтом идёт флаг кодировки; // на каждой границе кодировка может поменяться. func sstRecords(strs []testString, maxData int) []byte { var chunks [][]byte cur := append(le32(uint32(len(strs))), le32(uint32(len(strs)))...) flush := func() { chunks = append(chunks, cur); cur = nil } space := func() int { return maxData - len(cur) } for _, s := range strs { units := utf16.Encode([]rune(s.text)) headLen := 3 if s.runs > 0 { headLen += 2 } if s.ext > 0 { headLen += 4 } if space() < headLen+1 { // заголовок не делится между записями flush() } flags := byte(0) if !latin1(s.text) { flags |= 1 } if s.runs > 0 { flags |= 0x08 } if s.ext > 0 { flags |= 0x04 } cur = append(cur, le16(len(units))...) cur = append(cur, flags) if s.runs > 0 { cur = append(cur, le16(s.runs)...) } if s.ext > 0 { cur = append(cur, le32(uint32(s.ext))...) } high := flags&1 != 0 for done := 0; done < len(units); { width := 1 if high { width = 2 } n := min(len(units)-done, space()/width) if n == 0 { flush() rest := units[done:] high = false for _, u := range rest { if u > 0xFF { high = true } } flag := byte(0) if high { flag = 1 } cur = append(cur, flag) continue } for _, u := range units[done : done+n] { if high { cur = append(cur, byte(u), byte(u>>8)) } else { cur = append(cur, byte(u)) } } done += n } for left := s.runs*4 + s.ext; left > 0; { if space() == 0 { flush() } n := min(left, space()) cur = append(cur, make([]byte, n)...) left -= n } } flush() var out []byte for i, c := range chunks { id := uint16(recContinue) if i == 0 { id = recSST } out = append(out, biffRec(id, c)...) } return out } func boundSheetRec(name string, offset int) []byte { d := append(le32(uint32(offset)), 0, 0, byte(len(name)), 0) return biffRec(recBoundSheet, append(d, []byte(name)...)) } func bofRec(kind int) []byte { return biffRec(recBOF, append(append(le16(biff8Version), le16(kind)...), make([]byte, 12)...)) } func labelSSTCell(row, col, idx int) []byte { return biffRec(recLabelSST, append(append(append(le16(row), le16(col)...), le16(0)...), le32(uint32(idx))...)) } func numberCell(row, col int, f float64) []byte { d := append(append(le16(row), le16(col)...), le16(0)...) b := make([]byte, 8) binary.LittleEndian.PutUint64(b, math.Float64bits(f)) return biffRec(recNumber, append(d, b...)) } func rkCell(row, col int, rk uint32) []byte { return biffRec(recRK, append(append(append(le16(row), le16(col)...), le16(0)...), le32(rk)...)) } func mulRKCells(row, first int, rks ...uint32) []byte { d := append(le16(row), le16(first)...) for _, rk := range rks { d = append(d, le16(0)...) d = append(d, le32(rk)...) } d = append(d, le16(first+len(rks)-1)...) return biffRec(recMulRK, d) } // buildBook собирает книгу: общая часть (SST) и один лист с готовыми записями ячеек. func buildBook(strs []testString, maxData int, name string, cells ...[]byte) []byte { sst := sstRecords(strs, maxData) globalsLen := len(bofRec(5)) + len(boundSheetRec(name, 0)) + len(sst) + len(biffRec(recEOF, nil)) var out []byte out = append(out, bofRec(5)...) out = append(out, boundSheetRec(name, globalsLen)...) out = append(out, sst...) out = append(out, biffRec(recEOF, nil)...) out = append(out, bofRec(0x10)...) for _, c := range cells { out = append(out, c...) } out = append(out, biffRec(recEOF, nil)...) return out } // Главный регрессионный тест: строки через границы CONTINUE при любых размерах записей. func TestSSTStringsAcrossContinueBoundaries(t *testing.T) { rng := rand.New(rand.NewSource(7)) words := []string{"Врач", "Невролог", "Лабораторная диагностика", "ул. Ляхова, д. 7", "Report", "Дата создания записи", "Привет, мир", "Qwerty"} var strs []testString var want []string for i := 0; i < 220; i++ { var parts []string for k := rng.Intn(4); k >= 0; k-- { parts = append(parts, words[rng.Intn(len(words))]) } s := strings.Join(parts, " ") if i%7 == 0 { s = fmt.Sprintf("%02d.%02d.2026 %02d:%02d", i%28+1, i%12+1, i%24, i%60) // ASCII: сжатая форма } ts := testString{text: s} if i%11 == 0 { ts.runs = 2 } if i%13 == 0 { ts.ext = 9 } strs = append(strs, ts) want = append(want, s) } for _, maxData := range []int{24, 25, 31, 40, 63, 64, 97, 128, 255, 1000, 8224} { cells := make([][]byte, len(strs)) for i := range strs { cells[i] = labelSSTCell(i, 0, i) } sheets, err := parseBIFF8(buildBook(strs, maxData, "Report", cells...)) if err != nil { t.Fatalf("maxData=%d: %v", maxData, err) } if len(sheets) != 1 || sheets[0].name != "Report" || len(sheets[0].rows) != len(want) { t.Fatalf("maxData=%d: неверный состав листов", maxData) } for i, w := range want { if got := sheets[0].rows[i][0]; got != w { t.Fatalf("maxData=%d, строка %d: получили %q, ожидали %q", maxData, i, got, w) } } } } func TestNumbersAndShape(t *testing.T) { strs := []testString{{text: "a"}, {text: "b"}} book := buildBook(strs, 100, "Report", labelSSTCell(0, 1, 1), // ряд 0 начинается с пустого столбца 0 numberCell(1, 0, 59935608), numberCell(1, 1, 2500.5), rkCell(2, 0, uint32(7<<2)|2), // целое 7 rkCell(2, 1, uint32(12345<<2)|2|1), // 123,45 mulRKCells(3, 2, uint32(10<<2)|2, uint32(20<<2)|2), labelSSTCell(4, 0, 0), ) sheets, err := parseBIFF8(book) if err != nil { t.Fatal(err) } want := [][]string{{"", "b"}, {"59935608", "2500.5"}, {"7", "123.45"}, {"", "", "10", "20"}, {"a"}} if !reflect.DeepEqual(sheets[0].rows, want) { t.Fatalf("получили %q, ожидали %q", sheets[0].rows, want) } } func TestUnsupportedAndBrokenInput(t *testing.T) { strs := []testString{{text: "x"}} formula := biffRec(recFormula, make([]byte, 20)) if _, err := parseBIFF8(buildBook(strs, 100, "Report", labelSSTCell(0, 0, 0), formula)); err == nil { t.Error("формула должна давать явную ошибку, а не пропускаться") } if _, err := parseBIFF8(buildBook(strs, 100, "Report", labelSSTCell(0, 0, 5))); err == nil { t.Error("ссылка на несуществующую строку должна давать ошибку") } good := buildBook(strs, 100, "Report", labelSSTCell(0, 0, 0)) for cut := 1; cut < len(good); cut += 7 { func() { defer func() { if recover() != nil { t.Fatalf("обрезанная книга (%d байт) вызвала панику", cut) } }() _, _ = parseBIFF8(good[:cut]) }() } bad := append([]byte(nil), good...) binary.LittleEndian.PutUint16(bad[4:], 0x0500) // BIFF5 if _, err := parseBIFF8(bad); err == nil { t.Error("BIFF5 должен отклоняться") } huge := buildBook(strs, 100, "Report", labelSSTCell(maxSheetRows, 0, 0)) if _, err := parseBIFF8(huge); err == nil { t.Error("слишком большой лист должен отклоняться") } } func TestReadXLSRejectsNonExcel(t *testing.T) { for name, data := range map[string][]byte{ "html": []byte("
| x |