1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
public enum ResponseFiles: Sendable {
public static func responseFileContents(args: [String]) -> String {
UNIXShellCommandCodec(encodingStrategy: .singleQuotes, encodingBehavior: .argumentsOnly).encode(args)
}
// Adapted from SwiftDriver's response file support.
public static func expandResponseFiles(_ args: [String], fileSystem: any FSProxy, relativeTo basePath: Path) throws -> [String] {
var visited: Set<Path> = []
return try expandResponseFiles(args, fileSystem: fileSystem, relativeTo: basePath, visitedResponseFiles: &visited)
}
private static func expandResponseFiles(_ args: [String], fileSystem: any FSProxy, relativeTo basePath: Path, visitedResponseFiles: inout Set<Path>) throws -> [String] {
var result: [String] = []
for arg in args {
if arg.first == "@" {
let responseFile = basePath.join(arg.dropFirst())
// Guard against infinite parsing loop.
guard visitedResponseFiles.insert(responseFile.normalize()).inserted else {
throw StubError.error("Attempted to recursively expand '\(responseFile.str)'")
}
defer {
visitedResponseFiles.remove(responseFile)
}
let contents = try fileSystem.read(responseFile).asString
let lines = tokenizeResponseFile(contents)
result.append(contentsOf: try expandResponseFiles(lines, fileSystem: fileSystem, relativeTo: basePath, visitedResponseFiles: &visitedResponseFiles))
} else {
result.append(arg)
}
}
return result
}
private static func tokenizeResponseFile(_ content: String) -> [String] {
return content.split { $0 == "\n" || $0 == "\r\n" }
.flatMap { tokenizeResponseFileLine($0) }
}
private enum TokenState {
case normal, escaping, quoted
}
/// Tokenizes a response file line generated by `UNIXShellCommandCodec` using the `.singleQuotes` strategy.
private static func tokenizeResponseFileLine<S: StringProtocol>(_ line: S) -> [String] {
// Support double dash comments only if they start at the beginning of a line.
if line.hasPrefix("//") { return [] }
var tokens: [String] = []
var token: String = ""
// Conservatively assume ~1 token per line.
token.reserveCapacity(line.count)
var state: TokenState = .normal
for char in line {
if char == #"\"# && state == .normal {
// Backslashes only escape outside of quoted text.
state = .escaping
continue
}
if state == .escaping {
state = .normal
token.append(char)
continue
}
if char == "'" {
// We specify `.singleQuotes` as the quoting strategy for `UNIXShellCommandCodec`. All other special
// characters are escaped by quoting.
if state == .quoted {
state = .normal
} else {
state = .quoted
}
continue
}
if char.isWhitespace && state == .normal {
// This is unquoted, unescaped whitespace, start a new token.
if !token.isEmpty {
tokens.append(token)
token = ""
}
continue
}
token.append(char)
}
// Add the final token
if !token.isEmpty {
tokens.append(token)
}
return tokens
}
}
|