feat: file system support for ls, mkdir, rm, push, pull and get app path#228
feat: file system support for ls, mkdir, rm, push, pull and get app path#228gmegidish wants to merge 16 commits into
Conversation
Adds ControllableDevice interface methods for app container filesystem operations (push, pull, ls, mkdir, rm, get-container-path) with working Android implementation for ls and path, and stubs for iOS, simulator, and remote devices.
- PullFile uses exec-out cat for /sdcard paths (binary-safe) and run-as cat for /data/user/ paths - ls and pull now accept a bare absolute path without a bundle-id - Fix ls to follow symlinks by retrying with trailing slash - Fix ls parser to handle absolute path names in single-file listings - Add runAdbCommandStdout helper for stdout-only adb output
- PushFile for /sdcard uses adb push directly - PushFile for /data/user/ stages via /data/local/tmp then run-as cp - push/ls CLI commands now accept bare absolute paths without bundle-id - Document apps path, apps fs ls/pull/push in README with examples
Both commands work on /sdcard and /data/user/ paths (via run-as). bundle-id is optional when passing an absolute path directly.
- All fs ops use native Go os package (simulator container is a local Mac dir) - validatePath ensures all paths stay within ~/Library/Developer/CoreSimulator/Devices/<UDID>/ - Default ls path is <device>/data; Android default remains / - Fix directory size to always be 0 (metadata block size is meaningless)
- Fix #1: use UUID for adb tmp filenames (was rand.Int(), collision-prone) - Fix #2: replace manual mutex unlock with defer-free pattern in ListFiles - Fix #3: extract androidPackageName() to eliminate 4 duplicate blocks - Fix #6: extract iosBrowseAllApps() to avoid double BrowseAllApps scan - Fix #8: promote 'apps fs' to top-level 'fs' command (filesystem ops are not app-specific); 'apps path' stays under apps - Fix #9: rename parseLsOutput/parseLsLine -> androidParseLs* - Fix #11: remove 'on app' clause from push success message - Fix #12: use %w consistently in commands/fs.go - Update README and root.go examples for new 'fs' command path
Path contains all routing info needed on every platform.
WalkthroughAdds a Filesystem capability and an 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
cli/root.go (1)
124-142: ⚡ Quick winAdd an
apps pathexample in root help for discoverability.Since app-container workflows are shown here, adding one
mobilecli apps path <bundle-id> --device <device-id>example would make the new path capability easier to discover from--help.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cli/root.go` around lines 124 - 142, Add a short example for the new apps path subcommand to the CLI root help text: locate the help/examples string that contains the FILESYSTEM block in root.go (the same block showing fs ls/pull/push/mkdir/rm) and insert a line demonstrating usage of the apps path command, e.g. "mobilecli apps path <bundle-id> --device <device-id>", so the apps path capability is discoverable from --help; update the help string near the FILESYSTEM examples (the root command help definition) to include this new example.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cli/apps.go`:
- Around line 181-184: Remove the stray blank line between the
appsCmd.AddCommand(appsPathCmd) call and the
appsLaunchCmd.Flags().StringVar(...) call to satisfy gofmt; open the function
containing appsCmd, locate the block where appsCmd.AddCommand(appsPathCmd) and
appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to
launch app on") appear and delete the extra empty line so the two statements are
adjacent.
In `@devices/ios_fs.go`:
- Around line 108-158: The ListFiles method in IOSDevice never sets
FileEntry.ModTime, causing inconsistency with Android/Simulator; update
IOSDevice.ListFiles to populate ModTime from the AFC file info returned by
client.Stat (and the single-file stat path) — when you call
client.Stat(fullPath) or client.Stat(remotePath) extract the modification time
(e.g., info.ModTime or the appropriate timestamp field on the returned info) and
assign it to FileEntry.ModTime for both the single-file branch and the directory
listing loop so ModTime is consistently populated.
In `@README.md`:
- Line 279: Update the example heading text that currently reads "Example output
for `apps fs ls`" to the correct command label "Example output for `fs ls`" so
the README example matches the actual command; locate and edit the heading
string in the README where the example output for the file-system listing is
shown and replace `apps fs ls` with `fs ls`.
- Around line 227-305: Update the wording in the "Access files on the device..."
section to remove the contradiction by changing the phrase "Pulling binary files
(images, databases, DEX files) is fully supported and binary-safe on all
platforms." to either "…on all supported platforms." or explicitly "…on Android
and iOS Simulator." Locate and edit the notes block (the paragraph starting with
"Pulling binary files...") in the README's file-system section so the platform
claim matches the earlier sentence that support is currently limited to Android
and iOS Simulator.
---
Nitpick comments:
In `@cli/root.go`:
- Around line 124-142: Add a short example for the new apps path subcommand to
the CLI root help text: locate the help/examples string that contains the
FILESYSTEM block in root.go (the same block showing fs ls/pull/push/mkdir/rm)
and insert a line demonstrating usage of the apps path command, e.g. "mobilecli
apps path <bundle-id> --device <device-id>", so the apps path capability is
discoverable from --help; update the help string near the FILESYSTEM examples
(the root command help definition) to include this new example.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 18213652-dcd5-4af8-8273-195639792576
📒 Files selected for processing (12)
README.mdcli/apps.gocli/fs.gocli/root.gocommands/apps.gocommands/fs.godevices/android.godevices/android_fs.godevices/common.godevices/ios_fs.godevices/remote_fs.godevices/simulator_fs.go
| appsCmd.AddCommand(appsPathCmd) | ||
|
|
||
|
|
||
| appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to launch app on") |
There was a problem hiding this comment.
Fix gofmt violation blocking CI.
Line 183 has an extra blank line in this block; CI is already failing on go fmt ./... for this area.
Proposed fix
appsCmd.AddCommand(appsForegroundCmd)
appsCmd.AddCommand(appsPathCmd)
-
appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to launch app on")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cli/apps.go` around lines 181 - 184, Remove the stray blank line between the
appsCmd.AddCommand(appsPathCmd) call and the
appsLaunchCmd.Flags().StringVar(...) call to satisfy gofmt; open the function
containing appsCmd, locate the block where appsCmd.AddCommand(appsPathCmd) and
appsLaunchCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device to
launch app on") appear and delete the extra empty line so the two statements are
adjacent.
| func (d *IOSDevice) ListFiles(bundleID, remotePath string) ([]FileEntry, error) { | ||
| if remotePath == "" { | ||
| remotePath = "/" | ||
| } | ||
|
|
||
| d.mu.Lock() | ||
| err := d.startTunnel() | ||
| d.mu.Unlock() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to start tunnel: %w", err) | ||
| } | ||
|
|
||
| client, remotePath, err := d.resolveAfcClientAndPath(bundleID, remotePath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("afc connect failed: %w", err) | ||
| } | ||
| defer client.Close() | ||
|
|
||
| names, err := client.List(remotePath) | ||
| if err != nil { | ||
| // path might be a single file — stat it directly | ||
| info, statErr := client.Stat(remotePath) | ||
| if statErr != nil { | ||
| return nil, fmt.Errorf("ls failed: %w", err) | ||
| } | ||
| size := int64(0) | ||
| if !info.IsDir() { | ||
| size = info.Size | ||
| } | ||
| return []FileEntry{{ | ||
| Name: path.Base(remotePath), | ||
| Path: remotePath, | ||
| Size: size, | ||
| IsDir: info.IsDir(), | ||
| }}, nil | ||
| } | ||
|
|
||
| entries := make([]FileEntry, 0, len(names)) | ||
| for _, name := range names { | ||
| fullPath := strings.TrimRight(remotePath, "/") + "/" + name | ||
| entry := FileEntry{Name: name, Path: fullPath} | ||
| if info, err := client.Stat(fullPath); err == nil { | ||
| entry.IsDir = info.IsDir() | ||
| if !entry.IsDir { | ||
| entry.Size = info.Size | ||
| } | ||
| } | ||
| entries = append(entries, entry) | ||
| } | ||
| return entries, nil | ||
| } |
There was a problem hiding this comment.
Inconsistent ModTime population across platforms.
The iOS ListFiles implementation never sets the ModTime field in FileEntry results (both for single files at lines 137-142 and directory listings at lines 145-156). This creates an inconsistency with Android (devices/android_fs.go:144) and Simulator (devices/simulator_fs.go:92), which both populate ModTime.
Consider populating ModTime from AFC file info if available, or documenting why iOS cannot provide modification times.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@devices/ios_fs.go` around lines 108 - 158, The ListFiles method in IOSDevice
never sets FileEntry.ModTime, causing inconsistency with Android/Simulator;
update IOSDevice.ListFiles to populate ModTime from the AFC file info returned
by client.Stat (and the single-file stat path) — when you call
client.Stat(fullPath) or client.Stat(remotePath) extract the modification time
(e.g., info.ModTime or the appropriate timestamp field on the returned info) and
assign it to FileEntry.ModTime for both the single-file branch and the directory
listing loop so ModTime is consistently populated.
| Access files on the device or inside an app's data container. Currently supported on **Android** and **iOS Simulator**. | ||
|
|
||
| ```bash | ||
| # Get the data container path of an app (Android) | ||
| mobilecli apps path <bundle-id> --device <device-id> | ||
|
|
||
| # List files at any absolute path (defaults to device root if omitted) | ||
| mobilecli fs ls --device <device-id> | ||
| mobilecli fs ls --device <device-id> /sdcard | ||
| mobilecli fs ls --device <device-id> /sdcard/Download | ||
|
|
||
| # List files inside an app's data container | ||
| mobilecli fs ls --device <device-id> com.example.app | ||
| mobilecli fs ls --device <device-id> com.example.app /Documents | ||
|
|
||
| # Pull a file from the device to local disk | ||
| mobilecli fs pull --device <device-id> /sdcard/recording.mp4 ./recording.mp4 | ||
|
|
||
| # Pull a file from an app's private container | ||
| mobilecli fs pull --device <device-id> /data/user/0/com.example.app/files/db.sqlite ./db.sqlite | ||
|
|
||
| # Push a file to the device | ||
| mobilecli fs push --device <device-id> ./config.json /sdcard/config.json | ||
|
|
||
| # Push a file into an app's private container | ||
| mobilecli fs push --device <device-id> ./config.json /data/user/0/com.example.app/files/config.json | ||
|
|
||
| # Create a directory | ||
| mobilecli fs mkdir --device <device-id> /sdcard/myfolder | ||
|
|
||
| # Create a directory and all parent directories | ||
| mobilecli fs mkdir --device <device-id> -p /sdcard/a/b/c | ||
| mobilecli fs mkdir --device <device-id> -p /data/user/0/com.example.app/files/cache/v2 | ||
|
|
||
| # Remove a file | ||
| mobilecli fs rm --device <device-id> /sdcard/old_file.txt | ||
|
|
||
| # Remove a directory recursively | ||
| mobilecli fs rm --device <device-id> -r /sdcard/myfolder | ||
| mobilecli fs rm --device <device-id> -r /data/user/0/com.example.app/files/cache | ||
| ``` | ||
|
|
||
| Example output for `apps path`: | ||
| ```json | ||
| { | ||
| "status": "ok", | ||
| "data": { | ||
| "path": "/data/user/0/com.example.app" | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Example output for `apps fs ls`: | ||
| ```json | ||
| { | ||
| "status": "ok", | ||
| "data": [ | ||
| { | ||
| "name": "files", | ||
| "path": "/data/user/0/com.example.app/files", | ||
| "size": 4096, | ||
| "modTime": "2026-05-11T19:20:00Z", | ||
| "isDir": true | ||
| }, | ||
| { | ||
| "name": "shared_prefs", | ||
| "path": "/data/user/0/com.example.app/shared_prefs", | ||
| "size": 4096, | ||
| "modTime": "2026-05-11T12:49:00Z", | ||
| "isDir": true | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| **Notes:** | ||
| - Paths under `/data/user/` are accessed via `run-as`, so the app must be debuggable. | ||
| - Pushing to `/data/user/` stages the file through `/data/local/tmp/` then copies it into the container. | ||
| - Pulling binary files (images, databases, DEX files) is fully supported and binary-safe on all platforms. |
There was a problem hiding this comment.
Clarify platform-support wording to avoid contradiction.
Line 227 limits support to Android/iOS Simulator, but Line 305 says binary-safe pull works on “all platforms.” Please align this phrasing (for example, “all supported platforms”) so expectations are clear.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 227 - 305, Update the wording in the "Access files on
the device..." section to remove the contradiction by changing the phrase
"Pulling binary files (images, databases, DEX files) is fully supported and
binary-safe on all platforms." to either "…on all supported platforms." or
explicitly "…on Android and iOS Simulator." Locate and edit the notes block (the
paragraph starting with "Pulling binary files...") in the README's file-system
section so the platform claim matches the earlier sentence that support is
currently limited to Android and iOS Simulator.
push/pull use base64 content in request/response, 1 MB limit. Temp files are used internally and cleaned up after each call.
Tests cover /sdcard/Download and app container (com.mobilenext.mobilewright_demo): mkdir -p, push, ls, pull (verify contents), rm -r
- Add runAsArgs() to eliminate duplicated /data/user/ dispatch logic across PushFile, PullFile, ListFiles, Mkdir, and Rm - Defer mutex unlock in ios_fs.go ListFiles via anonymous function - Move defer os.Remove before write in handleFsPush to eliminate duplicate manual cleanup call - Use 1 << 20 for fsSizeLimit constant (idiomatic bit-shift)
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@server/server.go`:
- Around line 1589-1595: The code currently reads the entire file with
os.ReadFile(tmpPath) before checking fsSizeLimit which can spike memory; change
the flow in the function handling the pulled file so you stat the file first
(use os.Stat on tmpPath), compare fi.Size() to fsSizeLimit and return an error
if too large, and only then read the file into memory; reference tmpPath,
fsSizeLimit and the existing os.ReadFile call so you replace the pre-check with
a size check using os.Stat (or open the file and use io.CopyN/LimitReader)
before loading.
- Around line 1615-1621: The code currently calls
base64.StdEncoding.DecodeString(p.Content) before checking size, so huge base64
input can allocate large memory; instead compute the decoded length using
base64.StdEncoding.DecodedLen(len(p.Content)) and if that decoded length exceeds
fsSizeLimit return the existing "file too large" error before calling
DecodeString; update the block around p.Content, fsSizeLimit and DecodeString to
perform this pre-check and only decode when the decoded size is within limits.
In `@test/emulator.ts`:
- Around line 264-303: mobilecliJson and the related mobilecli call are
constructing shell strings and are vulnerable to injection and breaking on
spaces; replace execSync string-based calls with execFileSync and pass the
mobilecli binary plus each argument as an argv array (e.g., split the command
into explicit args for subcommands like 'apps','path', packageName, '--device',
deviceId or 'fs','ls', remotePath, etc.), preserve options (encoding, timeout,
stdio, env) and keep JSON.parse(result) in mobilecliJson; update fsPush, fsPull,
fsMkdir, fsRm, getAppPath and fsList callers to build and pass proper argv
arrays to mobilecli/ mobilecliJson so all dynamic values (deviceId, remotePath,
packageName, localPath) are passed as arguments rather than interpolated into a
shell string.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 52a136ee-a514-46ae-b554-120c4e8b2167
📒 Files selected for processing (5)
devices/android_fs.godevices/ios_fs.goserver/dispatch.goserver/server.gotest/emulator.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- devices/ios_fs.go
- devices/android_fs.go
| data, err := os.ReadFile(tmpPath) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to read pulled file: %w", err) | ||
| } | ||
| if len(data) > fsSizeLimit { | ||
| return nil, fmt.Errorf("file too large (%d bytes); maximum allowed size for JSON-RPC transfer is 1 MB", len(data)) | ||
| } |
There was a problem hiding this comment.
Check pulled file size before loading it into memory.
Line 1589 reads the full file before enforcing fsSizeLimit; oversized pulls can spike memory before rejection.
Suggested fix
- data, err := os.ReadFile(tmpPath)
+ info, err := os.Stat(tmpPath)
+ if err != nil {
+ return nil, fmt.Errorf("failed to stat pulled file: %w", err)
+ }
+ if info.Size() > int64(fsSizeLimit) {
+ return nil, fmt.Errorf("file too large (%d bytes); maximum allowed size for JSON-RPC transfer is 1 MB", info.Size())
+ }
+
+ data, err := os.ReadFile(tmpPath)
if err != nil {
return nil, fmt.Errorf("failed to read pulled file: %w", err)
}
- if len(data) > fsSizeLimit {
- return nil, fmt.Errorf("file too large (%d bytes); maximum allowed size for JSON-RPC transfer is 1 MB", len(data))
- }📝 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.
| data, err := os.ReadFile(tmpPath) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to read pulled file: %w", err) | |
| } | |
| if len(data) > fsSizeLimit { | |
| return nil, fmt.Errorf("file too large (%d bytes); maximum allowed size for JSON-RPC transfer is 1 MB", len(data)) | |
| } | |
| info, err := os.Stat(tmpPath) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to stat pulled file: %w", err) | |
| } | |
| if info.Size() > int64(fsSizeLimit) { | |
| return nil, fmt.Errorf("file too large (%d bytes); maximum allowed size for JSON-RPC transfer is 1 MB", info.Size()) | |
| } | |
| data, err := os.ReadFile(tmpPath) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to read pulled file: %w", err) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/server.go` around lines 1589 - 1595, The code currently reads the
entire file with os.ReadFile(tmpPath) before checking fsSizeLimit which can
spike memory; change the flow in the function handling the pulled file so you
stat the file first (use os.Stat on tmpPath), compare fi.Size() to fsSizeLimit
and return an error if too large, and only then read the file into memory;
reference tmpPath, fsSizeLimit and the existing os.ReadFile call so you replace
the pre-check with a size check using os.Stat (or open the file and use
io.CopyN/LimitReader) before loading.
| data, err := base64.StdEncoding.DecodeString(p.Content) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("'content' is not valid base64: %w", err) | ||
| } | ||
| if len(data) > fsSizeLimit { | ||
| return nil, fmt.Errorf("file too large (%d bytes); maximum allowed size for JSON-RPC transfer is 1 MB", len(data)) | ||
| } |
There was a problem hiding this comment.
Reject oversized base64 payloads before decode.
Line 1615 decodes first and checks size second; a large content can allocate heavily before being rejected.
Suggested fix
+ maxEncoded := base64.StdEncoding.EncodedLen(fsSizeLimit)
+ if len(p.Content) > maxEncoded {
+ return nil, fmt.Errorf("file too large; maximum allowed size for JSON-RPC transfer is 1 MB")
+ }
+
data, err := base64.StdEncoding.DecodeString(p.Content)
if err != nil {
return nil, fmt.Errorf("'content' is not valid base64: %w", err)
}📝 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.
| data, err := base64.StdEncoding.DecodeString(p.Content) | |
| if err != nil { | |
| return nil, fmt.Errorf("'content' is not valid base64: %w", err) | |
| } | |
| if len(data) > fsSizeLimit { | |
| return nil, fmt.Errorf("file too large (%d bytes); maximum allowed size for JSON-RPC transfer is 1 MB", len(data)) | |
| } | |
| maxEncoded := base64.StdEncoding.EncodedLen(fsSizeLimit) | |
| if len(p.Content) > maxEncoded { | |
| return nil, fmt.Errorf("file too large; maximum allowed size for JSON-RPC transfer is 1 MB") | |
| } | |
| data, err := base64.StdEncoding.DecodeString(p.Content) | |
| if err != nil { | |
| return nil, fmt.Errorf("'content' is not valid base64: %w", err) | |
| } | |
| if len(data) > fsSizeLimit { | |
| return nil, fmt.Errorf("file too large (%d bytes); maximum allowed size for JSON-RPC transfer is 1 MB", len(data)) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@server/server.go` around lines 1615 - 1621, The code currently calls
base64.StdEncoding.DecodeString(p.Content) before checking size, so huge base64
input can allocate large memory; instead compute the decoded length using
base64.StdEncoding.DecodedLen(len(p.Content)) and if that decoded length exceeds
fsSizeLimit return the existing "file too large" error before calling
DecodeString; update the block around p.Content, fsSizeLimit and DecodeString to
perform this pre-check and only decode when the decoded size is within limits.
| function mobilecliJson(args: string): any { | ||
| const mobilecliBinary = path.join(__dirname, '..', 'mobilecli'); | ||
| const result = execSync(`${mobilecliBinary} ${args}`, { | ||
| encoding: 'utf8', | ||
| timeout: 60000, | ||
| stdio: ['pipe', 'pipe', 'pipe'], | ||
| env: { ANDROID_HOME: process.env.ANDROID_HOME || '' }, | ||
| }); | ||
| return JSON.parse(result); | ||
| } | ||
|
|
||
| function getAppPath(deviceId: string, packageName: string): string { | ||
| const response = mobilecliJson(`apps path ${packageName} --device ${deviceId}`); | ||
| expect(response.status).to.equal('ok'); | ||
| return response.data.path; | ||
| } | ||
|
|
||
| function fsList(deviceId: string, remotePath: string): any[] { | ||
| const response = mobilecliJson(`fs ls --device ${deviceId} "${remotePath}"`); | ||
| expect(response.status).to.equal('ok'); | ||
| return response.data; | ||
| } | ||
|
|
||
| function fsPush(deviceId: string, localPath: string, remotePath: string): void { | ||
| mobilecli(`fs push --device ${deviceId} "${localPath}" "${remotePath}"`); | ||
| } | ||
|
|
||
| function fsPull(deviceId: string, remotePath: string, localPath: string): void { | ||
| mobilecli(`fs pull --device ${deviceId} "${remotePath}" "${localPath}"`); | ||
| } | ||
|
|
||
| function fsMkdir(deviceId: string, remotePath: string, parents: boolean): void { | ||
| const flag = parents ? '-p ' : ''; | ||
| mobilecli(`fs mkdir --device ${deviceId} ${flag}"${remotePath}"`); | ||
| } | ||
|
|
||
| function fsRm(deviceId: string, remotePath: string, recursive: boolean): void { | ||
| const flag = recursive ? '-r ' : ''; | ||
| mobilecli(`fs rm --device ${deviceId} ${flag}"${remotePath}"`); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify remaining dynamic shell-command execSync usage in this file.
rg -nP --type=ts 'execSync\(\s*`|execSync\(\s*["'\''].*\$\{.*["'\'']\s*\)' test/emulator.tsRepository: mobile-next/mobilecli
Length of output: 126
🏁 Script executed:
# Find the mobilecli function definition (not mobilecliJson)
rg -n -A 10 'function mobilecli\(' test/emulator.tsRepository: mobile-next/mobilecli
Length of output: 445
🏁 Script executed:
# Check for all execSync calls in the file
rg -n 'execSync' test/emulator.tsRepository: mobile-next/mobilecli
Length of output: 272
🏁 Script executed:
# Get file length to understand if we can read the entire file
wc -l test/emulator.tsRepository: mobile-next/mobilecli
Length of output: 86
🏁 Script executed:
cat -n test/emulator.ts | head -250 | tail -100Repository: mobile-next/mobilecli
Length of output: 4220
🏁 Script executed:
cat -n test/emulator.ts | tail -70Repository: mobile-next/mobilecli
Length of output: 3001
Fix shell injection vulnerability in both mobilecli() and mobilecliJson() by using argv-based execution.
Both mobilecli() (line 211) and mobilecliJson() (line 264) build shell commands through string interpolation with dynamic values like deviceId, remotePath, packageName, and localPath. This enables command injection and breaks on paths with spaces or special characters. Switch both functions to execFileSync() with argument arrays.
🧰 Tools
🪛 OpenGrep (1.20.0)
[ERROR] 266-271: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/emulator.ts` around lines 264 - 303, mobilecliJson and the related
mobilecli call are constructing shell strings and are vulnerable to injection
and breaking on spaces; replace execSync string-based calls with execFileSync
and pass the mobilecli binary plus each argument as an argv array (e.g., split
the command into explicit args for subcommands like 'apps','path', packageName,
'--device', deviceId or 'fs','ls', remotePath, etc.), preserve options
(encoding, timeout, stdio, env) and keep JSON.parse(result) in mobilecliJson;
update fsPush, fsPull, fsMkdir, fsRm, getAppPath and fsList callers to build and
pass proper argv arrays to mobilecli/ mobilecliJson so all dynamic values
(deviceId, remotePath, packageName, localPath) are passed as arguments rather
than interpolated into a shell string.
- Use %w instead of %v in AppPathCommand to preserve error chain
- Rename targetDevice to device for consistency with other commands
- Avoid mutating runAsArgs slice in PullFile; build pullArgs explicitly
- Fix stale README example label ('apps fs ls' → 'fs ls')
- Remove empty if-branch with noisy comment in fsLsCmd arg parsing
- Rename getAppPath test helper to getAppContainerPath (returns container
path, not APK path; avoids confusion with GetAppPath)
There was a problem hiding this comment.
♻️ Duplicate comments (1)
test/emulator.ts (1)
264-303:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReplace shell-string execution with argv-based execution in CLI helpers.
mobilecliJsonand the FS/app helper callers still construct shell strings from dynamic values. This is injection-prone and also brittle for paths containing spaces/quotes.Suggested fix (execFileSync + argument arrays)
-import { execSync } from 'child_process'; +import { execFileSync, execSync } from 'child_process'; -function mobilecli(args: string): void { +function mobilecli(args: string[]): void { const mobilecliBinary = path.join(__dirname, '..', 'mobilecli'); - const command = `${mobilecliBinary} ${args}`; try { - const result = execSync(command, { + execFileSync(mobilecliBinary, args, { encoding: 'utf8', timeout: 180000, stdio: ['pipe', 'pipe', 'pipe'], env: { + ...process.env, ANDROID_HOME: process.env.ANDROID_HOME || "", } }); } catch (error: any) { - console.log(`Command failed: ${command}`); + console.log(`Command failed: ${mobilecliBinary} ${args.map((a) => JSON.stringify(a)).join(' ')}`); if (error.stderr) { console.log(`Error stderr: ${error.stderr}`); } @@ -function mobilecliJson(args: string): any { +function mobilecliJson(args: string[]): any { const mobilecliBinary = path.join(__dirname, '..', 'mobilecli'); - const result = execSync(`${mobilecliBinary} ${args}`, { + const result = execFileSync(mobilecliBinary, args, { encoding: 'utf8', timeout: 60000, stdio: ['pipe', 'pipe', 'pipe'], - env: { ANDROID_HOME: process.env.ANDROID_HOME || '' }, + env: { ...process.env, ANDROID_HOME: process.env.ANDROID_HOME || '' }, }); return JSON.parse(result); } function getAppContainerPath(deviceId: string, packageName: string): string { - const response = mobilecliJson(`apps path ${packageName} --device ${deviceId}`); + const response = mobilecliJson(['apps', 'path', packageName, '--device', deviceId]); expect(response.status).to.equal('ok'); return response.data.path; } function fsList(deviceId: string, remotePath: string): any[] { - const response = mobilecliJson(`fs ls --device ${deviceId} "${remotePath}"`); + const response = mobilecliJson(['fs', 'ls', '--device', deviceId, remotePath]); expect(response.status).to.equal('ok'); return response.data; } function fsPush(deviceId: string, localPath: string, remotePath: string): void { - mobilecli(`fs push --device ${deviceId} "${localPath}" "${remotePath}"`); + mobilecli(['fs', 'push', '--device', deviceId, localPath, remotePath]); } function fsPull(deviceId: string, remotePath: string, localPath: string): void { - mobilecli(`fs pull --device ${deviceId} "${remotePath}" "${localPath}"`); + mobilecli(['fs', 'pull', '--device', deviceId, remotePath, localPath]); } function fsMkdir(deviceId: string, remotePath: string, parents: boolean): void { - const flag = parents ? '-p ' : ''; - mobilecli(`fs mkdir --device ${deviceId} ${flag}"${remotePath}"`); + const args = ['fs', 'mkdir', '--device', deviceId]; + if (parents) args.push('-p'); + args.push(remotePath); + mobilecli(args); } function fsRm(deviceId: string, remotePath: string, recursive: boolean): void { - const flag = recursive ? '-r ' : ''; - mobilecli(`fs rm --device ${deviceId} ${flag}"${remotePath}"`); + const args = ['fs', 'rm', '--device', deviceId]; + if (recursive) args.push('-r'); + args.push(remotePath); + mobilecli(args); }#!/bin/bash # Verify remaining string-based shell execution patterns in this file. rg -nP --type=ts 'execSync\(\s*`|execSync\(\s*["'\''].*\$\{.*["'\'']\s*\)' test/emulator.ts rg -nP --type=ts 'mobilecli(Json)?\(\s*`' test/emulator.ts # Expected after fix: no matches for dynamic/interpolated shell commands.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/emulator.ts` around lines 264 - 303, mobilecliJson and the helper callers (mobilecli, getAppContainerPath, fsList, fsPush, fsPull, fsMkdir, fsRm) currently build shell-interpolated command strings which is injection-prone; change mobilecliJson to use execFileSync (or spawnSync) with an argv array (no shell=true) and update all callers to pass arguments as separate array entries (e.g., ['apps','path', packageName, '--device', deviceId] or ['fs','push','--device', deviceId, localPath, remotePath]) instead of string interpolation, ensuring paths with spaces/quotes are passed as single argv elements and retaining existing timeout/encoding/env options.
🧹 Nitpick comments (1)
test/emulator.ts (1)
105-111: ⚡ Quick winEnsure temp files are always cleaned up on failing test paths.
Cleanup currently runs only on success. If
fsPush,fsPull, or assertions fail, temp files remain and can accumulate across CI retries.Suggested fix (use try/finally)
const localFile = writeTempFile('hello from mobilecli'); -fsPush(deviceId, localFile, remoteFile); -fs.unlinkSync(localFile); +try { + fsPush(deviceId, localFile, remoteFile); +} finally { + if (fs.existsSync(localFile)) fs.unlinkSync(localFile); +} const localDest = path.join(os.tmpdir(), `mobilecli-pull-${Date.now()}.txt`); -fsPull(deviceId, remoteFile, localDest); -const contents = fs.readFileSync(localDest, 'utf8'); -expect(contents.trim()).to.equal('hello from mobilecli'); -fs.unlinkSync(localDest); +try { + fsPull(deviceId, remoteFile, localDest); + const contents = fs.readFileSync(localDest, 'utf8'); + expect(contents.trim()).to.equal('hello from mobilecli'); +} finally { + if (fs.existsSync(localDest)) fs.unlinkSync(localDest); +}Also applies to: 121-129, 172-178, 188-196
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/emulator.ts` around lines 105 - 111, The temp files created by writeTempFile (localFile) must be removed even if fsPush/fsPull or assertions throw; wrap the test body that uses localFile in a try/finally so fs.unlinkSync(localFile) runs in finally (guarded by fs.existsSync(localFile) if needed) and move fs.unlinkSync out of the success path; apply the same try/finally pattern to the other tests that create localFile and call fsPush/fsPull (the blocks around fsPush/fsPull and the tests referenced at 121-129, 172-178, 188-196) so temp files are always cleaned up on failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@test/emulator.ts`:
- Around line 264-303: mobilecliJson and the helper callers (mobilecli,
getAppContainerPath, fsList, fsPush, fsPull, fsMkdir, fsRm) currently build
shell-interpolated command strings which is injection-prone; change
mobilecliJson to use execFileSync (or spawnSync) with an argv array (no
shell=true) and update all callers to pass arguments as separate array entries
(e.g., ['apps','path', packageName, '--device', deviceId] or
['fs','push','--device', deviceId, localPath, remotePath]) instead of string
interpolation, ensuring paths with spaces/quotes are passed as single argv
elements and retaining existing timeout/encoding/env options.
---
Nitpick comments:
In `@test/emulator.ts`:
- Around line 105-111: The temp files created by writeTempFile (localFile) must
be removed even if fsPush/fsPull or assertions throw; wrap the test body that
uses localFile in a try/finally so fs.unlinkSync(localFile) runs in finally
(guarded by fs.existsSync(localFile) if needed) and move fs.unlinkSync out of
the success path; apply the same try/finally pattern to the other tests that
create localFile and call fsPush/fsPull (the blocks around fsPush/fsPull and the
tests referenced at 121-129, 172-178, 188-196) so temp files are always cleaned
up on failure.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6ca1cf66-30b7-42e8-bc7a-8c3e75af3dba
📒 Files selected for processing (5)
README.mdcli/fs.gocommands/apps.godevices/android_fs.gotest/emulator.ts
✅ Files skipped from review due to trivial changes (1)
- README.md
🚧 Files skipped from review as they are similar to previous changes (3)
- commands/apps.go
- cli/fs.go
- devices/android_fs.go
No description provided.