refactor: introduce FileIO extension to abstract file transfer operations#297
refactor: introduce FileIO extension to abstract file transfer operations#297
Conversation
Introduce extension/fileio package with Provider + FileIO interface (Open/Stat/Save) following the same pattern as extension/credential. Default LocalFileIO implementation in internal/cmdutil handles path validation, directory creation, and atomic writes. This enables internal forks to inject custom FileIO implementations (e.g. streaming) without modifying shortcut code. Change-Id: I20dfbc1a597cb638690ead0c398d003f7eb5cae6 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughAdds a pluggable file I/O abstraction ( Changes
Sequence Diagram(s)sequenceDiagram
participant Cmd as Shortcut/Command
participant RT as Runtime/Factory
participant Prov as fileio.Provider
participant FIO as fileio.FileIO
participant Client as internal client (HandleResponse)
participant Local as localfileio (implementation)
Cmd->>RT: Runtime.FileIO()
RT->>Prov: ResolveFileIO(ctx)
Prov-->>RT: FIO (may be Local)
RT-->>Cmd: return FIO
Cmd->>Client: call HandleResponse(..., FileIO: FIO)
Client->>FIO: Save(path, SaveOptions, body)
FIO->>Local: (if local) validate+atomic-write via local helpers
Local-->>FIO: SaveResult(size)
FIO-->>Client: saved path/size
Client-->>Cmd: output metadata (saved_path, size_bytes)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Replace direct os.MkdirAll + validate.AtomicWrite with FileIO.Save in all download shortcuts: drive +download, docs +media-download, drive +export, im +messages-resources-download, sheets +export, vc +notes, and internal/client/response.go SaveResponse. LocalFileIO.Save now uses AtomicWriteFromReader for streaming writes instead of io.ReadAll, avoiding full in-memory buffering. Path validation and overwrite checks use FileIO.Stat instead of direct validate.SafeOutputPath + os.Stat calls. Change-Id: I516d2c9911f6d7bc464b1abfa7d6b6050270debf Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace direct os.Stat + os.Open + os.ReadFile with FileIO.Stat + FileIO.Open across all upload and file-read shortcuts: drive, doc, im, base, mail (draft/patch, emlbuilder, helpers, draft_edit). Add PlainFileIO in cmdutil/testing.go for tests that need absolute paths (e.g. t.TempDir()) without path validation. Production code continues to use LocalFileIO with SafeInputPath enforcement. Change-Id: I7afd62a39da253896ef089dfb3954f293039b967 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clean up leftover conflict markers from Phase 3 merge and remove unused vfs/bytes imports now that FileIO abstraction is in place. Change-Id: Id4a9435a4db5f24eefda2e2112454d99d685d6f6
Change-Id: I30647cd56089668bcab015e14ee226a4f73af1ee
Change-Id: I69a2c63497a755e2137dd5a6808c554ce2f4d9fe
…os/vfs bypasses - Move LocalFileIO from internal/cmdutil to internal/vfs/localfileio - Move SafeInputPath/SafeOutputPath/AtomicWrite from validate to localfileio, keep thin wrappers in validate for backward compatibility - Fix shortcuts bypassing FileIO: drive_upload os.Open, drive_import vfs.Stat, runner vfs.ReadFile, minutes_download manual SafeOutputPath+MkdirAll+AtomicWrite - Remove dead code: EnsureWritableFile, ValidateSafeOutputDir - RuntimeContext.FileIO() falls back to global registered provider when Factory or FileIOProvider is nil Change-Id: I2e00711c648499fd679321ffab7da353c496d246
…tion in shortcuts - im: add validateMediaFlagPath helper using FileIO.Stat, replace 20 SafeLocalFlagPath calls - im/helpers.go: remove redundant SafeInputPath (FileIO.Open does it internally) - sheets/sheet_export.go: replace SafeOutputPath with FileIO.Stat for early path validation Change-Id: Iebad72763fc30632f2b7169a4379c861df68c140
…leio - Remove validate.SafeInputPath and validate.SafeLocalFlagPath (no external callers) - Downgrade to unexported safeInputPath/safeLocalFlagPath in localfileio - Replace testLocalFileIO in response_test.go with localfileio.LocalFileIO Change-Id: I9fca2536abf028c2f81497a5551ac8d0558a5eb4
…dateMediaFlagPath test - Make AtomicWriteFromReader private (only used inside localfileio.Save) - Remove dead safeLocalFlagPath function and its tests - Add unit test for validateMediaFlagPath covering skip/accept/reject cases Change-Id: I3aa6959279b51c7d8d3ff87402153e5c52f61522
…path checks Replace 4 instances of the repeated FileIO().Stat + os.IsNotExist pattern with a single runtime.ValidatePath(path) call. drive_download retains inline Stat for its combined validate+overwrite check. Change-Id: I69e74c7e82b14f92dfb53d851c911fde301b4714
- depguard: block shortcuts from importing internal/vfs and localfileio - forbidigo: add missing os functions (Chtimes, CopyFS, MkdirTemp) and filepath functions that access filesystem (EvalSymlinks, Walk, Glob, Abs) - Consolidate forbidigo patterns using regex alternation to reduce config size Change-Id: I39bd750c5e74fd753751dd4220ec5eb124a2a0d4
Change-Id: I6206a8622bdc65caebf29a57d9f0e4ec487685eb
🚀 PR Preview Install Guide🧰 CLI updatenpm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@1c30e7408a338b348abb5a7eb86f7e58113fedd8🧩 Skill updatenpx skills add larksuite/cli#feat/fileio-extension-refactor-plugin -y -g |
Greptile SummaryThis PR introduces a clean Confidence Score: 4/5Safe to merge with minor UX regressions in error messages; no functional breakage or data-loss risk. All P0/P1 concerns from prior threads appear addressed (saved_path now uses ResolveSavePath, parseStringList nil-fio trap eliminated). Remaining findings are P2: error messages on invalid output paths say '--file' instead of '--output', and invalid paths in overwrite checks are silently forwarded to Save with a less informative error. These are UX regressions, not correctness bugs, which keeps the score at 4 rather than 5. shortcuts/drive/drive_download.go, shortcuts/doc/doc_media_download.go, shortcuts/sheets/sheet_export.go, shortcuts/minutes/minutes_download.go, shortcuts/drive/drive_export_common.go — all use FileIO.Stat for output path validation which gives '--file' error messages. Important Files Changed
Sequence DiagramsequenceDiagram
participant CLI as CLI Command
participant RC as RuntimeContext
participant Factory as Factory
participant Registry as fileio.Registry
participant FIO as LocalFileIO
participant FS as Filesystem
Note over CLI,FS: Initialization
Registry->>Registry: init() registers LocalFileIO Provider
Factory->>Registry: GetProvider()
Registry-->>Factory: LocalFileIO.Provider
Note over CLI,FS: Download / Upload
CLI->>RC: runtime.FileIO()
RC->>Factory: ResolveFileIO(ctx)
Factory->>FIO: Provider.ResolveFileIO(ctx)
FIO-->>RC: &LocalFileIO{}
RC-->>CLI: FileIO
CLI->>RC: ValidatePath(outputPath)
RC->>FIO: Stat(outputPath) [safeInputPath]
FIO-->>RC: (info, err)
RC-->>CLI: nil or error
CLI->>RC: FileIO().Save(path, opts, body)
RC->>FIO: Save(path, opts, body)
FIO->>FIO: SafeOutputPath(path)
FIO->>FS: MkdirAll + atomicWrite
FIO-->>RC: SaveResult{size}
RC-->>CLI: SaveResult
CLI->>RC: ResolveSavePath(path)
RC->>FIO: ResolvePath(path)
FIO->>FIO: SafeOutputPath(path)
FIO-->>CLI: absolute path
Reviews (3): Last reviewed commit: "fix: gofmt runner_jq_test.go" | Re-trigger Greptile |
| shortcuts-no-vfs: | ||
| files: | ||
| - "**/shortcuts/**" | ||
| deny: | ||
| - pkg: "github.com/larksuite/cli/internal/vfs" | ||
| desc: >- | ||
| shortcuts must not import internal/vfs directly. | ||
| Use runtime.FileIO() for file operations or runtime.ValidatePath() for path validation. | ||
| - pkg: "github.com/larksuite/cli/internal/vfs/localfileio" | ||
| desc: >- | ||
| shortcuts must not import internal/vfs/localfileio directly. | ||
| Use runtime.FileIO() for file operations or runtime.ValidatePath() for path validation. |
There was a problem hiding this comment.
New depguard rule breaks existing shortcuts files
The rule denies internal/vfs imports from **/shortcuts/**, but two non-test production files that are not in this PR still import it:
shortcuts/event/pipeline.go—vfs.MkdirAll(line 20)shortcuts/mail/mail_watch.go—vfs.UserHomeDirandvfs.MkdirAll(line 28)
The current FileIO interface only exposes Open, Stat, and Save, so these callers cannot be migrated without first extending the interface. Running golangci-lint on the full codebase will produce new depguard errors on these files.
ee0bc07 to
a4e0fcf
Compare
Change-Id: I9a7c4dd296d9c742144ec9e8e11465b5fbe6debf
Change-Id: Ic04827e1f1be20c036c5b731825f894ce57052d0
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
shortcuts/sheets/sheet_export.go (1)
114-141:⚠️ Potential issue | 🟠 MajorReturn after the no-download branch.
--output-pathis optional, but this still falls through intoSave(outputPath, ...)after emitting{file_token, ticket}. With an empty path that turns into a bogus save attempt, and the command can write a success JSON to stdout before returning an error.🐛 Proposed fix
if outputPath == "" { runtime.Out(map[string]interface{}{ "file_token": fileToken, "ticket": ticket, }, nil) + return nil } // Download resp, err := runtime.DoAPIStream(ctx, &larkcore.ApiReq{🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/sheets/sheet_export.go` around lines 114 - 141, When outputPath is empty the code writes the file_token/ticket via runtime.Out but then continues and attempts to call runtime.FileIO().Save with an empty path; change the branch handling outputPath == "" (the block that calls runtime.Out with file_token and ticket) to return immediately after emitting the JSON so execution does not fall through to the download and Save logic—ensure you return the appropriate nil/error value from the surrounding function so no further download or success JSON (saved_path/size_bytes) is produced.internal/vfs/localfileio/path_test.go (1)
223-235:⚠️ Potential issue | 🟡 MinorDon’t codify
--fileas the shared input-path label.
safeInputPathnow sits underneath genericFileIOreads, and this expectation hard-codes--fileinto any caller that forwards the raw error. The newly migrated--patch-fileand draft patchpathflows do exactly that, so users will get guidance for the wrong flag/op. Make the validator return a neutral path error or accept a caller-supplied label instead.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/vfs/localfileio/path_test.go` around lines 223 - 235, The test fails because safeInputPath currently returns an error that hard-codes the flag name "--file", which is incorrect for callers like --patch-file or other flows; change safeInputPath (and any helper it uses) to return a neutral path validation error message (no specific flag name) or refactor safeInputPath to accept a caller-provided label argument (e.g., safeInputPath(path string, label string)) and use that label in the error; update Test_safeInputPath_ErrorMessageContainsCorrectFlagName to assert for the neutral/error type or the provided label behavior instead of checking for "--file".
🧹 Nitpick comments (9)
.golangci.yml (1)
92-116: Duplicateforbidigopatterns foros.Stdin/Stdout/Stderrandos.Exit.The patterns at lines 92-98 (
os\.Std(in|out|err)andos\.Exit) duplicate the individual patterns at lines 106-116. This redundancy may cause duplicate lint warnings or confusion.Consider removing the duplicate individual patterns (lines 106-116) since the grouped regex patterns already cover them.
♻️ Remove duplicate patterns
- pattern: os\.Exit\b msg: >- Do not use os.Exit in shortcuts/. Return an error instead and let the caller (cmd layer) decide how to terminate. # ── filepath: functions that access the filesystem ── - pattern: filepath\.(EvalSymlinks|Walk|WalkDir|Glob|Abs)\b msg: >- These filepath functions access the filesystem directly. internal/: use vfs helpers or localfileio path validation. shortcuts/: use runtime.ValidatePath() or runtime.FileIO(). - # ── IO streams: use IOStreams from cmdutil instead ── - - pattern: os\.Stdin\b - msg: "use IOStreams.In instead of os.Stdin" - - pattern: os\.Stdout\b - msg: "use IOStreams.Out instead of os.Stdout" - - pattern: os\.Stderr\b - msg: "use IOStreams.ErrOut instead of os.Stderr" - # ── Process-level rules ── - - pattern: os\.Exit\b - msg: >- - Do not use os.Exit in shortcuts/. Return an error instead and let - the caller (cmd layer) decide how to terminate. analyze-types: true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.golangci.yml around lines 92 - 116, Remove the duplicated forbidigo rules: keep the grouped regex entries (pattern: os\.Std(in|out|err)\b and pattern: os\.Exit\b) and delete the redundant individual rules (pattern: os\.Stdin\b, pattern: os\.Stdout\b, pattern: os\.Stderr\b and the second pattern: os\.Exit\b) so each prohibition appears only once while preserving the original msg text for the retained patterns.shortcuts/drive/drive_upload.go (1)
177-193: Resource leak: file handle not closed on upload error beforepartFile.Close().If
runtime.DoAPIat line 188 returns an error,partFile.Close()at line 193 is still called. However, iflarkcore.NewFormdata()orfd.AddFilefails internally before DoAPI, or if there's a panic, the file might not be closed properly.Consider using
defer partFile.Close()immediately after opening, or restructuring to ensure cleanup.♻️ Suggested restructure using defer
partFile, err := runtime.FileIO().Open(filePath) if err != nil { return "", output.ErrValidation("cannot open file: %v", err) } + defer partFile.Close() fd := larkcore.NewFormdata() fd.AddField("upload_id", uploadID) fd.AddField("seq", fmt.Sprintf("%d", seq)) fd.AddField("size", fmt.Sprintf("%d", partSize)) fd.AddFile("file", io.NewSectionReader(partFile, offset, partSize)) apiResp, err := runtime.DoAPI(&larkcore.ApiReq{ HttpMethod: http.MethodPost, ApiPath: "/open-apis/drive/v1/files/upload_part", Body: fd, }, larkcore.WithFileUpload()) - partFile.Close() if err != nil {Note: Using
deferinside a loop creates a closure per iteration. If the loop has many iterations and files are large, this could delay cleanup until function return. The current explicitClose()is acceptable if you prefer immediate cleanup, but ensure all error paths are covered.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/drive/drive_upload.go` around lines 177 - 193, The opened file handle partFile (from runtime.FileIO().Open) can leak if NewFormdata, fd.AddFile, runtime.DoAPI or other intermediate operations return an error or panic before the explicit partFile.Close() call; ensure partFile is always closed by calling defer partFile.Close() immediately after a successful Open (or restructure to close on every error path) so that fd.AddFile, larkcore.NewFormdata and runtime.DoAPI error paths cannot leak the handle.shortcuts/drive/drive_import_test.go (1)
15-16: Prefer the standard test bootstrap over a blank import here.This package-level side effect wires
localfileiodifferently from the real CLI bootstrap, so these tests can still pass even if provider registration drifts elsewhere. Using the normal test factory plus an isolated config dir would keep the FileIO path closer to production.As per coding guidelines, "Use
cmdutil.TestFactory(t, config)for creating test factories in Go tests" and "Isolate config state in Go tests by usingt.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/drive/drive_import_test.go` around lines 15 - 16, Remove the package-level blank import of "github.com/larksuite/cli/internal/vfs/localfileio" and replace the implicit provider wiring by creating an explicit test factory using cmdutil.TestFactory(t, config) in the test setup; also isolate config state by calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) before initializing the factory so FileIO registration follows the normal CLI bootstrap path instead of package side-effects (refer to symbols localfileio, cmdutil.TestFactory, and the env var LARKSUITE_CLI_CONFIG_DIR).shortcuts/common/runner_jq_test.go (1)
155-167: Prefercmdutil.TestFactory(t, config)for test factories.
newTestFactorystill buildscmdutil.Factorymanually; using the standard helper will keep test setup consistent with repo conventions.As per coding guidelines:
**/*_test.go: Usecmdutil.TestFactory(t, config)for creating test factories in Go tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/common/runner_jq_test.go` around lines 155 - 167, The test helper newTestFactory manually constructs a cmdutil.Factory; replace it with the repo-standard cmdutil.TestFactory(t, config) call: remove newTestFactory and in tests call cmdutil.TestFactory(t, func() *cmdutil.Factory { return &cmdutil.Factory{ Config: func() (*core.CliConfig, error){ return &core.CliConfig{AppID:"test",AppSecret:"test",Brand:core.BrandFeishu}, nil }, LarkClient: func() (*lark.Client, error){ return lark.NewClient("test","test"), nil }, IOStreams: &cmdutil.IOStreams{Out: &bytes.Buffer{}, ErrOut: &bytes.Buffer{}}, FileIOProvider: fileio.GetProvider(), } }) so the test uses TestFactory(t, ...) instead of the manual newTestFactory construction and follows repository test conventions.extension/fileio/registry.go (1)
22-27: Consider usingsync.RWMutexfor better read concurrency.
GetProvider()is likely called frequently at runtime, whileRegister()is typically called once during init. UsingRWMutexwithRLock()for reads would allow concurrentGetProvider()calls without blocking each other.♻️ Optional: Use RWMutex for better read performance
var ( - mu sync.Mutex + mu sync.RWMutex provider Provider ) func Register(p Provider) { mu.Lock() defer mu.Unlock() provider = p } func GetProvider() Provider { - mu.Lock() - defer mu.Unlock() + mu.RLock() + defer mu.RUnlock() return provider }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@extension/fileio/registry.go` around lines 22 - 27, GetProvider currently uses mu.Lock() which serializes reads; change the mutex type from sync.Mutex to sync.RWMutex (symbol mu) and update GetProvider to use mu.RLock() / mu.RUnlock() so concurrent reads don't block each other; also update Register (the provider registration function) to use mu.Lock() / mu.Unlock() for writes and ensure the import of "sync" is preserved/updated accordingly while keeping the provider variable usage the same.shortcuts/base/base_shortcuts_test.go (2)
21-48: Use the standard test factory in this helper.This helper now wires
Factorymanually, which bypasses the repo’s test-factory defaults and still leaves no place to isolate config state. Please threadt *testing.TintonewBaseTestRuntimeand build the runtime withcmdutil.TestFactory(t, config)plus a tempLARKSUITE_CLI_CONFIG_DIR.As per coding guidelines,
**/*_test.go: Usecmdutil.TestFactory(t, config)for creating test factories in Go tests, and isolate config state viat.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/base/base_shortcuts_test.go` around lines 21 - 48, Update newBaseTestRuntime to accept t *testing.T, set isolated config dir with t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()), and build the runtime using cmdutil.TestFactory(t, config) instead of manually wiring Factory; ensure the returned RuntimeContext uses Factory from cmdutil.TestFactory and keep other flag setup the same so tests use the standard test factory and isolated config state.
77-96: Add a file-backed case for the newFileIObranch.These updates only adapt the signature by passing
nil, so this test still never exercises the new@pathloading path that motivated the extra parameter. A regression inloadJSONInput/parseObjectListwould slip through unnoticed.shortcuts/im/validate_media_test.go (1)
14-19: Usecmdutil.TestChdirfor consistency with other tests.Other tests in this PR (e.g.,
helpers_network_test.go) usecmdutil.TestChdir(t, t.TempDir())instead of manualos.Getwd/os.Chdir/deferbookkeeping. This helper handles cleanup more robustly.Proposed fix
func TestValidateMediaFlagPath(t *testing.T) { - dir := t.TempDir() - orig, _ := os.Getwd() - defer os.Chdir(orig) - os.Chdir(dir) - os.WriteFile(filepath.Join(dir, "photo.jpg"), []byte("img"), 0644) + cmdutil.TestChdir(t, t.TempDir()) + os.WriteFile("photo.jpg", []byte("img"), 0644)This requires adding the import:
import ( "os" "testing" "github.com/larksuite/cli/internal/cmdutil" "github.com/larksuite/cli/internal/vfs/localfileio" )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/im/validate_media_test.go` around lines 14 - 19, Replace the manual working-directory bookkeeping in TestValidateMediaFlagPath with the test helper cmdutil.TestChdir: call cmdutil.TestChdir(t, t.TempDir()) at the start of TestValidateMediaFlagPath and remove the orig := os.Getwd(), defer os.Chdir(orig) and os.Chdir(dir) lines; also add the import for the cmdutil package (e.g., import "github.com/larksuite/cli/internal/cmdutil") so the test compiles.internal/cmdutil/factory_default_test.go (1)
211-230: Missing config directory isolation.Other tests in this file use
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())to isolate config state. This test should follow the same pattern to prevent interference with machine-local configuration.Proposed fix
func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) { + t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) + prev := fileio.GetProvider() provider := &countingFileIOProvider{} fileio.Register(provider)As per coding guidelines: "Isolate config state in Go tests by using
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/cmdutil/factory_default_test.go` around lines 211 - 230, TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization fails to isolate config state; update the test to set the config dir to a temporary directory by calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start of the test (before calling NewDefault/InvocationContext) so machine-local config cannot affect NewDefault/ResolveFileIO behavior; ensure this change is applied inside the TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/client/response.go`:
- Around line 82-83: When handling errors returned from FileIO.Save (fio.Save)
where currently code does return output.Errorf(output.ExitInternal,
"file_error", "%s", err) and also uses fmt.Errorf("cannot write file: %s", err),
change the logic to detect typed validation errors from fio.Save (use errors.Is
/ errors.As against the validation error type returned by the FileIO layer) and,
when matched, return output.Errorf(output.ErrValidation, "file_error", "%v",
err) (preserving the original error rather than wrapping with fmt.Errorf); for
non-validation failures keep the ExitInternal mapping. Apply the same change to
the other similar return sites that currently remap all failures to ExitInternal
(the other occurrences in this file).
- Line 30: The code unconditionally calls fio.Save(...) which will panic when
ResponseOptions.FileIO is nil; update SaveResponse and the other save paths
(where fio.Save is used) to guard against nil by resolving a fallback OS-backed
FileIO (e.g., create or use a fileio.NewOSFileIO()/fileio.Default
implementation) before calling Save, or return a clear error if no FileIO is
available; locate the ResponseOptions struct and the SaveResponse (and related
save) functions and insert a small nil-check: if ro.FileIO == nil { ro.FileIO =
fileio.NewOSFileIO() } (or assign a local fio := ro.FileIO; if nil { fio =
fileio.NewOSFileIO() }) then call fio.Save(...) to avoid the panic.
In `@internal/cmdutil/testing.go`:
- Line 10: In TestChdir (and the related helper code around lines 96-104)
replace direct os package calls with the repository VFS wrappers: use the VFS
method to get the current working dir instead of os.Getwd and use the VFS
chdir-equivalent to change directories instead of os.Chdir; update any error
handling to reflect the VFS method signatures and ensure the function restores
the original VFS working directory on return (same semantics as before) so all
filesystem access goes through the vfs layer.
In `@internal/validate/path.go`:
- Around line 6-11: Update the stale docs/comments that still instruct callers
to use validate.SafeInputPath: change AGENTS.md (around the previous line 72)
and the comment in shortcuts/mail/draft/limits.go (previously referencing
"Callers must validate the file path via validate.SafeInputPath") to instead
state that path validation is performed by FileIO operations and callers should
use FileIO methods such as fio.Stat(), fio.Open(), fio.Read(), etc.; reference
the existing example in shortcuts/drive/drive_import.go (around the usage at
line 144) to show the new pattern and remove any mention of SafeInputPath or
validate.SafeInputPath.
In `@internal/vfs/localfileio/localfileio.go`:
- Around line 34-49: The Open and Stat methods in LocalFileIO (and the other
method around MkdirAll at lines ~59-67) call os.Open, os.Stat, and os.MkdirAll
directly which bypasses the repo VFS wrapper; change these to call the vfs
package equivalents (e.g., vfs.Open, vfs.Stat, vfs.MkdirAll or the project’s
vfs.FS interface methods used elsewhere) after validating the path with
safeInputPath so all filesystem access consistently goes through internal/vfs;
update imports to include the vfs package and ensure error propagation stays the
same.
In `@internal/vfs/localfileio/path.go`:
- Around line 21-23: The SafeOutputPath implementation only validates the path
string but does not prevent TOCTOU races; update the code so that subsequent I/O
operations (LocalFileIO.Open, LocalFileIO.Stat, LocalFileIO.Save and any code
paths referenced in the same file regions) are anchored to trusted directory
file descriptors using openat/mkdirat and O_NOFOLLOW semantics rather than
relying on a vetted path string. Concretely: change callers to walk and open
each path component relative to a secured dirfd (using openat with
O_DIRECTORY|O_NOFOLLOW or mkdirat when creating tails), avoid following symlinks
on the final component, and propagate the dirfd or opened file descriptor
instead of re-resolving the string path; ensure all usages referenced
(SafeOutputPath and the LocalFileIO Open/Stat/Save code paths) adopt this
dirfd-based approach so the TOCTOU window is closed.
In `@shortcuts/common/runner.go`:
- Around line 299-316: ValidatePath and resolveInputFlags currently call
ctx.FileIO() and dereference it without checking for nil; add defensive nil
checks where FileIO() is used (in the ValidatePath and resolveInputFlags
functions) and return a clear error (or fallback behavior) if ctx.FileIO() ==
nil. Specifically, in ValidatePath and in resolveInputFlags locate calls like
ctx.FileIO().Stat/Read/Open and guard them with fio := ctx.FileIO(); if fio ==
nil { return fmt.Errorf("file IO provider not available") } (or appropriate
error handling), ensuring RuntimeContext.FileIO() is safely used even if
Factory/FileIOProvider or global provider are absent; alternatively, if you
prefer contract documentation, add a comment on RuntimeContext.FileIO/Execute
guaranteeing non-nil in production and add a runtime check in Execute to panic
or return an error if Factory/FileIOProvider is unset.
In `@shortcuts/doc/doc_media_insert.go`:
- Around line 115-119: The current branch that handles errors from
runtime.FileIO().Stat(filePath) hides the original error by always returning
output.ErrValidation("file not found: %s", filePath); change it to preserve and
return the underlying error (or wrap it) so path/permission errors from
runtime.FileIO().Stat are conveyed. Locate the Stat call in doc_media_insert.go
(runtime.FileIO().Stat(filePath)), and replace the unconditional "file not
found" return with a return that includes err (e.g., wrap err into
output.ErrValidation or return err directly) while keeping the filePath context.
In `@shortcuts/drive/drive_download.go`:
- Line 10: The code currently treats any fileio.FileIO.Stat error as an
os.IsNotExist miss and labels it an "unsafe output path"; instead, validate the
path up-front and treat Stat errors generically: call validate.SafeInputPath (or
runtime.ValidatePath where used in the codebase) on the path before invoking
fileio.FileIO.Stat, remove any os.IsNotExist-based branching, and on Stat errors
return or propagate the original error (or a clear not-found error) rather than
assuming os.IsNotExist; update the logic around the fileio.FileIO.Stat call in
drive_download.go to use validate.SafeInputPath/runtime.ValidatePath and handle
Stat errors without os.IsNotExist checks.
In `@shortcuts/im/im_messages_send.go`:
- Around line 214-221: validateMediaFlagPath currently ignores missing local
files because it only returns an error when Stat fails with something other than
os.IsNotExist; update validateMediaFlagPath to reject non-empty, non-URL,
non-media-key paths that do not exist by checking fio.Stat(value) and returning
output.ErrValidation("%s: %v", flagName, err) whenever err != nil (including
os.IsNotExist), while preserving the early return for URLs/media keys/empty
values — change the conditional around fio.Stat in validateMediaFlagPath to
return a validation error on any Stat error instead of skipping when
os.IsNotExist(err).
In `@shortcuts/mail/emlbuilder/builder.go`:
- Around line 62-70: The readFile method can panic if Builder.fio is nil (when
WithFileIO was not called); update Builder.readFile to check if b.fio == nil and
return a clear error (e.g., "file IO not initialized: call WithFileIO") instead
of calling b.fio.Open, and reference the Builder.readFile and WithFileIO symbols
so the check is added near the start of that method; ensure the error message is
wrapped/returned consistently with existing fmt.Errorf usage.
---
Outside diff comments:
In `@internal/vfs/localfileio/path_test.go`:
- Around line 223-235: The test fails because safeInputPath currently returns an
error that hard-codes the flag name "--file", which is incorrect for callers
like --patch-file or other flows; change safeInputPath (and any helper it uses)
to return a neutral path validation error message (no specific flag name) or
refactor safeInputPath to accept a caller-provided label argument (e.g.,
safeInputPath(path string, label string)) and use that label in the error;
update Test_safeInputPath_ErrorMessageContainsCorrectFlagName to assert for the
neutral/error type or the provided label behavior instead of checking for
"--file".
In `@shortcuts/sheets/sheet_export.go`:
- Around line 114-141: When outputPath is empty the code writes the
file_token/ticket via runtime.Out but then continues and attempts to call
runtime.FileIO().Save with an empty path; change the branch handling outputPath
== "" (the block that calls runtime.Out with file_token and ticket) to return
immediately after emitting the JSON so execution does not fall through to the
download and Save logic—ensure you return the appropriate nil/error value from
the surrounding function so no further download or success JSON
(saved_path/size_bytes) is produced.
---
Nitpick comments:
In @.golangci.yml:
- Around line 92-116: Remove the duplicated forbidigo rules: keep the grouped
regex entries (pattern: os\.Std(in|out|err)\b and pattern: os\.Exit\b) and
delete the redundant individual rules (pattern: os\.Stdin\b, pattern:
os\.Stdout\b, pattern: os\.Stderr\b and the second pattern: os\.Exit\b) so each
prohibition appears only once while preserving the original msg text for the
retained patterns.
In `@extension/fileio/registry.go`:
- Around line 22-27: GetProvider currently uses mu.Lock() which serializes
reads; change the mutex type from sync.Mutex to sync.RWMutex (symbol mu) and
update GetProvider to use mu.RLock() / mu.RUnlock() so concurrent reads don't
block each other; also update Register (the provider registration function) to
use mu.Lock() / mu.Unlock() for writes and ensure the import of "sync" is
preserved/updated accordingly while keeping the provider variable usage the
same.
In `@internal/cmdutil/factory_default_test.go`:
- Around line 211-230:
TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization fails to isolate
config state; update the test to set the config dir to a temporary directory by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) at the start of the
test (before calling NewDefault/InvocationContext) so machine-local config
cannot affect NewDefault/ResolveFileIO behavior; ensure this change is applied
inside the TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization
function.
In `@shortcuts/base/base_shortcuts_test.go`:
- Around line 21-48: Update newBaseTestRuntime to accept t *testing.T, set
isolated config dir with t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()), and
build the runtime using cmdutil.TestFactory(t, config) instead of manually
wiring Factory; ensure the returned RuntimeContext uses Factory from
cmdutil.TestFactory and keep other flag setup the same so tests use the standard
test factory and isolated config state.
In `@shortcuts/common/runner_jq_test.go`:
- Around line 155-167: The test helper newTestFactory manually constructs a
cmdutil.Factory; replace it with the repo-standard cmdutil.TestFactory(t,
config) call: remove newTestFactory and in tests call cmdutil.TestFactory(t,
func() *cmdutil.Factory { return &cmdutil.Factory{ Config: func()
(*core.CliConfig, error){ return
&core.CliConfig{AppID:"test",AppSecret:"test",Brand:core.BrandFeishu}, nil },
LarkClient: func() (*lark.Client, error){ return lark.NewClient("test","test"),
nil }, IOStreams: &cmdutil.IOStreams{Out: &bytes.Buffer{}, ErrOut:
&bytes.Buffer{}}, FileIOProvider: fileio.GetProvider(), } }) so the test uses
TestFactory(t, ...) instead of the manual newTestFactory construction and
follows repository test conventions.
In `@shortcuts/drive/drive_import_test.go`:
- Around line 15-16: Remove the package-level blank import of
"github.com/larksuite/cli/internal/vfs/localfileio" and replace the implicit
provider wiring by creating an explicit test factory using
cmdutil.TestFactory(t, config) in the test setup; also isolate config state by
calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) before initializing
the factory so FileIO registration follows the normal CLI bootstrap path instead
of package side-effects (refer to symbols localfileio, cmdutil.TestFactory, and
the env var LARKSUITE_CLI_CONFIG_DIR).
In `@shortcuts/drive/drive_upload.go`:
- Around line 177-193: The opened file handle partFile (from
runtime.FileIO().Open) can leak if NewFormdata, fd.AddFile, runtime.DoAPI or
other intermediate operations return an error or panic before the explicit
partFile.Close() call; ensure partFile is always closed by calling defer
partFile.Close() immediately after a successful Open (or restructure to close on
every error path) so that fd.AddFile, larkcore.NewFormdata and runtime.DoAPI
error paths cannot leak the handle.
In `@shortcuts/im/validate_media_test.go`:
- Around line 14-19: Replace the manual working-directory bookkeeping in
TestValidateMediaFlagPath with the test helper cmdutil.TestChdir: call
cmdutil.TestChdir(t, t.TempDir()) at the start of TestValidateMediaFlagPath and
remove the orig := os.Getwd(), defer os.Chdir(orig) and os.Chdir(dir) lines;
also add the import for the cmdutil package (e.g., import
"github.com/larksuite/cli/internal/cmdutil") so the test compiles.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a1810c48-aa8f-4f85-96be-94ceb14109c1
📒 Files selected for processing (81)
.golangci.ymlcmd/api/api.gocmd/service/service.goextension/fileio/registry.goextension/fileio/types.gointernal/client/response.gointernal/client/response_test.gointernal/cmdutil/factory.gointernal/cmdutil/factory_default.gointernal/cmdutil/factory_default_test.gointernal/cmdutil/testing.gointernal/validate/atomicwrite.gointernal/validate/path.gointernal/vfs/localfileio/atomicwrite.gointernal/vfs/localfileio/atomicwrite_test.gointernal/vfs/localfileio/input.gointernal/vfs/localfileio/localfileio.gointernal/vfs/localfileio/path.gointernal/vfs/localfileio/path_test.goshortcuts/base/base_shortcut_helpers.goshortcuts/base/base_shortcuts_test.goshortcuts/base/dashboard_block_create.goshortcuts/base/dashboard_block_update.goshortcuts/base/dashboard_ops.goshortcuts/base/field_ops.goshortcuts/base/helpers.goshortcuts/base/helpers_test.goshortcuts/base/record_ops.goshortcuts/base/record_upload_attachment.goshortcuts/base/table_ops.goshortcuts/base/view_ops.goshortcuts/base/workflow_create.goshortcuts/base/workflow_update.goshortcuts/common/common_test.goshortcuts/common/helpers.goshortcuts/common/runner.goshortcuts/common/runner_input_test.goshortcuts/common/runner_jq_test.goshortcuts/common/validate.goshortcuts/common/validate_test.goshortcuts/doc/doc_media_download.goshortcuts/doc/doc_media_insert.goshortcuts/doc/doc_media_upload.goshortcuts/drive/drive_download.goshortcuts/drive/drive_export.goshortcuts/drive/drive_export_common.goshortcuts/drive/drive_export_test.goshortcuts/drive/drive_import.goshortcuts/drive/drive_import_common.goshortcuts/drive/drive_import_test.goshortcuts/drive/drive_upload.goshortcuts/im/coverage_additional_test.goshortcuts/im/helpers.goshortcuts/im/helpers_network_test.goshortcuts/im/helpers_test.goshortcuts/im/im_messages_reply.goshortcuts/im/im_messages_resources_download.goshortcuts/im/im_messages_send.goshortcuts/im/validate_media_test.goshortcuts/mail/draft/acceptance_test.goshortcuts/mail/draft/patch.goshortcuts/mail/draft/patch_attachment_test.goshortcuts/mail/draft/patch_body_test.goshortcuts/mail/draft/patch_header_test.goshortcuts/mail/draft/patch_recipient_test.goshortcuts/mail/draft/patch_test.goshortcuts/mail/draft/serialize_golden_test.goshortcuts/mail/draft/serialize_test.goshortcuts/mail/emlbuilder/builder.goshortcuts/mail/emlbuilder/builder_test.goshortcuts/mail/helpers.goshortcuts/mail/helpers_test.goshortcuts/mail/mail_draft_create.goshortcuts/mail/mail_draft_edit.goshortcuts/mail/mail_forward.goshortcuts/mail/mail_reply.goshortcuts/mail/mail_reply_all.goshortcuts/mail/mail_send.goshortcuts/minutes/minutes_download.goshortcuts/sheets/sheet_export.goshortcuts/vc/vc_notes.go
💤 Files with no reviewable changes (4)
- shortcuts/common/validate_test.go
- shortcuts/common/common_test.go
- shortcuts/common/helpers.go
- shortcuts/common/validate.go
| JqExpr string // if set, apply jq filter instead of Format | ||
| Out io.Writer // stdout | ||
| ErrOut io.Writer // stderr | ||
| FileIO fileio.FileIO // file transfer abstraction; nil falls back to direct os calls |
There was a problem hiding this comment.
Avoid a nil FileIO panic on save paths.
ResponseOptions still says nil falls back, but SaveResponse now unconditionally calls fio.Save(...). Any caller using the zero value will panic on --output or on the auto-save binary path instead of getting a normal error.
🩹 Minimal guard
func SaveResponse(fio fileio.FileIO, resp *larkcore.ApiResp, outputPath string) (map[string]interface{}, error) {
+ if fio == nil {
+ return nil, fmt.Errorf("file I/O not configured")
+ }
result, err := fio.Save(outputPath, fileio.SaveOptions{
ContentType: resp.Header.Get("Content-Type"),
ContentLength: int64(len(resp.RawBody)),
}, bytes.NewReader(resp.RawBody))Also applies to: 63-63, 77-81, 122-126
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/client/response.go` at line 30, The code unconditionally calls
fio.Save(...) which will panic when ResponseOptions.FileIO is nil; update
SaveResponse and the other save paths (where fio.Save is used) to guard against
nil by resolving a fallback OS-backed FileIO (e.g., create or use a
fileio.NewOSFileIO()/fileio.Default implementation) before calling Save, or
return a clear error if no FileIO is available; locate the ResponseOptions
struct and the SaveResponse (and related save) functions and insert a small
nil-check: if ro.FileIO == nil { ro.FileIO = fileio.NewOSFileIO() } (or assign a
local fio := ro.FileIO; if nil { fio = fileio.NewOSFileIO() }) then call
fio.Save(...) to avoid the panic.
| if err != nil { | ||
| return output.Errorf(output.ExitInternal, "file_error", "%s", err) |
There was a problem hiding this comment.
Preserve validation failures from FileIO.Save.
fio.Save can now return user-input errors such as an invalid --output path, but both save paths remap every failure to file_error/ExitInternal. fmt.Errorf("cannot write file: %s", err) also strips the cause chain, so callers cannot recover the right exit code later. Please keep typed validation errors intact and map them to output.ErrValidation here.
Also applies to: 92-93, 123-129
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/client/response.go` around lines 82 - 83, When handling errors
returned from FileIO.Save (fio.Save) where currently code does return
output.Errorf(output.ExitInternal, "file_error", "%s", err) and also uses
fmt.Errorf("cannot write file: %s", err), change the logic to detect typed
validation errors from fio.Save (use errors.Is / errors.As against the
validation error type returned by the FileIO layer) and, when matched, return
output.Errorf(output.ErrValidation, "file_error", "%v", err) (preserving the
original error rather than wrapping with fmt.Errorf); for non-validation
failures keep the ExitInternal mapping. Apply the same change to the other
similar return sites that currently remap all failures to ExitInternal (the
other occurrences in this file).
| "bytes" | ||
| "context" | ||
| "net/http" | ||
| "os" |
There was a problem hiding this comment.
Use vfs.* wrappers instead of direct os.* in TestChdir.
TestChdir currently uses os.Getwd/os.Chdir, which breaks the repo filesystem abstraction rule. Please switch this helper to the repository VFS layer for consistency.
As per coding guidelines: Use vfs.* instead of os.* for all filesystem access.
Also applies to: 96-104
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/cmdutil/testing.go` at line 10, In TestChdir (and the related helper
code around lines 96-104) replace direct os package calls with the repository
VFS wrappers: use the VFS method to get the current working dir instead of
os.Getwd and use the VFS chdir-equivalent to change directories instead of
os.Chdir; update any error handling to reflect the VFS method signatures and
ensure the function restores the original VFS working directory on return (same
semantics as before) so all filesystem access goes through the vfs layer.
| import "github.com/larksuite/cli/internal/vfs/localfileio" | ||
|
|
||
| "github.com/larksuite/cli/internal/vfs" | ||
| ) | ||
|
|
||
| // SafeOutputPath validates a download/export target path for --output flags. | ||
| // It rejects absolute paths, resolves symlinks to their real location, and | ||
| // verifies the canonical result is still under the current working directory. | ||
| // This prevents an AI Agent from being tricked into writing files outside the | ||
| // working directory (e.g. "../../.ssh/authorized_keys") or following symlinks | ||
| // to sensitive locations. | ||
| // | ||
| // The returned absolute path MUST be used for all subsequent I/O to prevent | ||
| // time-of-check-to-time-of-use (TOCTOU) race conditions. | ||
| // SafeOutputPath delegates to localfileio.SafeOutputPath. | ||
| func SafeOutputPath(path string) (string, error) { | ||
| return safePath(path, "--output") | ||
| } | ||
|
|
||
| // SafeInputPath validates an upload/read source path for --file flags. | ||
| // It applies the same rules as SafeOutputPath — rejecting absolute paths, | ||
| // resolving symlinks, and enforcing working directory containment — to prevent an AI Agent | ||
| // from being tricked into reading sensitive files like /etc/passwd. | ||
| func SafeInputPath(path string) (string, error) { | ||
| return safePath(path, "--file") | ||
| } | ||
|
|
||
| // SafeLocalFlagPath validates a flag value as a local file path. | ||
| // Empty values and http/https URLs are returned unchanged without validation, | ||
| // allowing the caller to handle non-path inputs (e.g. API keys, URLs) upstream. | ||
| // For all other values, SafeInputPath rules apply. | ||
| // The original relative path is returned unchanged (not resolved to absolute) so | ||
| // upload helpers can re-validate at the actual I/O point via SafeUploadPath. | ||
| func SafeLocalFlagPath(flagName, value string) (string, error) { | ||
| if value == "" || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") { | ||
| return value, nil | ||
| } | ||
| if _, err := SafeInputPath(value); err != nil { | ||
| return "", fmt.Errorf("%s: %v", flagName, err) | ||
| } | ||
| return value, nil | ||
| } | ||
|
|
||
| // safePath is the shared implementation for SafeOutputPath and SafeInputPath. | ||
| func safePath(raw, flagName string) (string, error) { | ||
| if err := RejectControlChars(raw, flagName); err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| path := filepath.Clean(raw) | ||
|
|
||
| if filepath.IsAbs(path) { | ||
| return "", fmt.Errorf("%s must be a relative path within the current directory, got %q (hint: cd to the target directory first, or use a relative path like ./filename)", flagName, raw) | ||
| } | ||
|
|
||
| cwd, err := vfs.Getwd() | ||
| if err != nil { | ||
| return "", fmt.Errorf("cannot determine working directory: %w", err) | ||
| } | ||
| resolved := filepath.Join(cwd, path) | ||
|
|
||
| // Resolve symlinks: for existing paths, follow to real location; | ||
| // for non-existing paths, walk up to the nearest existing ancestor, | ||
| // resolve its symlinks, and re-attach the remaining tail segments. | ||
| // This prevents TOCTOU attacks where a non-existent intermediate | ||
| // directory is replaced with a symlink between check and use. | ||
| if _, err := vfs.Lstat(resolved); err == nil { | ||
| resolved, err = filepath.EvalSymlinks(resolved) | ||
| if err != nil { | ||
| return "", fmt.Errorf("cannot resolve symlinks: %w", err) | ||
| } | ||
| } else { | ||
| resolved, err = resolveNearestAncestor(resolved) | ||
| if err != nil { | ||
| return "", fmt.Errorf("cannot resolve symlinks: %w", err) | ||
| } | ||
| } | ||
|
|
||
| canonicalCwd, _ := filepath.EvalSymlinks(cwd) | ||
| if !isUnderDir(resolved, canonicalCwd) { | ||
| return "", fmt.Errorf("%s %q resolves outside the current working directory (hint: the path must stay within the working directory after resolving .. and symlinks)", flagName, raw) | ||
| } | ||
|
|
||
| return resolved, nil | ||
| } | ||
|
|
||
| // resolveNearestAncestor walks up from path until it finds an existing | ||
| // ancestor, resolves that ancestor's symlinks, and re-joins the tail. | ||
| // This ensures even deeply nested non-existent paths are anchored to a | ||
| // real filesystem location, closing the TOCTOU symlink gap. | ||
| func resolveNearestAncestor(path string) (string, error) { | ||
| var tail []string | ||
| cur := path | ||
| for { | ||
| if _, err := vfs.Lstat(cur); err == nil { | ||
| real, err := filepath.EvalSymlinks(cur) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| parts := append([]string{real}, tail...) | ||
| return filepath.Join(parts...), nil | ||
| } | ||
| parent := filepath.Dir(cur) | ||
| if parent == cur { | ||
| // Reached filesystem root without finding an existing ancestor; | ||
| // return path as-is and let the containment check reject it. | ||
| parts := append([]string{cur}, tail...) | ||
| return filepath.Join(parts...), nil | ||
| } | ||
| tail = append([]string{filepath.Base(cur)}, tail...) | ||
| cur = parent | ||
| } | ||
| } | ||
|
|
||
| // isUnderDir checks whether child is under parent directory. | ||
| func isUnderDir(child, parent string) bool { | ||
| rel, err := filepath.Rel(parent, child) | ||
| if err != nil { | ||
| return false | ||
| } | ||
| return !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != ".." | ||
| return localfileio.SafeOutputPath(path) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if SafeInputPath is still used anywhere in the codebase
echo "=== Searching for SafeInputPath usage ==="
rg -n 'SafeInputPath' --type=go
echo ""
echo "=== Check if localfileio exports SafeInputPath ==="
rg -n 'func SafeInputPath' internal/vfs/localfileio/Repository: larksuite/cli
Length of output: 686
🏁 Script executed:
#!/bin/bash
# Check validate package structure
echo "=== List files in internal/validate ==="
ls -la internal/validate/
echo ""
echo "=== Search for SafeInputPath definition in validate package ==="
rg -n 'func SafeInputPath' internal/validate/
echo ""
echo "=== Check internal/validate/path.go for all exported functions ==="
rg -n '^func ' internal/validate/path.goRepository: larksuite/cli
Length of output: 957
🏁 Script executed:
#!/bin/bash
# Check what's in input.go
echo "=== Content of internal/validate/input.go ==="
cat -n internal/validate/input.go
echo ""
echo "=== Search for SafeInputPath anywhere in codebase (including non-go files) ==="
rg -n 'SafeInputPath' --max-count=20Repository: larksuite/cli
Length of output: 3679
🏁 Script executed:
#!/bin/bash
# Check AGENTS.md documentation
echo "=== AGENTS.md around line 72 ==="
sed -n '65,80p' AGENTS.md
echo ""
echo "=== drive_import.go around line 144 ==="
sed -n '140,155p' shortcuts/drive/drive_import.go
echo ""
echo "=== limits.go around line 35 ==="
sed -n '30,45p' shortcuts/mail/draft/limits.goRepository: larksuite/cli
Length of output: 2060
Update documentation and stale comments to reflect FileIO-based path validation.
SafeInputPath has been intentionally removed as part of the refactoring to consolidate validation within FileIO abstractions. This is correct—callers now use fio.Stat(), fio.Open(), and similar methods which validate paths internally.
However, documentation and comments have not been updated to reflect this pattern:
- AGENTS.md (line 72) still instructs: "Call
validate.SafeInputPathbefore any file I/O" - shortcuts/mail/draft/limits.go (line 35) still references the old pattern: "Callers must validate the file path via validate.SafeInputPath"
Update these to document the new pattern: validation is performed automatically by FileIO operations (e.g., as shown in shortcuts/drive/drive_import.go:144).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/validate/path.go` around lines 6 - 11, Update the stale
docs/comments that still instruct callers to use validate.SafeInputPath: change
AGENTS.md (around the previous line 72) and the comment in
shortcuts/mail/draft/limits.go (previously referencing "Callers must validate
the file path via validate.SafeInputPath") to instead state that path validation
is performed by FileIO operations and callers should use FileIO methods such as
fio.Stat(), fio.Open(), fio.Read(), etc.; reference the existing example in
shortcuts/drive/drive_import.go (around the usage at line 144) to show the new
pattern and remove any mention of SafeInputPath or validate.SafeInputPath.
| func (l *LocalFileIO) Open(name string) (fileio.File, error) { | ||
| safePath, err := safeInputPath(name) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return os.Open(safePath) | ||
| } | ||
|
|
||
| // Stat returns file metadata after validating the path. | ||
| func (l *LocalFileIO) Stat(name string) (os.FileInfo, error) { | ||
| safePath, err := safeInputPath(name) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return os.Stat(safePath) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Use internal/vfs for the remaining filesystem calls here.
os.Open, os.Stat, and os.MkdirAll bypass the repo's filesystem wrapper, so this implementation will not respect any vfs indirection consistently. Please route these through vfs.* as well, or add the missing wrappers before merging.
As per coding guidelines, Use vfs.* instead of os.* for all filesystem access.
Also applies to: 59-67
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/vfs/localfileio/localfileio.go` around lines 34 - 49, The Open and
Stat methods in LocalFileIO (and the other method around MkdirAll at lines
~59-67) call os.Open, os.Stat, and os.MkdirAll directly which bypasses the repo
VFS wrapper; change these to call the vfs package equivalents (e.g., vfs.Open,
vfs.Stat, vfs.MkdirAll or the project’s vfs.FS interface methods used elsewhere)
after validating the path with safeInputPath so all filesystem access
consistently goes through internal/vfs; update imports to include the vfs
package and ensure error propagation stays the same.
| // FileIO resolves the FileIO using the current execution context. | ||
| // Falls back to the globally registered provider when Factory or its | ||
| // FileIOProvider is nil (e.g. in lightweight test helpers). | ||
| func (ctx *RuntimeContext) FileIO() fileio.FileIO { | ||
| if ctx != nil && ctx.Factory != nil { | ||
| if fio := ctx.Factory.ResolveFileIO(ctx.ctx); fio != nil { | ||
| return fio | ||
| } | ||
| } | ||
| if p := fileio.GetProvider(); p != nil { | ||
| c := context.Background() | ||
| if ctx != nil { | ||
| c = ctx.ctx | ||
| } | ||
| return p.ResolveFileIO(c) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if any FileIO() caller handles nil return
echo "=== Checking FileIO() usage patterns ==="
rg -n '\.FileIO\(\)' --type=go -A2 | head -80Repository: larksuite/cli
Length of output: 5586
🏁 Script executed:
#!/bin/bash
# Check the two specific callers mentioned in the original comment
echo "=== ValidatePath (line 323) ==="
sed -n '318,330p' shortcuts/common/runner.go
echo ""
echo "=== resolveInputFlags (line 608) ==="
sed -n '603,620p' shortcuts/common/runner.go
# Check if Factory/provider is always initialized
echo ""
echo "=== Check RuntimeContext initialization and Factory setup ==="
rg -n 'NewRuntimeContext|\.Factory\s*=' --type=go shortcuts/common/runner.go | head -20Repository: larksuite/cli
Length of output: 1132
🏁 Script executed:
#!/bin/bash
# Check where RuntimeContext is created and if Factory is always set
echo "=== RuntimeContext initialization ==="
rg -n 'RuntimeContext\{|&RuntimeContext\{' --type=go | head -20
echo ""
echo "=== Check fileio.GetProvider setup ==="
rg -n 'GetProvider|RegisterProvider' --type=go | head -20
# Check imports at top of runner.go to understand dependencies
echo ""
echo "=== Top of runner.go for context ==="
head -30 shortcuts/common/runner.goRepository: larksuite/cli
Length of output: 3861
🏁 Script executed:
#!/bin/bash
# Check how ValidatePath and resolveInputFlags are used/called
echo "=== ValidatePath usage ==="
rg -n 'ValidatePath' --type=go | head -20
echo ""
echo "=== Check factory_default.go for fileio provider setup ==="
sed -n '45,55p' internal/cmdutil/factory_default.go
echo ""
echo "=== Check if fileio.GetProvider() is initialized in main flow ==="
rg -n 'fileio.*Register|SetProvider' --type=go | head -15Repository: larksuite/cli
Length of output: 1331
🏁 Script executed:
#!/bin/bash
# Check if ValidatePath callers might be called in tests or production only
echo "=== Check one ValidatePath caller (sheet_export.go:66) in context ==="
sed -n '60,75p' shortcuts/sheets/sheet_export.go
echo ""
echo "=== Check localfileio registration more thoroughly ==="
cat internal/vfs/localfileio/localfileio.go
echo ""
echo "=== Check fileio registry.go to understand GetProvider behavior ==="
cat extension/fileio/registry.goRepository: larksuite/cli
Length of output: 3213
🏁 Script executed:
#!/bin/bash
# Check if localfileio is imported in main application flow
echo "=== Check main.go or cmd entrypoint ==="
find . -name "main.go" -type f | head -5 | xargs grep -l "localfileio\|fileio" 2>/dev/null
echo ""
echo "=== Check cmd package for imports ==="
fd -e go "cmd.*main" | head -3 | xargs head -30
echo ""
echo "=== Check if localfileio is imported anywhere in cmd/main flow ==="
rg 'import.*localfileio' --type=goRepository: larksuite/cli
Length of output: 288
🏁 Script executed:
#!/bin/bash
# Find the actual main entry point
echo "=== Find main function ==="
rg -n 'func main\(' --type=go
echo ""
echo "=== Check validate/path.go which imports localfileio ==="
head -40 internal/validate/path.go
echo ""
echo "=== Check if validate.SafeInputPath is used (mentioned in learnings) ==="
rg -n 'validate.SafeInputPath\|SafeInputPath' --type=go | head -20Repository: larksuite/cli
Length of output: 554
🏁 Script executed:
#!/bin/bash
# Check main.go for imports
echo "=== main.go full content ==="
cat main.go
echo ""
echo "=== Check if there's an init or import that brings in localfileio ==="
grep -r "_ \".*localfileio\"" --include="*.go" | head -10Repository: larksuite/cli
Length of output: 770
🏁 Script executed:
#!/bin/bash
# Check if factory_default.go is used in cmd/Execute flow
echo "=== Check cmd package ==="
head -50 cmd/root.go 2>/dev/null || head -50 cmd/*.go | head -60
echo ""
echo "=== Look for NewFactory or factory creation in cmd ==="
rg -n 'NewFactory|factory_default\|Factory\(' cmd/ --type=go | head -15Repository: larksuite/cli
Length of output: 1473
🏁 Script executed:
#!/bin/bash
# Check where RuntimeContext is created with Factory in main flow (line 545 in runner.go)
echo "=== Context around line 545 in runner.go ==="
sed -n '530,560p' shortcuts/common/runner.go
echo ""
echo "=== Look for callers of NewRuntimeContext or similar ==="
rg -n 'func.*NewRuntimeContext|executeShortcut\|RuntimeContext.*Factory' shortcuts/common/ --type=go | head -15Repository: larksuite/cli
Length of output: 1726
🏁 Script executed:
#!/bin/bash
# Final verification: check if Factory is ALWAYS set in production path
echo "=== Trace Execute path to see if Factory is always provided ==="
rg -n 'func Execute' cmd/ --type=go -A 30 | head -50
echo ""
echo "=== Check if there's any code path that creates RuntimeContext without Factory ==="
rg -n 'newRuntimeContext' shortcuts/common/ --type=go -B 5 | head -30
echo ""
echo "=== Summary of FileIO() method contract ==="
sed -n '299,316p' shortcuts/common/runner.goRepository: larksuite/cli
Length of output: 3324
Add nil check for FileIO() in ValidatePath and resolveInputFlags, or document the guarantee.
While FileIO() can theoretically return nil when both the Factory provider and global provider are unavailable, the production code path ensures this won't happen—Factory is always initialized in Execute() with a FileIOProvider, and localfileio is registered at init time. However, ValidatePath() (line 323) and resolveInputFlags() (line 612) both dereference the result without nil checks.
For defensive programming, either:
- Add nil checks before calling methods on FileIO()
- Document clearly that FileIO() is guaranteed non-nil in production contexts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shortcuts/common/runner.go` around lines 299 - 316, ValidatePath and
resolveInputFlags currently call ctx.FileIO() and dereference it without
checking for nil; add defensive nil checks where FileIO() is used (in the
ValidatePath and resolveInputFlags functions) and return a clear error (or
fallback behavior) if ctx.FileIO() == nil. Specifically, in ValidatePath and in
resolveInputFlags locate calls like ctx.FileIO().Stat/Read/Open and guard them
with fio := ctx.FileIO(); if fio == nil { return fmt.Errorf("file IO provider
not available") } (or appropriate error handling), ensuring
RuntimeContext.FileIO() is safely used even if Factory/FileIOProvider or global
provider are absent; alternatively, if you prefer contract documentation, add a
comment on RuntimeContext.FileIO/Execute guaranteeing non-nil in production and
add a runtime check in Execute to panic or return an error if
Factory/FileIOProvider is unset.
| // Validate file | ||
| stat, err := vfs.Stat(filePath) | ||
| stat, err := runtime.FileIO().Stat(filePath) | ||
| if err != nil { | ||
| return output.ErrValidation("file not found: %s", filePath) | ||
| } |
There was a problem hiding this comment.
Preserve the original FileIO error instead of rewriting everything to "file not found".
Now that runtime.FileIO().Stat() owns path and permission checks, this branch turns unsafe-path and permission failures into the same missing-file message. That makes the error misleading and regresses the clearer validation feedback from the new FileIO layer.
Suggested fix
// Validate file
stat, err := runtime.FileIO().Stat(filePath)
if err != nil {
- return output.ErrValidation("file not found: %s", filePath)
+ var exitErr *output.ExitError
+ if errors.As(err, &exitErr) {
+ return err
+ }
+ return output.ErrValidation("cannot read file %q: %v", filePath, err)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shortcuts/doc/doc_media_insert.go` around lines 115 - 119, The current branch
that handles errors from runtime.FileIO().Stat(filePath) hides the original
error by always returning output.ErrValidation("file not found: %s", filePath);
change it to preserve and return the underlying error (or wrap it) so
path/permission errors from runtime.FileIO().Stat are conveyed. Locate the Stat
call in doc_media_insert.go (runtime.FileIO().Stat(filePath)), and replace the
unconditional "file not found" return with a return that includes err (e.g.,
wrap err into output.ErrValidation or return err directly) while keeping the
filePath context.
| "fmt" | ||
| "net/http" | ||
| "path/filepath" | ||
| "os" |
There was a problem hiding this comment.
Don’t derive FileIO semantics from os.IsNotExist.
This gate assumes every fileio.FileIO.Stat miss satisfies os.IsNotExist, which is not part of the FileIO contract. A wrapped or provider-specific miss will be reported here as unsafe output path, and this also bypasses the centralized runtime.ValidatePath(...) flow the rest of the refactor moved to.
Suggested adjustment
- "os"
@@
- // Early path validation + overwrite check via FileIO.Stat
- if _, statErr := runtime.FileIO().Stat(outputPath); statErr != nil && !os.IsNotExist(statErr) {
- return output.ErrValidation("unsafe output path: %s", statErr)
- } else if statErr == nil && !overwrite {
- return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
- }
+ if err := runtime.ValidatePath(outputPath); err != nil {
+ return output.ErrValidation("unsafe output path: %s", err)
+ }
+ if !overwrite {
+ if _, statErr := runtime.FileIO().Stat(outputPath); statErr == nil {
+ return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)
+ }
+ }As per coding guidelines, "Validate paths using validate.SafeInputPath before any file I/O operations".
Also applies to: 55-60
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shortcuts/drive/drive_download.go` at line 10, The code currently treats any
fileio.FileIO.Stat error as an os.IsNotExist miss and labels it an "unsafe
output path"; instead, validate the path up-front and treat Stat errors
generically: call validate.SafeInputPath (or runtime.ValidatePath where used in
the codebase) on the path before invoking fileio.FileIO.Stat, remove any
os.IsNotExist-based branching, and on Stat errors return or propagate the
original error (or a clear not-found error) rather than assuming os.IsNotExist;
update the logic around the fileio.FileIO.Stat call in drive_download.go to use
validate.SafeInputPath/runtime.ValidatePath and handle Stat errors without
os.IsNotExist checks.
| func validateMediaFlagPath(fio fileio.FileIO, flagName, value string) error { | ||
| if value == "" || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") || isMediaKey(value) { | ||
| return nil | ||
| } | ||
| if _, err := fio.Stat(value); err != nil && !os.IsNotExist(err) { | ||
| return output.ErrValidation("%s: %v", flagName, err) | ||
| } | ||
| return nil |
There was a problem hiding this comment.
Reject missing local media files here.
fio.Stat failures are ignored when the file does not exist, so values like --image ./missing.png now pass both Validate and the early Execute preflight and only fail later in the upload path. That regresses the old “bad local path” feedback and makes user errors surface much later.
🐛 Proposed fix
-import (
- "context"
- "encoding/json"
- "net/http"
- "os"
- "strings"
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "strings"
@@
func validateMediaFlagPath(fio fileio.FileIO, flagName, value string) error {
if value == "" || strings.HasPrefix(value, "http://") || strings.HasPrefix(value, "https://") || isMediaKey(value) {
return nil
}
- if _, err := fio.Stat(value); err != nil && !os.IsNotExist(err) {
+ if _, err := fio.Stat(value); err != nil {
return output.ErrValidation("%s: %v", flagName, err)
}
return nil
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shortcuts/im/im_messages_send.go` around lines 214 - 221,
validateMediaFlagPath currently ignores missing local files because it only
returns an error when Stat fails with something other than os.IsNotExist; update
validateMediaFlagPath to reject non-empty, non-URL, non-media-key paths that do
not exist by checking fio.Stat(value) and returning output.ErrValidation("%s:
%v", flagName, err) whenever err != nil (including os.IsNotExist), while
preserving the early return for URLs/media keys/empty values — change the
conditional around fio.Stat in validateMediaFlagPath to return a validation
error on any Stat error instead of skipping when os.IsNotExist(err).
| // readFile reads the named file and returns its contents via the Builder's FileIO. | ||
| func (b Builder) readFile(path string) ([]byte, error) { | ||
| f, err := b.fio.Open(path) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("attachment %q: %w", path, err) | ||
| } | ||
| return vfs.ReadFile(safePath) | ||
| defer f.Close() | ||
| return io.ReadAll(f) | ||
| } |
There was a problem hiding this comment.
Potential nil pointer dereference if WithFileIO is not called.
The readFile method calls b.fio.Open(path) but fio could be nil if WithFileIO was never called on the builder. This would cause a panic.
Consider adding a nil check or documenting that WithFileIO must be called before using file-based methods.
🛡️ Proposed defensive check
func (b Builder) readFile(path string) ([]byte, error) {
+ if b.fio == nil {
+ return nil, fmt.Errorf("attachment %q: FileIO not configured (call WithFileIO first)", path)
+ }
f, err := b.fio.Open(path)
if err != nil {
return nil, fmt.Errorf("attachment %q: %w", path, err)
}
defer f.Close()
return io.ReadAll(f)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shortcuts/mail/emlbuilder/builder.go` around lines 62 - 70, The readFile
method can panic if Builder.fio is nil (when WithFileIO was not called); update
Builder.readFile to check if b.fio == nil and return a clear error (e.g., "file
IO not initialized: call WithFileIO") instead of calling b.fio.Open, and
reference the Builder.readFile and WithFileIO symbols so the check is added near
the start of that method; ensure the error message is wrapped/returned
consistently with existing fmt.Errorf usage.
Add ResolvePath to FileIO interface so callers can obtain the validated absolute path without re-importing internal path helpers. Update all download shortcuts and SaveResponse to return resolved absolute paths in saved_path output, fixing a regression where relative paths were returned instead of absolute ones. Also add nil guard in RuntimeContext.ValidatePath for defensive safety. Change-Id: I8bb0e323f7e44149df5e6f558a030e505380a1ce
Change-Id: If8a0f8588dddf4dca8741a2646881c7729fdfa2e
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
shortcuts/common/runner_jq_test.go (1)
156-168:⚠️ Potential issue | 🟠 MajorUse the repo test factory helper and isolate config env in this test helper.
Line 156still buildscmdutil.Factorymanually, and this helper path does not isolateLARKSUITE_CLI_CONFIG_DIR. Please switch tocmdutil.TestFactory(t, config)and set per-test config dir viat.Setenv(...)to keep tests hermetic.As per coding guidelines,
**/*_test.go: Usecmdutil.TestFactory(t, config)for creating test factories in Go tests and isolate config state witht.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/common/runner_jq_test.go` around lines 156 - 168, The test helper newTestFactory() currently constructs a cmdutil.Factory manually and doesn't isolate config state; replace its usage with the repository test helper by switching to cmdutil.TestFactory(t, config) in tests and ensure you call t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) so each test gets a hermetic per-test config dir; locate references to newTestFactory and update the test to create a cfg (core.CliConfig with AppID/AppSecret/Brand) passed into cmdutil.TestFactory(t, cfg) and remove the manual Factory construction and direct LarkClient/IOStreams setup.shortcuts/minutes/minutes_download.go (1)
264-287:⚠️ Potential issue | 🟠 MajorValidate the finalized output path before writing.
outputPathis only known after this block, but the code goes straight toStat/Save. An unsafe path currently comes back as a latecannot create fileafter the download request has already succeeded. Validate it once here and return a validation error before touching the body.As per coding guidelines, `Validate paths using validate.SafeInputPath before any file I/O operations`.Suggested change
if outputPath == "" { filename := resolveFilenameFromResponse(resp, minuteToken) // Deduplicate filenames in batch mode: prefix with token on collision. if opts.usedNames != nil { @@ } outputPath = filepath.Join(opts.outputDir, filename) } + if _, err := opts.fio.ResolvePath(outputPath); err != nil { + return nil, output.ErrValidation("unsafe output path: %s", err) + } if !opts.overwrite { if _, statErr := opts.fio.Stat(outputPath); statErr == nil { return nil, output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/minutes/minutes_download.go` around lines 264 - 287, After computing outputPath (after the block that sets opts.outputPath / calls resolveFilenameFromResponse and deduplicates via opts.usedNames), validate the finalized path using validate.SafeInputPath(outputPath) and return an output.ErrValidation if validation fails; do this before any file I/O such as opts.fio.Stat or opts.fio.Save so you fail fast (before consuming resp.Body). Update the code paths around opts.overwrite/opts.fio.Stat and the subsequent opts.fio.Save call to assume a pre-validated outputPath, and reference the existing symbols resolveFilenameFromResponse, minuteToken, opts.usedNames, opts.outputDir, opts.overwrite, opts.fio.Stat, opts.fio.Save, and validate.SafeInputPath when making the change.
♻️ Duplicate comments (3)
internal/vfs/localfileio/localfileio.go (1)
34-39:⚠️ Potential issue | 🟠 MajorRoute these filesystem calls through
internal/vfs.
Open,Stat, andSavestill hitos.Open,os.Stat, andos.MkdirAlldirectly, so the new FileIO layer bypasses the repo’s VFS abstraction at its core filesystem boundaries.As per coding guidelines,
Use vfs.* instead of os.* for all filesystem access.Also applies to: 43-48, 64-72
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/vfs/localfileio/localfileio.go` around lines 34 - 39, The LocalFileIO methods (Open, Stat, Save) call os.Open/os.Stat/os.MkdirAll directly and must instead route filesystem access through the repo VFS layer; update LocalFileIO.Open, LocalFileIO.Stat and LocalFileIO.Save (and any use of safeInputPath) to call the corresponding internal/vfs functions (e.g., vfs.Open, vfs.Stat, vfs.MkdirAll or their equivalents) and preserve existing error handling/return values so the FileIO implementation uses the VFS abstraction rather than the os package.shortcuts/drive/drive_download.go (1)
55-60:⚠️ Potential issue | 🟠 MajorDon’t key FileIO miss handling off
os.IsNotExist.
fileio.FileIO.Statdoes not promiseos.IsNotExistsemantics. A provider-specific validation/miss error is misclassified here, and the directruntime.FileIO().Stat(...)call still panics when no provider is registered. Reuseruntime.ValidatePath(outputPath)for the safety check, then only treatstatErr == nilas “already exists”. This also lets you drop theosimport.As per coding guidelines, `Validate paths using validate.SafeInputPath before any file I/O operations`.Suggested change
- // Early path validation + overwrite check via FileIO.Stat - if _, statErr := runtime.FileIO().Stat(outputPath); statErr != nil && !os.IsNotExist(statErr) { - return output.ErrValidation("unsafe output path: %s", statErr) - } else if statErr == nil && !overwrite { - return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath) - } + if err := runtime.ValidatePath(outputPath); err != nil { + return output.ErrValidation("unsafe output path: %s", err) + } + if !overwrite { + if _, statErr := runtime.FileIO().Stat(outputPath); statErr == nil { + return output.ErrValidation("output file already exists: %s (use --overwrite to replace)", outputPath) + } + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/drive/drive_download.go` around lines 55 - 60, Replace the runtime.FileIO().Stat-based validation with a call to runtime.ValidatePath(outputPath) (or validate.SafeInputPath per guidelines) to perform safety checks and avoid panics when no FileIO provider is registered, then only treat a subsequent runtime.FileIO().Stat(outputPath) result of statErr == nil as “already exists” (return the overwrite validation error when statErr == nil and !overwrite); ignore provider-specific/stat errors instead of checking os.IsNotExist and remove the os import. Ensure you update the code around the existing runtime.FileIO().Stat usage and the output.ErrValidation checks (referencing runtime.ValidatePath, runtime.FileIO().Stat, and overwrite).internal/cmdutil/testing.go (1)
95-104:⚠️ Potential issue | 🟠 MajorKeep
TestChdiron the VFS abstraction.This helper still uses process-wide
os.Chdirdirectly, and the cleanup path drops any restore failure. Please add/use a VFSChdirwrapper here so both the switch and restore stay inside the repo abstraction and can fail the test if cleanup breaks.As per coding guidelines,
Use vfs.* instead of os.* for all filesystem access.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/cmdutil/testing.go` around lines 95 - 104, The TestChdir helper currently calls os.Chdir and ignores errors on restore; change it to use the VFS abstraction by calling vfs.Chdir(dir) for the switch and capture any error, and in the t.Cleanup closure call vfs.Chdir(orig) and fail the test (t.Fatalf) if that restore returns an error; keep the existing vfs.Getwd usage and preserve the nolint comments only if still applicable.
🧹 Nitpick comments (1)
shortcuts/base/helpers.go (1)
86-99: Extract the repeated comma-splitting logic into one helper.Line 90–Line 98 duplicates behavior already present in
parseStringListFlexible(Line 74–Line 82), which can drift over time.♻️ Proposed refactor
func parseStringListFlexible(fio fileio.FileIO, raw string, flagName string) ([]string, error) { @@ - parts := strings.Split(raw, ",") - result := make([]string, 0, len(parts)) - for _, part := range parts { - item := strings.TrimSpace(part) - if item != "" { - result = append(result, item) - } - } - return result, nil + return splitCommaList(raw), nil } func parseStringList(raw string) []string { raw = strings.TrimSpace(raw) if raw == "" { return nil } + return splitCommaList(raw) +} + +func splitCommaList(raw string) []string { parts := strings.Split(raw, ",") result := make([]string, 0, len(parts)) for _, part := range parts { item := strings.TrimSpace(part) if item != "" { result = append(result, item) } } return result }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@shortcuts/base/helpers.go` around lines 86 - 99, Extract the duplicated comma-splitting/trimming logic into a single helper (e.g., splitAndTrimCommaSeparated(raw string) []string) and replace the duplicated block in the current function (the block shown) and parseStringListFlexible to call this helper; the helper should TrimSpace(raw), return nil for empty input, Split by ",", TrimSpace each part, skip empty items, and return the resulting slice so nil-vs-empty semantics are preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/cmdutil/factory_default_test.go`:
- Around line 211-231: The test
TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization can pick up
machine-local config because NewDefault follows normal initialization; to
isolate state, set the config dir to a temp directory at the start of the test
by calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) before invoking
NewDefault(InvocationContext{}), so the factory initialization (NewDefault) will
use an isolated config directory for the duration of the test.
In `@shortcuts/common/runner_jq_test.go`:
- Around line 109-114: The testResolvedFileIO stub methods (Open, Stat,
ResolvePath, Save) have formatting that causes gofmt drift; run gofmt (or gofmt
-w) on shortcuts/common/runner_jq_test.go to reformat these method declarations
so they follow Go formatting rules (ensuring spacing and line breaks for the
return statements match gofmt output) and commit the resulting changes.
In `@shortcuts/minutes/minutes_download.go`:
- Around line 81-82: The code dereferences runtime.FileIO() (used in the Stat
call and later saved into downloadOpts used by downloadMediaFile) without
checking for nil; add an explicit nil check after calling runtime.FileIO() and
return a CLI error (e.g., via output.ErrValidation or a suitable error helper)
if no provider is registered so the code fails fast instead of panicking. Update
the two occurrences (the Stat check around outputPath and the later assignment
into downloadOpts) to validate fileIO != nil before using or storing it, and
return a clear error message like "no FileIO provider registered" when nil.
---
Outside diff comments:
In `@shortcuts/common/runner_jq_test.go`:
- Around line 156-168: The test helper newTestFactory() currently constructs a
cmdutil.Factory manually and doesn't isolate config state; replace its usage
with the repository test helper by switching to cmdutil.TestFactory(t, config)
in tests and ensure you call t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
so each test gets a hermetic per-test config dir; locate references to
newTestFactory and update the test to create a cfg (core.CliConfig with
AppID/AppSecret/Brand) passed into cmdutil.TestFactory(t, cfg) and remove the
manual Factory construction and direct LarkClient/IOStreams setup.
In `@shortcuts/minutes/minutes_download.go`:
- Around line 264-287: After computing outputPath (after the block that sets
opts.outputPath / calls resolveFilenameFromResponse and deduplicates via
opts.usedNames), validate the finalized path using
validate.SafeInputPath(outputPath) and return an output.ErrValidation if
validation fails; do this before any file I/O such as opts.fio.Stat or
opts.fio.Save so you fail fast (before consuming resp.Body). Update the code
paths around opts.overwrite/opts.fio.Stat and the subsequent opts.fio.Save call
to assume a pre-validated outputPath, and reference the existing symbols
resolveFilenameFromResponse, minuteToken, opts.usedNames, opts.outputDir,
opts.overwrite, opts.fio.Stat, opts.fio.Save, and validate.SafeInputPath when
making the change.
---
Duplicate comments:
In `@internal/cmdutil/testing.go`:
- Around line 95-104: The TestChdir helper currently calls os.Chdir and ignores
errors on restore; change it to use the VFS abstraction by calling
vfs.Chdir(dir) for the switch and capture any error, and in the t.Cleanup
closure call vfs.Chdir(orig) and fail the test (t.Fatalf) if that restore
returns an error; keep the existing vfs.Getwd usage and preserve the nolint
comments only if still applicable.
In `@internal/vfs/localfileio/localfileio.go`:
- Around line 34-39: The LocalFileIO methods (Open, Stat, Save) call
os.Open/os.Stat/os.MkdirAll directly and must instead route filesystem access
through the repo VFS layer; update LocalFileIO.Open, LocalFileIO.Stat and
LocalFileIO.Save (and any use of safeInputPath) to call the corresponding
internal/vfs functions (e.g., vfs.Open, vfs.Stat, vfs.MkdirAll or their
equivalents) and preserve existing error handling/return values so the FileIO
implementation uses the VFS abstraction rather than the os package.
In `@shortcuts/drive/drive_download.go`:
- Around line 55-60: Replace the runtime.FileIO().Stat-based validation with a
call to runtime.ValidatePath(outputPath) (or validate.SafeInputPath per
guidelines) to perform safety checks and avoid panics when no FileIO provider is
registered, then only treat a subsequent runtime.FileIO().Stat(outputPath)
result of statErr == nil as “already exists” (return the overwrite validation
error when statErr == nil and !overwrite); ignore provider-specific/stat errors
instead of checking os.IsNotExist and remove the os import. Ensure you update
the code around the existing runtime.FileIO().Stat usage and the
output.ErrValidation checks (referencing runtime.ValidatePath,
runtime.FileIO().Stat, and overwrite).
---
Nitpick comments:
In `@shortcuts/base/helpers.go`:
- Around line 86-99: Extract the duplicated comma-splitting/trimming logic into
a single helper (e.g., splitAndTrimCommaSeparated(raw string) []string) and
replace the duplicated block in the current function (the block shown) and
parseStringListFlexible to call this helper; the helper should TrimSpace(raw),
return nil for empty input, Split by ",", TrimSpace each part, skip empty items,
and return the resulting slice so nil-vs-empty semantics are preserved.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5f0caf83-bf43-4d93-86b5-f17aa7b5ed34
📒 Files selected for processing (18)
extension/fileio/types.gointernal/client/response.gointernal/client/response_test.gointernal/cmdutil/factory_default_test.gointernal/cmdutil/testing.gointernal/vfs/localfileio/localfileio.gointernal/vfs/localfileio/path.gointernal/vfs/localfileio/path_test.goshortcuts/base/helpers.goshortcuts/common/runner.goshortcuts/common/runner_jq_test.goshortcuts/doc/doc_media_download.goshortcuts/drive/drive_download.goshortcuts/drive/drive_export_common.goshortcuts/drive/drive_export_test.goshortcuts/im/im_messages_resources_download.goshortcuts/minutes/minutes_download.goshortcuts/sheets/sheet_export.go
✅ Files skipped from review due to trivial changes (2)
- extension/fileio/types.go
- shortcuts/common/runner.go
🚧 Files skipped from review as they are similar to previous changes (8)
- shortcuts/drive/drive_export_test.go
- internal/client/response_test.go
- internal/vfs/localfileio/path_test.go
- internal/client/response.go
- internal/vfs/localfileio/path.go
- shortcuts/sheets/sheet_export.go
- shortcuts/im/im_messages_resources_download.go
- shortcuts/drive/drive_export_common.go
| func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) { | ||
| prev := fileio.GetProvider() | ||
| provider := &countingFileIOProvider{} | ||
| fileio.Register(provider) | ||
| t.Cleanup(func() { fileio.Register(prev) }) | ||
|
|
||
| mid := &extensionMiddleware{Base: capturer, Ext: tamperIC} | ||
|
|
||
| origCtx := context.WithValue(context.Background(), testKey, "original") | ||
| req, _ := http.NewRequestWithContext(origCtx, "GET", srv.URL, nil) | ||
| resp, err := mid.RoundTrip(req) | ||
| if err != nil { | ||
| t.Fatalf("request failed: %v", err) | ||
| } | ||
| resp.Body.Close() | ||
|
|
||
| // Built-in chain should see original context, not tampered | ||
| if ctxValue != "original" { | ||
| t.Fatalf("built-in chain saw context value %q, want %q", ctxValue, "original") | ||
| } | ||
| } | ||
|
|
||
| // interceptorFunc adapts a function to exttransport.Interceptor. | ||
| type interceptorFunc func(*http.Request) func(*http.Response, error) | ||
|
|
||
| func (f interceptorFunc) PreRoundTrip(req *http.Request) func(*http.Response, error) { return f(req) } | ||
|
|
||
| func TestBuildSDKTransport_WithExtension(t *testing.T) { | ||
| exttransport.Register(&stubTransportProvider{}) | ||
| t.Cleanup(func() { exttransport.Register(nil) }) | ||
|
|
||
| transport := buildSDKTransport() | ||
|
|
||
| // Chain: extensionMiddleware → SecurityPolicy → UserAgent → Retry → Base | ||
| mid, ok := transport.(*extensionMiddleware) | ||
| if !ok { | ||
| t.Fatalf("outer transport type = %T, want *extensionMiddleware", transport) | ||
| } | ||
| sec, ok := mid.Base.(*internalauth.SecurityPolicyTransport) | ||
| if !ok { | ||
| t.Fatalf("transport type = %T, want *auth.SecurityPolicyTransport", mid.Base) | ||
| } | ||
| ua, ok := sec.Base.(*UserAgentTransport) | ||
| if !ok { | ||
| t.Fatalf("transport type = %T, want *UserAgentTransport", sec.Base) | ||
| f := NewDefault(InvocationContext{}) | ||
| if f.FileIOProvider != provider { | ||
| t.Fatalf("NewDefault() provider = %T, want %T", f.FileIOProvider, provider) | ||
| } | ||
| if _, ok := ua.Base.(*RetryTransport); !ok { | ||
| t.Fatalf("innermost transport type = %T, want *RetryTransport", ua.Base) | ||
| if provider.resolveCalls != 0 { | ||
| t.Fatalf("ResolveFileIO() calls after NewDefault() = %d, want 0", provider.resolveCalls) | ||
| } | ||
| } | ||
|
|
||
| func TestBuildSDKTransport_WithoutExtension(t *testing.T) { | ||
| exttransport.Register(nil) | ||
|
|
||
| transport := buildSDKTransport() | ||
|
|
||
| sec, ok := transport.(*internalauth.SecurityPolicyTransport) | ||
| if !ok { | ||
| t.Fatalf("outer transport type = %T, want *auth.SecurityPolicyTransport", transport) | ||
| } | ||
| ua, ok := sec.Base.(*UserAgentTransport) | ||
| if !ok { | ||
| t.Fatalf("middle transport type = %T, want *UserAgentTransport", sec.Base) | ||
| if got := f.ResolveFileIO(context.Background()); got == nil { | ||
| t.Fatal("ResolveFileIO() = nil, want non-nil") | ||
| } | ||
| if _, ok := ua.Base.(*RetryTransport); !ok { | ||
| t.Fatalf("inner transport type = %T, want *RetryTransport", ua.Base) | ||
| if provider.resolveCalls != 1 { | ||
| t.Fatalf("ResolveFileIO() calls after explicit resolve = %d, want 1", provider.resolveCalls) | ||
| } | ||
| } |
There was a problem hiding this comment.
Isolate config state in this test.
NewDefault still walks the normal factory initialization path, so this test can pick up machine-local config unless it sets a temp config dir first.
Suggested change
func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) {
+ t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
prev := fileio.GetProvider()
provider := &countingFileIOProvider{}
fileio.Register(provider)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) { | |
| prev := fileio.GetProvider() | |
| provider := &countingFileIOProvider{} | |
| fileio.Register(provider) | |
| t.Cleanup(func() { fileio.Register(prev) }) | |
| mid := &extensionMiddleware{Base: capturer, Ext: tamperIC} | |
| origCtx := context.WithValue(context.Background(), testKey, "original") | |
| req, _ := http.NewRequestWithContext(origCtx, "GET", srv.URL, nil) | |
| resp, err := mid.RoundTrip(req) | |
| if err != nil { | |
| t.Fatalf("request failed: %v", err) | |
| } | |
| resp.Body.Close() | |
| // Built-in chain should see original context, not tampered | |
| if ctxValue != "original" { | |
| t.Fatalf("built-in chain saw context value %q, want %q", ctxValue, "original") | |
| } | |
| } | |
| // interceptorFunc adapts a function to exttransport.Interceptor. | |
| type interceptorFunc func(*http.Request) func(*http.Response, error) | |
| func (f interceptorFunc) PreRoundTrip(req *http.Request) func(*http.Response, error) { return f(req) } | |
| func TestBuildSDKTransport_WithExtension(t *testing.T) { | |
| exttransport.Register(&stubTransportProvider{}) | |
| t.Cleanup(func() { exttransport.Register(nil) }) | |
| transport := buildSDKTransport() | |
| // Chain: extensionMiddleware → SecurityPolicy → UserAgent → Retry → Base | |
| mid, ok := transport.(*extensionMiddleware) | |
| if !ok { | |
| t.Fatalf("outer transport type = %T, want *extensionMiddleware", transport) | |
| } | |
| sec, ok := mid.Base.(*internalauth.SecurityPolicyTransport) | |
| if !ok { | |
| t.Fatalf("transport type = %T, want *auth.SecurityPolicyTransport", mid.Base) | |
| } | |
| ua, ok := sec.Base.(*UserAgentTransport) | |
| if !ok { | |
| t.Fatalf("transport type = %T, want *UserAgentTransport", sec.Base) | |
| f := NewDefault(InvocationContext{}) | |
| if f.FileIOProvider != provider { | |
| t.Fatalf("NewDefault() provider = %T, want %T", f.FileIOProvider, provider) | |
| } | |
| if _, ok := ua.Base.(*RetryTransport); !ok { | |
| t.Fatalf("innermost transport type = %T, want *RetryTransport", ua.Base) | |
| if provider.resolveCalls != 0 { | |
| t.Fatalf("ResolveFileIO() calls after NewDefault() = %d, want 0", provider.resolveCalls) | |
| } | |
| } | |
| func TestBuildSDKTransport_WithoutExtension(t *testing.T) { | |
| exttransport.Register(nil) | |
| transport := buildSDKTransport() | |
| sec, ok := transport.(*internalauth.SecurityPolicyTransport) | |
| if !ok { | |
| t.Fatalf("outer transport type = %T, want *auth.SecurityPolicyTransport", transport) | |
| } | |
| ua, ok := sec.Base.(*UserAgentTransport) | |
| if !ok { | |
| t.Fatalf("middle transport type = %T, want *UserAgentTransport", sec.Base) | |
| if got := f.ResolveFileIO(context.Background()); got == nil { | |
| t.Fatal("ResolveFileIO() = nil, want non-nil") | |
| } | |
| if _, ok := ua.Base.(*RetryTransport); !ok { | |
| t.Fatalf("inner transport type = %T, want *RetryTransport", ua.Base) | |
| if provider.resolveCalls != 1 { | |
| t.Fatalf("ResolveFileIO() calls after explicit resolve = %d, want 1", provider.resolveCalls) | |
| } | |
| } | |
| func TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization(t *testing.T) { | |
| t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) | |
| prev := fileio.GetProvider() | |
| provider := &countingFileIOProvider{} | |
| fileio.Register(provider) | |
| t.Cleanup(func() { fileio.Register(prev) }) | |
| f := NewDefault(InvocationContext{}) | |
| if f.FileIOProvider != provider { | |
| t.Fatalf("NewDefault() provider = %T, want %T", f.FileIOProvider, provider) | |
| } | |
| if provider.resolveCalls != 0 { | |
| t.Fatalf("ResolveFileIO() calls after NewDefault() = %d, want 0", provider.resolveCalls) | |
| } | |
| if got := f.ResolveFileIO(context.Background()); got == nil { | |
| t.Fatal("ResolveFileIO() = nil, want non-nil") | |
| } | |
| if provider.resolveCalls != 1 { | |
| t.Fatalf("ResolveFileIO() calls after explicit resolve = %d, want 1", provider.resolveCalls) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/cmdutil/factory_default_test.go` around lines 211 - 231, The test
TestNewDefault_FileIOProviderDoesNotResolveDuringInitialization can pick up
machine-local config because NewDefault follows normal initialization; to
isolate state, set the config dir to a temp directory at the start of the test
by calling t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) before invoking
NewDefault(InvocationContext{}), so the factory initialization (NewDefault) will
use an isolated config directory for the duration of the test.
| if fi, err := runtime.FileIO().Stat(outputPath); err == nil && !fi.IsDir() { | ||
| return output.ErrValidation("--output %q is a file; batch mode expects a directory path", outputPath) |
There was a problem hiding this comment.
Fail fast when no FileIO provider is available.
runtime.FileIO() is dereferenced here and then stored into downloadOpts without a nil check. If a test or custom factory forgets to register a provider, downloadMediaFile will panic on Stat/Save instead of returning a normal CLI error.
Suggested change
errOut := runtime.IO().ErrOut
single := len(tokens) == 1
+ fio := runtime.FileIO()
+ if fio == nil {
+ return output.Errorf(output.ExitInternal, "io", "no file I/O provider registered")
+ }
// Batch mode: --output must be a directory, not an existing file.
if !single && outputPath != "" {
- if fi, err := runtime.FileIO().Stat(outputPath); err == nil && !fi.IsDir() {
+ if fi, err := fio.Stat(outputPath); err == nil && !fi.IsDir() {
return output.ErrValidation("--output %q is a file; batch mode expects a directory path", outputPath)
}
}
@@
- opts := downloadOpts{fio: runtime.FileIO(), overwrite: overwrite, usedNames: usedNames}
+ opts := downloadOpts{fio: fio, overwrite: overwrite, usedNames: usedNames}Also applies to: 164-165
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@shortcuts/minutes/minutes_download.go` around lines 81 - 82, The code
dereferences runtime.FileIO() (used in the Stat call and later saved into
downloadOpts used by downloadMediaFile) without checking for nil; add an
explicit nil check after calling runtime.FileIO() and return a CLI error (e.g.,
via output.ErrValidation or a suitable error helper) if no provider is
registered so the code fails fast instead of panicking. Update the two
occurrences (the Stat check around outputPath and the later assignment into
downloadOpts) to validate fileIO != nil before using or storing it, and return a
clear error message like "no FileIO provider registered" when nil.
Summary
Introduce a FileIO extension layer that abstracts file transfer operations (Open/Stat/Save) in shortcuts behind a replaceable interface, eliminating direct
ospackage dependencies and providing an extension point for server mode and other scenarios.Changes
extension/fileiopackage with Provider/FileIO/File interfaces and registryLocalFileIOdefault implementation with built-in path validation and atomic writesFileIO.SaveFileIO.Open/FileIO.StatRuntimeContext.ValidatePathto deduplicate Stat-based path checksTest Plan
lark xxxcommand works as expectedRelated Issues
Summary by CodeRabbit