Fix potential file corrupted when changing cell value or the col/row

- Remove shared formula subsequent cell when setting the cell values
- Support adjust table range when removing and inserting column/row
This commit is contained in:
xuri 2022-07-16 12:50:13 +08:00
parent 6429588e14
commit 40ed1d1b81
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
8 changed files with 169 additions and 46 deletions

View File

@ -11,6 +11,13 @@
package excelize
import (
"bytes"
"encoding/xml"
"io"
"strings"
)
type adjustDirection bool
const (
@ -41,6 +48,7 @@ func (f *File) adjustHelper(sheet string, dir adjustDirection, num, offset int)
f.adjustColDimensions(ws, num, offset)
}
f.adjustHyperlinks(ws, sheet, dir, num, offset)
f.adjustTable(ws, sheet, dir, num, offset)
if err = f.adjustMergeCells(ws, dir, num, offset); err != nil {
return err
}
@ -138,6 +146,54 @@ func (f *File) adjustHyperlinks(ws *xlsxWorksheet, sheet string, dir adjustDirec
}
}
// adjustTable provides a function to update the table when inserting or
// deleting rows or columns.
func (f *File) adjustTable(ws *xlsxWorksheet, sheet string, dir adjustDirection, num, offset int) {
if ws.TableParts == nil || len(ws.TableParts.TableParts) == 0 {
return
}
for idx := 0; idx < len(ws.TableParts.TableParts); idx++ {
tbl := ws.TableParts.TableParts[idx]
target := f.getSheetRelationshipsTargetByID(sheet, tbl.RID)
tableXML := strings.ReplaceAll(target, "..", "xl")
content, ok := f.Pkg.Load(tableXML)
if !ok {
continue
}
t := xlsxTable{}
if err := f.xmlNewDecoder(bytes.NewReader(namespaceStrictToTransitional(content.([]byte)))).
Decode(&t); err != nil && err != io.EOF {
return
}
coordinates, err := areaRefToCoordinates(t.Ref)
if err != nil {
return
}
// Remove the table when deleting the header row of the table
if dir == rows && num == coordinates[0] {
ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...)
ws.TableParts.Count = len(ws.TableParts.TableParts)
idx--
continue
}
coordinates = f.adjustAutoFilterHelper(dir, coordinates, num, offset)
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]
if y2-y1 < 2 || x2-x1 < 1 {
ws.TableParts.TableParts = append(ws.TableParts.TableParts[:idx], ws.TableParts.TableParts[idx+1:]...)
ws.TableParts.Count = len(ws.TableParts.TableParts)
idx--
continue
}
t.Ref, _ = f.coordinatesToAreaRef([]int{x1, y1, x2, y2})
if t.AutoFilter != nil {
t.AutoFilter.Ref = t.Ref
}
_, _ = f.setTableHeader(sheet, x1, y1, x2)
table, _ := xml.Marshal(t)
f.saveFileList(tableXML, table)
}
}
// adjustAutoFilter provides a function to update the auto filter when
// inserting or deleting rows or columns.
func (f *File) adjustAutoFilter(ws *xlsxWorksheet, dir adjustDirection, num, offset int) error {
@ -182,10 +238,13 @@ func (f *File) adjustAutoFilterHelper(dir adjustDirection, coordinates []int, nu
if coordinates[3] >= num {
coordinates[3] += offset
}
} else {
if coordinates[2] >= num {
coordinates[2] += offset
}
return coordinates
}
if coordinates[0] >= num {
coordinates[0] += offset
}
if coordinates[2] >= num {
coordinates[2] += offset
}
return coordinates
}

View File

