Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"sync"
"time"
"unicode"
"unicode/utf16"
"unicode/utf8"
"unsafe"

Expand Down Expand Up @@ -14366,7 +14367,7 @@ func (fn *formulaFuncs) TEXTJOIN(argsList *list.List) formulaArg {
return ok
}
result := strings.Join(args, delimiter.Value())
if len(result) > TotalCellChars {
if len(utf16.Encode([]rune(result))) > TotalCellChars {
return newErrorFormulaArg(formulaErrorVALUE, fmt.Sprintf("TEXTJOIN function exceeds %d characters", TotalCellChars))
}
return newStringFormulaArg(result)
Expand Down
1 change: 1 addition & 0 deletions calc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3969,6 +3969,7 @@ func TestCalcCellValue(t *testing.T) {
"TEXTJOIN(\"\",TRUE,NA())": {"#N/A", "#N/A"},
"TEXTJOIN(\"\",TRUE," + strings.Repeat("0,", 250) + ",0)": {"#VALUE!", "TEXTJOIN accepts at most 252 arguments"},
"TEXTJOIN(\",\",FALSE,REPT(\"*\",32768))": {"#VALUE!", "TEXTJOIN function exceeds 32767 characters"},
"TEXTJOIN(\"\",FALSE,REPT(\"\U0001F600\",16384))": {"#VALUE!", "TEXTJOIN function exceeds 32767 characters"},
// TRIM
"TRIM()": {"#VALUE!", "TRIM requires 1 argument"},
"TRIM(1,2)": {"#VALUE!", "TRIM requires 1 argument"},
Expand Down
12 changes: 6 additions & 6 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"
"unicode/utf16"

"github.com/xuri/efp"
)
Expand Down Expand Up @@ -447,8 +447,8 @@ func (f *File) SetCellStr(sheet, cell, value string) error {

// setCellString provides a function to set string type to shared string table.
func (f *File) setCellString(value string) (t, v string, err error) {
if utf8.RuneCountInString(value) > TotalCellChars {
value = string([]rune(value)[:TotalCellChars])
if len(utf16.Encode([]rune(value))) > TotalCellChars {
value = truncateUTF16Units(value, TotalCellChars)
}
t = "s"
var si int
Expand Down Expand Up @@ -510,8 +510,8 @@ func (f *File) setSharedString(val string) (int, error) {

// trimCellValue provides a function to set string type to cell.
func trimCellValue(value string, escape bool) (v string, ns xml.Attr) {
if utf8.RuneCountInString(value) > TotalCellChars {
value = string([]rune(value)[:TotalCellChars])
if len(utf16.Encode([]rune(value))) > TotalCellChars {
value = truncateUTF16Units(value, TotalCellChars)
}
if value != "" {
prefix, suffix := value[0], value[len(value)-1]
Expand Down Expand Up @@ -1211,7 +1211,7 @@ func setRichText(runs []RichTextRun) ([]xlsxR, error) {
totalCellChars int
)
for _, textRun := range runs {
totalCellChars += utf8.RuneCountInString(textRun.Text)
totalCellChars += len(utf16.Encode([]rune(textRun.Text)))
if totalCellChars > TotalCellChars {
return textRuns, ErrCellCharsLength
}
Expand Down
13 changes: 13 additions & 0 deletions lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"regexp"
"strconv"
"strings"
"unicode/utf16"
)

// ReadZipReader extract spreadsheet with given options.
Expand Down Expand Up @@ -940,6 +941,18 @@ func setPtrFieldsVal(fields []string, immutable, mutable reflect.Value) {
}
}

// truncateUTF16Units truncates a string to a maximum number of UTF-16 code
// units.
func truncateUTF16Units(s string, length int) string {
var cnt int
for i, r := range s {
if cnt += utf16.RuneLen(r); cnt > length {
return s[:i]
}
}
return s
}

// Stack defined an abstract data type that serves as a collection of elements.
type Stack struct {
list *list.List
Expand Down
23 changes: 23 additions & 0 deletions lib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"strings"
"sync"
"testing"
"unicode/utf16"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -353,6 +354,28 @@ func TestBstrMarshal(t *testing.T) {
}
}

func TestTruncateUTF16Units(t *testing.T) {
assertTrunc := func(s string, max int, expected string) {
assert.Equal(t, expected, truncateUTF16Units(s, max), "src=%q max=%d", s, max)
assert.LessOrEqual(t, len(utf16.Encode([]rune(truncateUTF16Units(s, max)))), max)
}
// No truncation
assertTrunc("ABC", 3, "ABC")
assertTrunc("A\U0001F600B", 4, "A\U0001F600B")
// Truncate cutting before BMP rune
assertTrunc("ABCDE", 3, "ABC")
// Truncate with surrogate pair boundary: keep pair intact
assertTrunc("A\U0001F600B", 3, "A\U0001F600") // 1 + 2 units
assertTrunc("A\U0001F600B", 2, "A") // pair would overflow
assertTrunc("\U0001F600B", 1, "") // first rune (2 units) exceeds limit
assertTrunc("\U0001F600B", 2, "\U0001F600") // exact fit
assertTrunc("\U0001F600B", 3, "\U0001F600B") // allow extra
// Multiple surrogate pairs
assertTrunc("\U0001F600\U0001F600B", 2, "\U0001F600") // corrected expectation per logic
assertTrunc("\U0001F600\U0001F600B", 3, "\U0001F600") // 2 units kept, next pair would exceed
assertTrunc("\U0001F600\U0001F600B", 4, "\U0001F600\U0001F600") // both pairs (4 units)
}

func TestReadBytes(t *testing.T) {
f := &File{tempFiles: sync.Map{}}
sheet := "xl/worksheets/sheet1.xml"
Expand Down
3 changes: 1 addition & 2 deletions sheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"strconv"
"strings"
"unicode/utf16"
"unicode/utf8"

"github.com/tiendc/go-deepcopy"
)
Expand Down Expand Up @@ -1485,7 +1484,7 @@ func checkSheetName(name string) error {
if name == "" {
return ErrSheetNameBlank
}
if utf8.RuneCountInString(name) > MaxSheetNameLength {
if len(utf16.Encode([]rune(name))) > MaxSheetNameLength {
return ErrSheetNameLength
}
if strings.HasPrefix(name, "'") || strings.HasSuffix(name, "'") {
Expand Down
38 changes: 22 additions & 16 deletions sheet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -762,22 +762,28 @@ func TestSetSheetBackgroundFromBytes(t *testing.T) {
}

func TestCheckSheetName(t *testing.T) {
// Test valid sheet name
assert.NoError(t, checkSheetName("Sheet1"))
assert.NoError(t, checkSheetName("She'et1"))
// Test invalid sheet name, empty name
assert.EqualError(t, checkSheetName(""), ErrSheetNameBlank.Error())
// Test invalid sheet name, include :\/?*[]
assert.EqualError(t, checkSheetName("Sheet:"), ErrSheetNameInvalid.Error())
assert.EqualError(t, checkSheetName(`Sheet\`), ErrSheetNameInvalid.Error())
assert.EqualError(t, checkSheetName("Sheet/"), ErrSheetNameInvalid.Error())
assert.EqualError(t, checkSheetName("Sheet?"), ErrSheetNameInvalid.Error())
assert.EqualError(t, checkSheetName("Sheet*"), ErrSheetNameInvalid.Error())
assert.EqualError(t, checkSheetName("Sheet["), ErrSheetNameInvalid.Error())
assert.EqualError(t, checkSheetName("Sheet]"), ErrSheetNameInvalid.Error())
// Test invalid sheet name, single quotes at the front or at the end
assert.EqualError(t, checkSheetName("'Sheet"), ErrSheetNameSingleQuote.Error())
assert.EqualError(t, checkSheetName("Sheet'"), ErrSheetNameSingleQuote.Error())
for expected, name := range map[error]string{
// Test valid sheet name
nil: "Sheet1",
nil: "She'et1",
// Test invalid sheet name, empty name
ErrSheetNameBlank: "",
// Test invalid sheet name, include :\/?*[]
ErrSheetNameInvalid: "Sheet:",
ErrSheetNameInvalid: `Sheet\`,
ErrSheetNameInvalid: "Sheet/",
ErrSheetNameInvalid: "Sheet?",
ErrSheetNameInvalid: "Sheet*",
ErrSheetNameInvalid: "Sheet[",
ErrSheetNameInvalid: "Sheet]",
// Test invalid sheet name, single quotes at the front or at the end
ErrSheetNameSingleQuote: "'Sheet",
ErrSheetNameSingleQuote: "Sheet'",
// Test invalid sheet name, exceed max length
ErrSheetNameLength: "Sheet" + strings.Repeat("\U0001F600", 14),
} {
assert.Equal(t, expected, checkSheetName(name))
}
}

func TestSheetDimension(t *testing.T) {
Expand Down
Loading