diff --git a/Package.resolved b/Package.resolved index 2179a8a..d0c1ba2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "f549ee4f482c73bec051b377de7b419a29337e3fb588d352ad1c7c6fad491b80", + "originHash" : "f3be3e2bc12b09669080e2780616cd0eb702c7a1e844b6aca5fe930314fc04f6", "pins" : [ { "identity" : "swift-argument-parser", @@ -15,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-subprocess", "state" : { - "revision" : "13d087685b95d64d6aac9b94500d347bbe84c39b", - "version" : "0.4.0" + "revision" : "11633673a41f509f8945f23c96c7acd4adafd679", + "version" : "0.5.0" } }, { diff --git a/Sources/ShellKit/Shell+Path.swift b/Sources/ShellKit/Shell+Path.swift index d9c1468..0278101 100644 --- a/Sources/ShellKit/Shell+Path.swift +++ b/Sources/ShellKit/Shell+Path.swift @@ -180,14 +180,13 @@ extension Shell { url = URL(fileURLWithPath: path) } else { #if os(Windows) - if path.count >= 2, - let second = path.dropFirst().first, second == ":" { - // Drive-letter absolutes live outside the unix-style - // virtual namespace; under a mapping they fall through - // to the unmapped sentinel below. - let driveURL = URL(fileURLWithPath: path) - guard sandbox?.pathMapping != nil else { return driveURL } - return Self.voidedUnmappedPath(path) + if path.hasPrefix("\\") || path.dropFirst().first == ":" { + // Drive-letter (`C:\x`) and backslash-rooted (`\x`, + // `\\server\share`) absolutes live outside the + // unix-style virtual namespace; under a mapping they + // fall through to the unmapped sentinel below. + guard sandbox?.pathMapping == nil else { return Self.voidedUnmappedPath(path) } + return URL(fileURLWithPath: onShellDrive(path) ?? path) } #endif let cwd = environment.workingDirectory @@ -227,6 +226,24 @@ extension Shell { return URL(fileURLWithPath: translated.host) } + #if os(Windows) + /// `path` on the shell's drive when a backslash roots it at the + /// current drive's root (`\x`, not a `\\server\share` UNC path): + /// Windows takes the current drive for it, and the shell's is its + /// working directory's, which needn't be the process's. `nil` for + /// any other path, or when the working directory names no drive. + private func onShellDrive(_ path: String) -> String? { + guard path.hasPrefix("\\") else { return nil } + // A second separator makes it a UNC path. + if let second = path.dropFirst().first, second == "\\" || second == "/" { return nil } + let cwd = environment.workingDirectory + guard let letter = cwd.first, letter.isASCII, letter.isLetter, + cwd.dropFirst().first == ":" + else { return nil } + return String(cwd.prefix(2)) + path + } + #endif + /// Resolve `path` against ``current``'s working directory. /// Equivalent to `Shell.current.resolve(path)`; provided for /// call-site convenience. diff --git a/Tests/ShellKitTests/ShellTests.swift b/Tests/ShellKitTests/ShellTests.swift index 6f58044..2d78561 100644 --- a/Tests/ShellKitTests/ShellTests.swift +++ b/Tests/ShellKitTests/ShellTests.swift @@ -89,6 +89,43 @@ import ShellCommandKit // Shell.register(_:) bridge #expect(url.path == "/tmp/some-where/file.txt") } + #if os(Windows) + @Test func resolveBackslashRootedPathsAsAbsolute() async throws { + var env = Environment() + // A drive the process is unlikely to be on. + env.workingDirectory = #"Q:\work"# + let shell = Shell(environment: env) + // Rooted by a backslash, as by a drive letter: not joined to the + // working directory. A UNC path or a drive keeps its own root… + for path in [#"\\server\share\repo.git"#, "//server/share/repo.git", #"C:\repo.git"#] { + #expect(shell.resolve(path) == URL(fileURLWithPath: path)) + } + // …and the current drive's root is the shell's, not the process's. + #expect(shell.resolve(#"\repo.git"#) == URL(fileURLWithPath: #"Q:\repo.git"#)) + #expect(shell.resolve(#"\"#) == URL(fileURLWithPath: #"Q:\"#)) + // Without a drive to take (a unix-style working directory), the + // process's. `Environment()` itself starts in the process's + // directory, drive included. + var driveless = Environment() + driveless.workingDirectory = "/virtual/cwd" + #expect(Shell(environment: driveless).resolve(#"\repo.git"#) + == URL(fileURLWithPath: #"\repo.git"#)) + // Outside the virtual namespace, so voided under a mapping. + shell.sandbox = .confined( + to: PathMapping(mounts: [.init(virtual: "/work", host: #"C:\work"#)]), + home: "/work") + #expect(shell.resolve(#"\\server\share\repo.git"#).path + .contains(Shell.unmappedPathSentinel)) + } + #else + @Test func resolveTakesABackslashAsAnOrdinaryCharacter() async throws { + var env = Environment() + env.workingDirectory = "/tmp/some-where" + let shell = Shell(environment: env) + #expect(shell.resolve(#"\file.txt"#).path == #"/tmp/some-where/\file.txt"#) + } + #endif + @Test func resolveStaticHonoursCurrent() async throws { var env = Environment() env.workingDirectory = "/var/scope"