@ -1,6 +1,8 @@
package excelize
import (
"fmt"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -281,7 +283,7 @@ func TestAdjustAutoFilter(t *testing.T) {
Ref: "A1:A3",
},
}, rows, 1, -1))
// testing adjustAutoFilter with illegal cell coordinates.
// Test adjustAutoFilter with illegal cell coordinates.
assert.EqualError(t, f.adjustAutoFilter(&xlsxWorksheet{
AutoFilter: &xlsxAutoFilter{
Ref: "A:B1",
@ -294,6 +296,36 @@ func TestAdjustAutoFilter(t *testing.T) {
}, rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error())
}
func TestAdjustTable(t *testing.T) {
f, sheetName := NewFile(), "Sheet1"
for idx, tableRange := range [][]string{{"B2", "C3"}, {"E3", "F5"}, {"H5", "H8"}, {"J5", "K9"}} {
assert.NoError(t, f.AddTable(sheetName, tableRange[0], tableRange[1], fmt.Sprintf(`{
"table_name": "table%d",
"table_style": "TableStyleMedium2",
"show_first_column": true,
"show_last_column": true,
"show_row_stripes": false,
"show_column_stripes": true
}`, idx)))
}
assert.NoError(t, f.RemoveRow(sheetName, 2))
assert.NoError(t, f.RemoveRow(sheetName, 3))
assert.NoError(t, f.RemoveCol(sheetName, "H"))
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAdjustTable.xlsx")))
f = NewFile()
assert.NoError(t, f.AddTable(sheetName, "A1", "D5", ""))
// Test adjust table with non-table part
f.Pkg.Delete("xl/tables/table1.xml")
assert.NoError(t, f.RemoveRow(sheetName, 1))
// Test adjust table with unsupported charset
f.Pkg.Store("xl/tables/table1.xml", MacintoshCyrillicCharset)
assert.NoError(t, f.RemoveRow(sheetName, 1))
// Test adjust table with invalid table range reference
f.Pkg.Store("xl/tables/table1.xml", []byte(`<table ref="-" />`))
assert.NoError(t, f.RemoveRow(sheetName, 1))
}
func TestAdjustHelper(t *testing.T) {
f := NewFile()
f.NewSheet("Sheet2")
@ -303,10 +335,10 @@ func TestAdjustHelper(t *testing.T) {
f.Sheet.Store("xl/worksheets/sheet2.xml", &xlsxWorksheet{
AutoFilter: &xlsxAutoFilter{Ref: "A1:B"},
})
// testing adjustHelper with illegal cell coordinates.
// Test adjustHelper with illegal cell coordinates.
assert.EqualError(t, f.adjustHelper("Sheet1", rows, 0, 0), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.EqualError(t, f.adjustHelper("Sheet2", rows, 0, 0), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error())
// testing adjustHelper on not exists worksheet.
// Test adjustHelper on not exists worksheet.
assert.EqualError(t, f.adjustHelper("SheetN", rows, 0, 0), "sheet SheetN is not exist")
}

30
cell.go
View File

@ -169,6 +169,21 @@ func (c *xlsxC) hasValue() bool {
return c.S != 0 || c.V != "" || c.F != nil || c.T != ""
}
// removeFormula delete formula for the cell.
func (c *xlsxC) removeFormula(ws *xlsxWorksheet) {
if c.F != nil && c.F.T == STCellFormulaTypeShared && c.F.Ref != "" {
si := c.F.Si
for r, row := range ws.SheetData.Row {
for col, cell := range row.C {
if cell.F != nil && cell.F.Si != nil && *cell.F.Si == *si {
ws.SheetData.Row[r].C[col].F = nil
}
}
}
}
c.F = nil
}
// setCellIntFunc is a wrapper of SetCellInt.
func (f *File) setCellIntFunc(sheet, axis string, value interface{}) error {
var err error
@ -266,7 +281,8 @@ func (f *File) SetCellInt(sheet, axis string, value int) error {
defer ws.Unlock()
cellData.S = f.prepareCellStyle(ws, col, row, cellData.S)
cellData.T, cellData.V = setCellInt(value)
cellData.F, cellData.IS = nil, nil
cellData.removeFormula(ws)
cellData.IS = nil
return err
}
@ -292,7 +308,8 @@ func (f *File) SetCellBool(sheet, axis string, value bool) error {
defer ws.Unlock()
cellData.S = f.prepareCellStyle(ws, col, row, cellData.S)
cellData.T, cellData.V = setCellBool(value)
cellData.F, cellData.IS = nil, nil
cellData.removeFormula(ws)
cellData.IS = nil
return err
}
@ -330,7 +347,8 @@ func (f *File) SetCellFloat(sheet, axis string, value float64, precision, bitSiz
defer ws.Unlock()
cellData.S = f.prepareCellStyle(ws, col, row, cellData.S)
cellData.T, cellData.V = setCellFloat(value, precision, bitSize)
cellData.F, cellData.IS = nil, nil
cellData.removeFormula(ws)
cellData.IS = nil
return err
}
@ -356,7 +374,8 @@ func (f *File) SetCellStr(sheet, axis, value string) error {
defer ws.Unlock()
cellData.S = f.prepareCellStyle(ws, col, row, cellData.S)
cellData.T, cellData.V, err = f.setCellString(value)
cellData.F, cellData.IS = nil, nil
cellData.removeFormula(ws)
cellData.IS = nil
return err
}
@ -455,7 +474,8 @@ func (f *File) SetCellDefault(sheet, axis, value string) error {
defer ws.Unlock()
cellData.S = f.prepareCellStyle(ws, col, row, cellData.S)
cellData.T, cellData.V = setCellDefault(value)
cellData.F, cellData.IS = nil, nil
cellData.removeFormula(ws)
cellData.IS = nil
return err
}

View File

@ -173,7 +173,7 @@ func TestGetPicture(t *testing.T) {
}
func TestAddDrawingPicture(t *testing.T) {
// testing addDrawingPicture with illegal cell coordinates.
// Test addDrawingPicture with illegal cell coordinates.
f := NewFile()
assert.EqualError(t, f.addDrawingPicture("sheet1", "", "A", "", 0, 0, 0, 0, nil), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
}

View File

@ -322,7 +322,7 @@ func TestInsertRow(t *testing.T) {
assert.NoError(t, f.SaveAs(filepath.Join("test", "TestInsertRow.xlsx")))
}
// Testing internal structure state after insert operations. It is important
// Test internal structure state after insert operations. It is important
// for insert workflow to be constant to avoid side effect with functions
// related to internal structure.
func TestInsertRowInEmptyFile(t *testing.T) {

View File

@ -478,12 +478,11 @@ func (f *File) SetSheetBackground(sheet, picture string) error {
}
// DeleteSheet provides a function to delete worksheet in a workbook by given
// worksheet name, the sheet names are not case-sensitive. The sheet names are
// not case-sensitive. Use this method with caution, which will affect
// changes in references such as formulas, charts, and so on. If there is any
// referenced value of the deleted worksheet, it will cause a file error when
// you open it. This function will be invalid when only the one worksheet is
// left.
// worksheet name, the sheet names are not case-sensitive. Use this method
// with caution, which will affect changes in references such as formulas,
// charts, and so on. If there is any referenced value of the deleted
// worksheet, it will cause a file error when you open it. This function will
// be invalid when only one worksheet is left
func (f *File) DeleteSheet(name string) {
if f.SheetCount == 1 || f.GetSheetIndex(name) == -1 {
return

View File

@ -129,6 +129,35 @@ func (f *File) addSheetTable(sheet string, rID int) error {
return err
}
// setTableHeader provides a function to set cells value in header row for the
// table.
func (f *File) setTableHeader(sheet string, x1, y1, x2 int) ([]*xlsxTableColumn, error) {
var (
tableColumns []*xlsxTableColumn
idx int
)
for i := x1; i <= x2; i++ {
idx++
cell, err := CoordinatesToCellName(i, y1)
if err != nil {
return tableColumns, err
}
name, _ := f.GetCellValue(sheet, cell)
if _, err := strconv.Atoi(name); err == nil {
_ = f.SetCellStr(sheet, cell, name)
}
if name == "" {
name = "Column" + strconv.Itoa(idx)
_ = f.SetCellStr(sheet, cell, name)
}
tableColumns = append(tableColumns, &xlsxTableColumn{
ID: idx,
Name: name,
})
}
return tableColumns, nil
}
// addTable provides a function to add table by given worksheet name,
// coordinate area and format set.
func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet *formatTable) error {
@ -142,29 +171,7 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet
if err != nil {
return err
}
var tableColumn []*xlsxTableColumn
idx := 0
for i := x1; i <= x2; i++ {
idx++
cell, err := CoordinatesToCellName(i, y1)
if err != nil {
return err
}
name, _ := f.GetCellValue(sheet, cell)
if _, err := strconv.Atoi(name); err == nil {
_ = f.SetCellStr(sheet, cell, name)
}
if name == "" {
name = "Column" + strconv.Itoa(idx)
_ = f.SetCellStr(sheet, cell, name)
}
tableColumn = append(tableColumn, &xlsxTableColumn{
ID: idx,
Name: name,
})
}
tableColumns, _ := f.setTableHeader(sheet, x1, y1, x2)
name := formatSet.TableName
if name == "" {
name = "Table" + strconv.Itoa(i)
@ -179,8 +186,8 @@ func (f *File) addTable(sheet, tableXML string, x1, y1, x2, y2, i int, formatSet
Ref: ref,
},
TableColumns: &xlsxTableColumns{
Count: idx,
TableColumn: tableColumn,
Count: len(tableColumns),
TableColumn: tableColumns,
},
TableStyleInfo: &xlsxTableStyleInfo{
Name: formatSet.TableStyle,

View File

@ -45,6 +45,12 @@ func TestAddTable(t *testing.T) {
assert.EqualError(t, f.addTable("sheet1", "", 1, 1, 0, 0, 0, nil), "invalid cell coordinates [0, 0]")
}
func TestSetTableHeader(t *testing.T) {
f := NewFile()
_, err := f.setTableHeader("Sheet1", 1, 0, 1)
assert.EqualError(t, err, "invalid cell coordinates [1, 0]")
}
func TestAutoFilter(t *testing.T) {
outFile := filepath.Join("test", "TestAutoFilter%d.xlsx")
@ -72,7 +78,7 @@ func TestAutoFilter(t *testing.T) {
})
}
// testing AutoFilter with illegal cell coordinates.
// Test AutoFilter with illegal cell coordinates.
assert.EqualError(t, f.AutoFilter("Sheet1", "A", "B1", ""), newCellNameToCoordinatesError("A", newInvalidCellNameError("A")).Error())
assert.EqualError(t, f.AutoFilter("Sheet1", "A1", "B", ""), newCellNameToCoordinatesError("B", newInvalidCellNameError("B")).Error())
}