From 6c3bd2b2e1a05a1ad5fc0f6fb306427c25db8ff8 Mon Sep 17 00:00:00 2001 From: Artem Chikin Date: Fri, 5 May 2023 15:42:25 -0700 Subject: [PATCH] [Dependency Scanning] Unify path-escaping handling using TSC's spm_shellEscaped Command lines generated for libSwiftScan (either for actual dependency scanning queries, or for target-info queries) do not go through the shell, and are instead tokenized at the entry-points (inside libSwiftScan). In order for them to be correclty tokenized, we must ensure that all the various filepaths are escaped in case they contain whitespace characters, to ensure they get treated as a whole path, rather than multiple strings. The driver previously relied on a separate mechanism to do this for libSwiftScan command lines, by getting the 'ArgsResolver' to always escape '.path' arguments. This turned out to be insufficient, because it happens to miss a whole class of paths specified on the command-line that the driver cannot/doesn't recognize as paths: arguments forwarded to tools, such as '-Xcc' or '-Xfrontend' or '-Xclang-linker'. As a result, it is possible for such paths to end-up unescaped and fail to get tokenized correctly by libSwiftScan. Instead, this change switches the formulation of these command-lines to use the existing 'spm_shellEscaped' mechanism from TSC which is a robust way to achieve exactly the desired behavior: produce shell-escaped command-line arguments by detecting flags/arguments that contain characters that would prevent correct tokenization: whitespaces, etc, and only quote-escaping them, and doing so in a platform-appropriate manner (e.g. using '"' instead of "'" on Windows) Resolves rdar://108971395 --- .../SwiftDriver/Execution/ArgsResolver.swift | 42 +++++++++---------- .../ModuleDependencyScanning.swift | 11 +++-- .../ExplicitModuleBuildTests.swift | 22 ++++++++++ 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/Sources/SwiftDriver/Execution/ArgsResolver.swift b/Sources/SwiftDriver/Execution/ArgsResolver.swift index cd85fe75d..c2c6baea3 100644 --- a/Sources/SwiftDriver/Execution/ArgsResolver.swift +++ b/Sources/SwiftDriver/Execution/ArgsResolver.swift @@ -57,17 +57,16 @@ public final class ArgsResolver { } } - public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic, - quotePaths: Bool = false) throws -> [String] { - let (arguments, _) = try resolveArgumentList(for: job, useResponseFiles: useResponseFiles, - quotePaths: quotePaths) + public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic) + throws -> [String] { + let (arguments, _) = try resolveArgumentList(for: job, useResponseFiles: useResponseFiles) return arguments } - public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic, - quotePaths: Bool = false) throws -> ([String], usingResponseFile: Bool) { - let tool = try resolve(.path(job.tool), quotePaths: quotePaths) - var arguments = [tool] + (try job.commandLine.map { try resolve($0, quotePaths: quotePaths) }) + public func resolveArgumentList(for job: Job, useResponseFiles: ResponseFileHandling = .heuristic) + throws -> ([String], usingResponseFile: Bool) { + let tool = try resolve(.path(job.tool)) + var arguments = [tool] + (try job.commandLine.map { try resolve($0) }) let usingResponseFile = try createResponseFileIfNeeded(for: job, resolvedArguments: &arguments, useResponseFiles: useResponseFiles) return (arguments, usingResponseFile) @@ -77,46 +76,45 @@ public final class ArgsResolver { public func resolveArgumentList(for job: Job, forceResponseFiles: Bool, quotePaths: Bool = false) throws -> [String] { let useResponseFiles: ResponseFileHandling = forceResponseFiles ? .forced : .heuristic - return try resolveArgumentList(for: job, useResponseFiles: useResponseFiles, quotePaths: quotePaths) + return try resolveArgumentList(for: job, useResponseFiles: useResponseFiles) } @available(*, deprecated, message: "use resolveArgumentList(for:,useResponseFiles:,quotePaths:)") public func resolveArgumentList(for job: Job, forceResponseFiles: Bool, quotePaths: Bool = false) throws -> ([String], usingResponseFile: Bool) { let useResponseFiles: ResponseFileHandling = forceResponseFiles ? .forced : .heuristic - return try resolveArgumentList(for: job, useResponseFiles: useResponseFiles, quotePaths: quotePaths) + return try resolveArgumentList(for: job, useResponseFiles: useResponseFiles) } /// Resolve the given argument. - public func resolve(_ arg: Job.ArgTemplate, - quotePaths: Bool = false) throws -> String { + public func resolve(_ arg: Job.ArgTemplate) throws -> String { switch arg { case .flag(let flag): return flag case .path(let path): return try lock.withLock { - return try unsafeResolve(path: path, quotePaths: quotePaths) + return try unsafeResolve(path: path) } case .responseFilePath(let path): - return "@\(try resolve(.path(path), quotePaths: quotePaths))" + return "@\(try resolve(.path(path)))" case let .joinedOptionAndPath(option, path): - return option + (try resolve(.path(path), quotePaths: quotePaths)) + return option + (try resolve(.path(path))) case let .squashedArgumentList(option: option, args: args): return try option + args.map { - try resolve($0, quotePaths: quotePaths) + try resolve($0) }.joined(separator: " ") } } /// Needs to be done inside of `lock`. Marked unsafe to make that more obvious. - private func unsafeResolve(path: VirtualPath, quotePaths: Bool) throws -> String { + private func unsafeResolve(path: VirtualPath) throws -> String { // If there was a path mapping, use it. if let actualPath = pathMapping[path] { - return quotePaths ? "'\(actualPath)'" : actualPath + return actualPath } // Return the path from the temporary directory if this is a temporary file. @@ -140,13 +138,13 @@ public final class ArgsResolver { let result = actualPath.name pathMapping[path] = result - return quotePaths ? "'\(result)'" : result + return result } // Otherwise, return the path. let result = path.name pathMapping[path] = result - return quotePaths ? "'\(result)'" : result + return result } private func createFileList(path: VirtualPath, contents: [VirtualPath]) throws { @@ -154,7 +152,7 @@ public final class ArgsResolver { if let absPath = path.absolutePath { try fileSystem.writeFileContents(absPath) { out in for path in contents { - try! out <<< unsafeResolve(path: path, quotePaths: false) <<< "\n" + try! out <<< unsafeResolve(path: path) <<< "\n" } } } @@ -184,7 +182,7 @@ public final class ArgsResolver { } private func quoteAndEscape(path: VirtualPath) -> String { - let inputNode = Node.scalar(Node.Scalar(try! unsafeResolve(path: path, quotePaths: false), + let inputNode = Node.scalar(Node.Scalar(try! unsafeResolve(path: path), Tag(.str), .doubleQuoted)) // Width parameter of -1 sets preferred line-width to unlimited so that no extraneous // line-breaks will be inserted during serialization. diff --git a/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift b/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift index 7edbd2909..066205f3a 100644 --- a/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift +++ b/Sources/SwiftDriver/ExplicitModuleBuilds/ModuleDependencyScanning.swift @@ -415,12 +415,11 @@ public extension Driver { static func itemizedJobCommand(of job: Job, useResponseFiles: ResponseFileHandling, using resolver: ArgsResolver) throws -> [String] { - // FIXME: this is to walkaround rdar://108769167 - let quotePaths = job.kind != .scanDependencies - let (args, _) = try resolver.resolveArgumentList(for: job, - useResponseFiles: useResponseFiles, - quotePaths: quotePaths) - return args + // Because the command-line passed to libSwiftScan does not go through the shell + // we must ensure that we generate a shell-escaped string for all arguments/flags that may + // potentially need it. + return try resolver.resolveArgumentList(for: job, + useResponseFiles: useResponseFiles).0.map { $0.spm_shellEscaped() } } static func getRootPath(of toolchain: Toolchain, env: [String: String]) diff --git a/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift b/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift index 05d9858bc..f506e360b 100644 --- a/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift +++ b/Tests/SwiftDriverTests/ExplicitModuleBuildTests.swift @@ -1558,6 +1558,28 @@ final class ExplicitModuleBuildTests: XCTestCase { } } + func testDependencyScanCommandLineEscape() throws { +#if os(Windows) + let quoteCharacter: Character = "\"" +#else + let quoteCharacter: Character = "'" +#endif + let swiftInputWithSpace = "/tmp/input example/test.swift" + let swiftInputWithoutSpace = "/tmp/baz.swift" + let clangInputWithSpace = "/tmp/input example/bar.o" + var driver = try Driver(args: ["swiftc", "-explicit-module-build", + "-module-name", "testDependencyScanning", + swiftInputWithSpace, swiftInputWithoutSpace, + "-Xcc", clangInputWithSpace]) + let scanJob = try driver.dependencyScanningJob() + let scanJobCommand = try Driver.itemizedJobCommand(of: scanJob, + useResponseFiles: .disabled, + using: ArgsResolver(fileSystem: InMemoryFileSystem())) + XCTAssertTrue(scanJobCommand.contains(String(quoteCharacter) + swiftInputWithSpace + String(quoteCharacter))) + XCTAssertTrue(scanJobCommand.contains(String(quoteCharacter) + clangInputWithSpace + String(quoteCharacter))) + XCTAssertTrue(scanJobCommand.contains(swiftInputWithoutSpace)) + } + func testExplicitSwiftModuleMap() throws { let jsonExample : String = """ [