mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
feat(sheets): guard +csv-put --csv against a path passed without @ (#1337)
+csv-put --csv data.csv (a forgotten @) was silently written as one-cell content, because any string parses as valid CSV — unlike malformed JSON it never errored, so the filename landed in the sheet instead of the file's contents. +csv-put's Validate now rejects a --csv value when it names a real file in the cwd subtree (guardCSVValueIsNotFilePath; fileIO.Stat, fail-open), hinting to use --csv @file or stdin (--csv -). Scoped to --csv only — no framework or other-flag change. Checking real existence (not name shape) lets inline content that merely ends in a filename pass through. Adds TestGuardCSVValueIsNotFilePath.
This commit is contained in:
committed by
GitHub
parent
4f4c0b59c9
commit
eed711bb11
58
shortcuts/sheets/csv_put_guard_test.go
Normal file
58
shortcuts/sheets/csv_put_guard_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package sheets
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
_ "github.com/larksuite/cli/internal/vfs/localfileio"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newCSVGuardRuntime(csvVal string) *common.RuntimeContext {
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
cmd.Flags().String("csv", "", "")
|
||||
cmd.ParseFlags(nil)
|
||||
cmd.Flags().Set("csv", csvVal)
|
||||
return &common.RuntimeContext{Cmd: cmd}
|
||||
}
|
||||
|
||||
// TestGuardCSVValueIsNotFilePath verifies the guard flags a bare --csv value
|
||||
// only when it names a real file (a forgotten @), while leaving genuine inline
|
||||
// content alone — including the case the old name-shape heuristic got wrong:
|
||||
// prose that merely ends in or mentions a filename.
|
||||
func TestGuardCSVValueIsNotFilePath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cmdutil.TestChdir(t, dir)
|
||||
if err := os.WriteFile("data.csv", []byte("a,b\n1,2\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Bare value naming an existing file → guarded with a fix-it hint.
|
||||
err := guardCSVValueIsNotFilePath(newCSVGuardRuntime("data.csv"))
|
||||
if err == nil {
|
||||
t.Fatal("expected guard error when --csv names an existing file")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "existing file") || !strings.Contains(err.Error(), "@data.csv") {
|
||||
t.Errorf("error should flag the file and suggest @data.csv, got: %v", err)
|
||||
}
|
||||
|
||||
// Content that is not a real file must pass through unchanged.
|
||||
for _, v := range []string{
|
||||
"改完记得更新config.json", // prose ending in a filename — not a real file
|
||||
"remember to update data.csv", // mentions the real file but isn't its name
|
||||
"a,b\n1,2", // multi-cell CSV
|
||||
"hello world",
|
||||
"nope.csv", // path-shaped but no such file
|
||||
"",
|
||||
} {
|
||||
if err := guardCSVValueIsNotFilePath(newCSVGuardRuntime(v)); err != nil {
|
||||
t.Errorf("content %q must pass through, got: %v", v, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,7 +219,12 @@ var CsvPut = common.Shortcut{
|
||||
}
|
||||
cmd.MarkFlagsOneRequired("start-cell", "range")
|
||||
},
|
||||
Validate: validateViaInput(csvPutInput),
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
if err := guardCSVValueIsNotFilePath(runtime); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateViaInput(csvPutInput)(ctx, runtime)
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
token, _ := resolveSpreadsheetToken(runtime)
|
||||
sheetID, sheetName, _ := resolveSheetSelector(runtime)
|
||||
@@ -295,6 +300,36 @@ func csvPutWriteRangeFromInput(input map[string]interface{}) (string, bool) {
|
||||
return fmt.Sprintf("%s:%s%d", anchor, endCol, endRow), true
|
||||
}
|
||||
|
||||
// guardCSVValueIsNotFilePath catches the common slip of passing a CSV file path
|
||||
// to --csv without the "@" that reads it (e.g. `--csv data.csv` instead of
|
||||
// `--csv @data.csv`). Because any string is a valid one-cell CSV, the mistake
|
||||
// would otherwise be written silently as the literal text "data.csv". It runs
|
||||
// in +csv-put's Validate, after resolveInputFlags — so an @file / stdin value is
|
||||
// already its contents (a real CSV blob, never a path) and only a bare value
|
||||
// reaches here unchanged. It flags the value only when it actually names an
|
||||
// existing file in the cwd subtree; checking real existence (not name shape)
|
||||
// means inline content that merely ends in a filename ("see config.json") is
|
||||
// never misjudged. Fails open: any Stat error or a directory leaves the value
|
||||
// untouched. Scoped to --csv only — no other flag is affected.
|
||||
func guardCSVValueIsNotFilePath(runtime *common.RuntimeContext) error {
|
||||
raw := strings.TrimSpace(runtime.Str("csv"))
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
fio := runtime.FileIO()
|
||||
if fio == nil {
|
||||
return nil
|
||||
}
|
||||
info, err := fio.Stat(raw)
|
||||
if err != nil || info == nil || info.IsDir() {
|
||||
return nil //nolint:nilerr // fail-open: a missing/unreadable path is treated as inline content, not a forgotten @
|
||||
}
|
||||
return common.FlagErrorf(
|
||||
"--csv value %q is an existing file, not inline CSV; to read it use --csv @%s, or pass the literal text via stdin (--csv -)",
|
||||
raw, raw,
|
||||
)
|
||||
}
|
||||
|
||||
func csvPutInput(runtime flagView, token, sheetID, sheetName string) (map[string]interface{}, error) {
|
||||
if err := requireSheetSelector(sheetID, sheetName); err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user