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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `screenshot` / `ios screenshot`: a simulator name containing path separators (simctl accepts any free text, e.g. `My iPhone/Work`) no longer turns the default output filename into a directory hierarchy — the name is collapsed into a single path component, matching `ios-device screenshot`. The Android default filename embeds the adb serial, whose accepted charset already excludes separators; it is now sanitised too as defence in depth. Video default filenames embed no user-controlled text and were already safe.
- `sim-use ios-device ui` no longer drops the navigation-bar back button (and any other element whose token the daemon aliases with the root). On a pushed screen `deviceFetchSpecialElement: 0` returns the back button as the root, so seeding the walk's visited set with the raw root token silently discarded it; the walk now dedups on `(token, summary, role)`, so the back button appears in the outline and is tappable with the ordinary `tap`.
- `sim-use ios-device` discovery bails in ~1 s when no iPhone is attached, instead of waiting the full 5 s timeout. An empty attachment set never satisfies the quiescence rule (it needs a non-empty, unchanged set), so the discovery loop used to run to the deadline on every device-less host; it now gives up after a short grace once nothing has appeared. A device that is present still settles in ~0.4 s, and a multi-device attach burst still coalesces (the grace only applies until the first device is seen).
- Top-level and `sim-use ios` verbs now reject a physical iOS device UDID at resolution time with a pointer to `sim-use ios-device`, instead of misclassifying it as an Android serial and diagnosing a plugged-in iPhone as "not reachable via adb".
Expand Down
5 changes: 4 additions & 1 deletion Sources/SimUse/Commands/Screenshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ struct Screenshot: SimUseExecutableCommand {

private func resolveAndroidOutputPath(serial: String) -> String {
let stamp = IOSSimScreenshotCommand.formatTimestamp(Date())
let defaultName = "Android Screenshot - \(serial) - \(stamp).png"
// The adb-serial charset the Android router accepts already excludes
// path separators; the sanitiser is defence in depth should those
// routing rules ever loosen.
let defaultName = "Android Screenshot - \(OutputFilePath.safeFilenameComponent(serial)) - \(stamp).png"
guard let provided = output?.trimmingCharacters(in: .whitespacesAndNewlines), !provided.isEmpty else {
return FileManager.default.currentDirectoryPath + "/" + defaultName
}
Expand Down
8 changes: 6 additions & 2 deletions Sources/iOSSimBackend/Verbs/IOSSimScreenshotCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,14 @@ public struct IOSSimScreenshotCommand: SimUseExecutableCommand {
/// file URL using iOS naming conventions. Public so tests can
/// pin the path expansion behaviour without spinning up an
/// FBSimulator. Path semantics live in `OutputFilePath`, shared
/// with the video verbs and the physical-device screenshot.
/// with the video verbs and the physical-device screenshot. The
/// simulator name is user-editable free text (simctl accepts any
/// name), so it is collapsed into a single safe path component —
/// "My iPhone/Work" must not turn the default output into a
/// directory hierarchy.
public static func prepareOutputURL(output: String?, simulatorName: String) throws -> URL {
let url = OutputFilePath.resolve(output: output) {
"Simulator Screenshot - \(simulatorName) - \(formatTimestamp(Date())).png"
"Simulator Screenshot - \(OutputFilePath.safeFilenameComponent(simulatorName)) - \(formatTimestamp(Date())).png"
}
try OutputFilePath.prepare(url)
return url
Expand Down
20 changes: 20 additions & 0 deletions Tests/ScreenshotForwarderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ struct ScreenshotForwarderTests {
#expect(url.pathExtension == "png")
}

@Test("A simulator name containing path separators stays a single filename component")
func slashedSimulatorNameStaysSingleComponent() throws {
let url = try IOSSimScreenshotCommand.prepareOutputURL(
output: nil,
simulatorName: "My iPhone/Work"
)
#expect(url.deletingLastPathComponent().path == FileManager.default.currentDirectoryPath)
#expect(url.lastPathComponent.hasPrefix("Simulator Screenshot - My iPhone-Work - "))
}

@Test("A traversal-shaped simulator name cannot escape the current directory")
func traversalSimulatorNameResolvesIntoCwd() throws {
let url = try IOSSimScreenshotCommand.prepareOutputURL(
output: nil,
simulatorName: "../../evil"
)
#expect(url.deletingLastPathComponent().path == FileManager.default.currentDirectoryPath)
#expect(!url.lastPathComponent.contains("/"))
}

@Test("Tilde-prefixed --output expands the home directory")
func tildeExpansion() throws {
let url = try IOSSimScreenshotCommand.prepareOutputURL(
Expand Down
Loading