File: ResponseFiles.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (112 lines) | stat: -rw-r--r-- 4,244 bytes parent folder | download
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
    }
}