fix(sheet): reject row/column ranges with trailing characters up front
parseRowColRange gates --styles row_sizes/col_sizes before the document is created. The row branch parsed with fmt.Sscanf(a, "%d", &r1), which consumes only the leading digits and does not require the whole token, so "1x:3" was silently accepted as row 1 and "2foo" as row 2. Such input passed pre-flight, then update_dimension was sent to the wrong row after the document and data had already been created — an unrollbackable wrong edit, the opposite of the gate's purpose. The row branch now parses with strconv.Atoi, which requires the entire token to be a valid integer. The column branch had the same class of hole via a different path: parseA1Cell appends "1" to the column token, so "A5" became "A51" and was accepted as column A. A new isAllLetters pre-check requires the column token to be non-empty and letters-only before parsing; genuine multi-letter columns like "AX" still pass. With that guarantee the subsequent parseA1Cell can no longer fail, so its now-dead error branch is removed. Adds trailing-character rejection cases to TestParseRowColRange: "1x:3", "2foo", "1 2:3" (rows), "A5:C", "A1", ":C" (columns), plus "AX:C" to confirm multi-letter columns remain valid. Changed-code coverage stays at 100%.
This commit is contained in:
1 parent
ecaa375be8
commit
eb73b944a1
2 files changed
+40
-8
No files matched your search
@@ -380,6 +380,21 @@ func cellStyleItemToSpec(item map[string]any) (*styleSpec, string, error) {
|
||||
return spec, rangeAddr, nil
|
||||
}
|
||||
|
||||
// isAllLetters 判断字符串是否非空且只含英文字母(大小写皆可),用于列范围
|
||||
// 预检:拒绝 "A5" / "Ax" 这类带数字或尾随字符、会被 parseA1Cell 误判的列名。
|
||||
func isAllLetters(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// parseRowColRange 解析行/列范围:"1:3"→(start "1", len 3);"A:C"→(start "A", len 3);"1"/"A"→len 1。
|
||||
func parseRowColRange(addr string, isRow bool) (start string, length int, err error) {
|
||||
addr = strings.TrimSpace(addr)
|
||||
@@ -396,11 +411,13 @@ func parseRowColRange(addr string, isRow bool) (start string, length int, err er
|
||||
b = strings.TrimSpace(parts[1])
|
||||
}
|
||||
if isRow {
|
||||
var r1, r2 int
|
||||
if _, e := fmt.Sscanf(a, "%d", &r1); e != nil || r1 < 1 {
|
||||
return "", 0, fmt.Errorf("行范围非法: %s(应形如 \"1:3\")", addr)
|
||||
}
|
||||
if _, e := fmt.Sscanf(b, "%d", &r2); e != nil || r2 < 1 {
|
||||
// strconv.Atoi 要求整个字符串都是合法整数,拒绝 "1x" / "2foo" 这类带尾随
|
||||
// 字符的输入。fmt.Sscanf("%d") 只消费前缀数字,会把 "1x" 静默当成第 1 行 ——
|
||||
// 这是建文档前的校验关口,放过后 update_dimension 会改到错误的行,且此时
|
||||
// 文档与数据已创建、无法原子回滚。
|
||||
r1, e1 := strconv.Atoi(a)
|
||||
r2, e2 := strconv.Atoi(b)
|
||||
if e1 != nil || e2 != nil || r1 < 1 || r2 < 1 {
|
||||
return "", 0, fmt.Errorf("行范围非法: %s(应形如 \"1:3\")", addr)
|
||||
}
|
||||
if r2 < r1 {
|
||||
@@ -408,11 +425,15 @@ func parseRowColRange(addr string, isRow bool) (start string, length int, err er
|
||||
}
|
||||
return fmt.Sprintf("%d", r1), r2 - r1 + 1, nil
|
||||
}
|
||||
c1, _, e1 := parseA1Cell(strings.ToUpper(a) + "1")
|
||||
c2, _, e2 := parseA1Cell(strings.ToUpper(b) + "1")
|
||||
if e1 != nil || e2 != nil {
|
||||
// 列同理必须是纯字母:parseA1Cell 会给列名补 "1" 再解析,"A5" 会变成 "A51"
|
||||
// 被当成 A 列静默放过。先要求 a/b 只含字母,杜绝带数字/尾随字符的输入通过预检;
|
||||
// 纯多字母列名(如 "AX")是合法的,正常通过。
|
||||
if !isAllLetters(a) || !isAllLetters(b) {
|
||||
return "", 0, fmt.Errorf("列范围非法: %s(应形如 \"A:C\")", addr)
|
||||
}
|
||||
// isAllLetters 已保证 a/b 为非空纯字母,补 "1" 后 parseA1Cell 必然成功,无需再判错。
|
||||
c1, _, _ := parseA1Cell(strings.ToUpper(a) + "1")
|
||||
c2, _, _ := parseA1Cell(strings.ToUpper(b) + "1")
|
||||
// 反序范围(如 "C:A")必须同时交换起始列,否则会静默改错目标:
|
||||
// 只交换用于算长度的索引会得到 startIndex="C"/length=3,改到 C/D/E 而非 A/B/C。
|
||||
// 行分支同样在交换后返回较小的 r1。
|
||||
|
||||
@@ -812,6 +812,17 @@ func TestParseRowColRange(t *testing.T) {
|
||||
{"0:2", true, "", 0, true}, // 行号必须 >= 1
|
||||
{"1:0", true, "", 0, true},
|
||||
{"1:2", false, "", 0, true}, // 列范围收到数字
|
||||
// 带尾随字符的行范围必须整体拒绝:Sscanf("%d") 只消费前缀数字,会把
|
||||
// "1x"/"2foo" 静默当成第 1/2 行,放过后 update_dimension 改到错误行。
|
||||
{"1x:3", true, "", 0, true},
|
||||
{"2foo", true, "", 0, true},
|
||||
{"1 2:3", true, "", 0, true}, // 内部空格也非法
|
||||
// 列范围拒绝带数字/尾随字符:否则 "A5" 会被 parseA1Cell(补 "1" 后)
|
||||
// 当成 A 列静默放过。纯多字母列名(如 "AX")是合法的,不在此列。
|
||||
{"A5:C", false, "", 0, true},
|
||||
{"A1", false, "", 0, true},
|
||||
{":C", false, "", 0, true}, // 空列 token 也要拒绝
|
||||
{"AX:C", false, "C", 48, false}, // 多字母列合法:C..AX 共 48 列
|
||||
}
|
||||
for _, tc := range cases {
|
||||
start, length, err := parseRowColRange(tc.addr, tc.isRow)
|
||||
|
||||
Reference in new issue
Block a user