cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Bound shared formula expansion and look up column styles once

Dami committed 627e90e
Found by fuzzing: a small file of shared formulas could expand to
gigabytes of formula text, and a sheet of many <col> elements made
every blank cell scan them all. Shared formulas now expand only for
cells that are imported, within 256 MB of text.
commit
627e90e5178c9b8dacc234d94d06c706e498f058
parent
b704e10b38d0c7f5f4443b6673ac6b181a9ca274
author
Dami <dami.etoile@gmail.com>
5 files changed, +66 -29
Minternal/fileio/xlsxbook.go1+
Minternal/fileio/xlsximport.go21++++++---
Minternal/fileio/xlsxpkg.go2+
Minternal/fileio/xlsxread_test.go6++-
Minternal/fileio/xlsxsheet.go65+++++++++++++++++++++---------
M internal/fileio/xlsxbook.go +1 -0
M internal/fileio/xlsximport.go +14 -7
@@ -97,7 +97,9 @@ func (bk *xlsxBook) importSheet(ctx context.Context, b *builder, i int, report f
9797 }9898 report(r.row.num)9999 }100- bk.importRow(b, r)100+ if err := bk.importRow(b, r); err != nil {101+ return 0, fmt.Errorf("sheet %s: %w", bk.sheets[i].name, err)102+ }101103 last = max(last, r.row.num)102104 }103105 for c := range sheet.MaxCols {
@@ -114,7 +116,7 @@ func (bk *xlsxBook) importSheet(ctx context.Context, b *builder, i int, report f
114116 // with neither a value nor a formula only counts for its format, and115117 // only up to the row's last cell with one: formatted blanks after it are116118 // left out.117-func (bk *xlsxBook) importRow(b *builder, r *xlsxSheetReader) {119+func (bk *xlsxBook) importRow(b *builder, r *xlsxSheetReader) error {118120 row := &r.row119121 cells := row.cells120122 if !slices.IsSortedFunc(cells, func(a, b xlsxCell) int { return a.col - b.col }) {
@@ -127,7 +129,7 @@ func (bk *xlsxBook) importRow(b *builder, r *xlsxSheetReader) {
127129 }128130 }129131 if lastCol == 0 || !b.fits(sheet.Addr{Col: lastCol - 1, Row: row.num - 1}) && row.num > sheet.MaxRows {130- return132+ return nil131133 }132134 j := 0133135 for col := 1; col <= min(lastCol, sheet.MaxCols); col++ {
@@ -152,13 +154,18 @@ func (bk *xlsxBook) importRow(b *builder, r *xlsxSheetReader) {
152154 b.put(a, "", st.format, st.style)153155 continue154156 }155- bk.importCell(b, a, c, st)157+ f, err := r.formula(c)158+ if err != nil {159+ return err160+ }161+ bk.importCell(b, a, c, f, st)156162 }163+ return nil157164 }158165 159-func (bk *xlsxBook) importCell(b *builder, a sheet.Addr, c *xlsxCell, st xlsxStyle) {160- if c.formula != "" {161- b.formula(a, fromExcelFormula(c.formula), st.format, st.style, func() { bk.keep(b, a, c, st) })166+func (bk *xlsxBook) importCell(b *builder, a sheet.Addr, c *xlsxCell, formula string, st xlsxStyle) {167+ if formula != "" {168+ b.formula(a, fromExcelFormula(formula), st.format, st.style, func() { bk.keep(b, a, c, st) })162169 return163170 }164171 if c.value == "" {
M internal/fileio/xlsxpkg.go +2 -0
M internal/fileio/xlsxread_test.go +4 -2
M internal/fileio/xlsxsheet.go +45 -20
@@ -6,6 +6,8 @@ import (
66 "io"77 "strconv"88 "strings"9+10+ "github.com/FelineStateMachine/012/internal/sheet"911 )1012 1113 // Excel's own limits on a worksheet. A reference past them is an error,
@@ -17,12 +19,12 @@ const (
1719 1820 // xlsxCell is a cell of a worksheet row, as the reader passes it on.1921 type xlsxCell struct {20- col int // from 121- style int // the cell format index (s), 0 when not given22- typ string // the type (t): s, str, inlineStr, b, e, n, d or ""23- value string // the value, a shared string looked up24- formula string // the formula, a shared formula expanded25- hasF bool // the cell has a formula element, even an empty one22+ col int // from 123+ style int // the cell format index (s), 0 when not given24+ typ string // the type (t): s, str, inlineStr, b, e, n, d or ""25+ value string // the value, a shared string looked up26+ f cellFormula // its <f>, see xlsxSheetReader.formula27+ hasF bool // the cell has a formula element, even an empty one2628 }2729 2830 // kept reports whether excelize's row reader would return the cell: it
@@ -47,6 +49,8 @@ type xlsxSheetReader struct {
4749 defaultWidth float644850 baseWidth int4951 52+ colStyles []int // by column, see colStyle53+5054 inData bool // inside <sheetData>5155 row xlsxRowData5256 shared map[int]sharedFormula
@@ -120,8 +124,22 @@ func (r *xlsxSheetReader) readHead() error {
120124 }121125 122126 // colStyle is the cell format of column col (from 1): the first <col>123-// covering it with a style.127+// covering it with a style. Columns up to sheet.MaxCols, the ones asked128+// for once per cell, are looked up once.124129 func (r *xlsxSheetReader) colStyle(col int) int {130+ if r.colStyles == nil {131+ r.colStyles = make([]int, sheet.MaxCols)132+ for _, c := range r.cols {133+ for i := max(c.min, 1); i <= min(c.max, sheet.MaxCols); i++ {134+ if r.colStyles[i-1] == 0 {135+ r.colStyles[i-1] = c.style136+ }137+ }138+ }139+ }140+ if col >= 1 && col <= len(r.colStyles) {141+ return r.colStyles[col-1]142+ }125143 for _, c := range r.cols {126144 if c.min <= col && col <= c.max && c.style != 0 {127145 return c.style
@@ -258,7 +276,10 @@ func (r *xlsxSheetReader) readCell(se xml.StartElement, prev int) (int, error) {
258276 }259277 c.value = r.value(c.typ, v, is, hasIS)260278 if c.hasF {261- c.formula = r.formula(f, c.col, r.row.num)279+ c.f = f280+ if _, ok := r.shared[f.si]; !ok && f.typ == "shared" && f.si >= 0 && f.ref != "" {281+ r.shared[f.si] = sharedFormula{text: f.text, col: c.col, row: r.row.num}282+ }262283 }263284 r.row.cells = append(r.row.cells, c)264285 return c.col, nil
@@ -302,22 +323,26 @@ func (r *xlsxSheetReader) readFormula(se xml.StartElement) (cellFormula, error)
302323 return f, err303324 }304325 305-// formula is the formula of the cell at col, row: its own text, or for306-// a cell sharing a formula, the master's with its relative references307-// moved by the distance between the cells.308-func (r *xlsxSheetReader) formula(f cellFormula, col, row int) string {309- if f.typ != "shared" || f.si < 0 {310- return f.text326+// formula is the formula of cell c of the current row: its own text,327+// or for a cell sharing a formula, the first cell's with its relative328+// references moved by the distance between the cells ("" when the first329+// is missing). Shared formulas are expanded only on request, and only330+// up to the limit on their text: a small file of shared formulas could331+// otherwise expand to gigabytes.332+func (r *xlsxSheetReader) formula(c *xlsxCell) (string, error) {333+ f := c.f334+ if !c.hasF || f.typ != "shared" || f.si < 0 {335+ return f.text, nil311336 }312337 m, ok := r.shared[f.si]313338 if !ok {314- if f.ref == "" {315- return "" // the master is missing316- }317- m = sharedFormula{text: f.text, col: col, row: row}318- r.shared[f.si] = m339+ return "", nil340+ }341+ out := shiftFormula(m.text, c.col-m.col, r.row.num-m.row)342+ if r.bk.expanded += int64(len(out)); r.bk.expanded > r.bk.pkg.lim.shared {343+ return "", fmt.Errorf("shared formulas expand to more than %s: %w", mb(r.bk.pkg.lim.shared), errXLSXLimit)319344 }320- return shiftFormula(m.text, col-m.col, row-m.row)345+ return out, nil321346 }322347 323348 // parseCellRef parses an A1 reference (without $) into a column and row