File: Netrc.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (179 lines) | stat: -rw-r--r-- 8,207 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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import Foundation
import TSCBasic

/// Supplies `Authorization` header, typically to be appended to `URLRequest`

// deprecated 9/2021
@available(*, deprecated)
public protocol AuthorizationProviding {
    /// Optional `Authorization` header, likely added to `URLRequest`
    func authorization(for url: Foundation.URL) -> String?
}

// deprecated 9/2021
@available(*, deprecated)
extension AuthorizationProviding {
    public func authorization(for url: Foundation.URL) -> String? {
        return nil
    }
}

/*
 Netrc feature depends upon `NSTextCheckingResult.range(withName name: String) -> NSRange`,
 which is only available in macOS 10.13+, iOS 11+, etc at this time.
 */
/// Container of parsed netrc connection settings
// FIXME: deprecate 2/2022, remove once clients transitioned
@available(*, deprecated, message: "moved to SwiftPM")
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
public struct Netrc {
    /// Representation of `machine` connection settings & `default` connection settings.
    /// If `default` connection settings present, they will be last element.
    public let machines: [Machine]
    
    private init(machines: [Machine]) {
        self.machines = machines
    }
    
    /// Basic authorization header string
    /// - Parameter url: URI of network resource to be accessed
    /// - Returns: (optional) Basic Authorization header string to be added to the request
    public func authorization(for url: Foundation.URL) -> String? {
        guard let index = machines.firstIndex(where: { $0.name == url.host }) ?? machines.firstIndex(where: { $0.isDefault }) else { return nil }
        let machine = machines[index]
        let authString = "\(machine.login):\(machine.password)"
        guard let authData = authString.data(using: .utf8) else { return nil }
        return "Basic \(authData.base64EncodedString())"
    }
    
    /// Reads file at path or default location, and returns parsed Netrc representation
    /// - Parameter fileURL: Location of netrc file, defaults to `~/.netrc`
    /// - Returns: `Netrc` container with parsed connection settings, or error
    public static func load(fromFileAtPath filePath: AbsolutePath? = nil) throws -> Result<Netrc, Netrc.Error> {
        let filePath = try filePath ?? AbsolutePath(validating: "\(NSHomeDirectory())/.netrc")
        
        guard FileManager.default.fileExists(atPath: filePath.pathString) else { return .failure(.fileNotFound(filePath)) }
        guard FileManager.default.isReadableFile(atPath: filePath.pathString) else { return .failure(.unreadableFile(filePath)) }

        let fileContents = try String(contentsOf: filePath.asURL, encoding: .utf8)
        return Netrc.from(fileContents)
    }
    
    /// Regex matching logic for deriving `Netrc` container from string content
    /// - Parameter content: String text of netrc file
    /// - Returns: `Netrc` container with parsed connection settings, or error
    public static func from(_ content: String) -> Result<Netrc, Netrc.Error> {
        let content = trimComments(from: content)
        let regex = try! NSRegularExpression(pattern: RegexUtil.netrcPattern, options: [])
        let matches = regex.matches(in: content, options: [], range: NSRange(content.startIndex..<content.endIndex, in: content))
        
        let machines: [Machine] = matches.compactMap {
            return Machine(for: $0, string: content, variant: "lp") ??
            Machine(for: $0, string: content, variant: "pl")
        }
        
        if let defIndex = machines.firstIndex(where: { $0.isDefault }) {
            guard defIndex == machines.index(before: machines.endIndex) else { return .failure(.invalidDefaultMachinePosition) }
        }
        guard machines.count > 0 else { return .failure(.machineNotFound) }
        return .success(Netrc(machines: machines))
    }
    /// Utility method to trim comments from netrc content
    /// - Parameter text: String text of netrc file
    /// - Returns: String text of netrc file *sans* comments
    private static func trimComments(from text: String) -> String {
        let regex = try! NSRegularExpression(pattern: RegexUtil.comments, options: .anchorsMatchLines)
        let nsString = text as NSString
        let range = NSRange(location: 0, length: nsString.length)
        let matches = regex.matches(in: text, range: range)
        var trimmedCommentsText = text
        matches.forEach {
            trimmedCommentsText = trimmedCommentsText
                .replacingOccurrences(of: nsString.substring(with: $0.range), with: "")
        }
        return trimmedCommentsText
    }
}

// deprecated 9/2021
@available(*, deprecated)
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
extension Netrc: AuthorizationProviding {}

// FIXME: deprecate 2/2022, remove once clients transitioned
@available(*, deprecated, message: "moved to SwiftPM")
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
public extension Netrc {
    enum Error: Swift.Error {
        case invalidFilePath
        case fileNotFound(AbsolutePath)
        case unreadableFile(AbsolutePath)
        case machineNotFound
        case invalidDefaultMachinePosition
    }
    
    /// Representation of connection settings
    /// - important: Default connection settings are stored in machine named `default`
    struct Machine: Equatable {
        public let name: String
        public let login: String
        public let password: String
        
        public var isDefault: Bool {
            return name == "default"
        }
        
        public init(name: String, login: String, password: String) {
            self.name = name
            self.login = login
            self.password = password
        }
        
        init?(for match: NSTextCheckingResult, string: String, variant: String = "") {
            guard let name = RegexUtil.Token.machine.capture(in: match, string: string) ?? RegexUtil.Token.default.capture(in: match, string: string),
                  let login = RegexUtil.Token.login.capture(prefix: variant, in: match, string: string),
                  let password = RegexUtil.Token.password.capture(prefix: variant, in: match, string: string) else {
                      return nil
                  }
            self = Machine(name: name, login: login, password: password)
        }
    }
}

// FIXME: deprecate 2/2022, remove once clients transitioned
@available(*, deprecated, message: "moved to SwiftPM")
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
extension Netrc.Error: CustomNSError {
    public var errorUserInfo: [String : Any] {
        return [NSLocalizedDescriptionKey: "\(self)"]
    }
}

// FIXME: deprecate 2/2022, remove once clients transitioned
@available(*, deprecated, message: "moved to SwiftPM")
@available (macOS 10.13, iOS 11, tvOS 11, watchOS 4, *)
fileprivate enum RegexUtil {
    @frozen fileprivate enum Token: String, CaseIterable {
        case machine, login, password, account, macdef, `default`
        
        func capture(prefix: String = "", in match: NSTextCheckingResult, string: String) -> String? {
            guard let range = Range(match.range(withName: prefix + rawValue), in: string) else { return nil }
            return String(string[range])
        }
    }

    static let comments: String = "\\#[\\s\\S]*?.*$"
    static let `default`: String = #"(?:\s*(?<default>default))"#
    static let accountOptional: String = #"(?:\s*account\s+\S++)?"#
    static let loginPassword: String = #"\#(namedTrailingCapture("login", prefix: "lp"))\#(accountOptional)\#(namedTrailingCapture("password", prefix: "lp"))"#
    static let passwordLogin: String = #"\#(namedTrailingCapture("password", prefix: "pl"))\#(accountOptional)\#(namedTrailingCapture("login", prefix: "pl"))"#
    static let netrcPattern = #"(?:(?:(\#(namedTrailingCapture("machine"))|\#(namedMatch("default"))))(?:\#(loginPassword)|\#(passwordLogin)))"#
    
    static func namedMatch(_ string: String) -> String {
        return #"(?:\s*(?<\#(string)>\#(string)))"#
    }
    
    static func namedTrailingCapture(_ string: String, prefix: String = "") -> String {
        return #"\s*\#(string)\s+(?<\#(prefix + string)>\S++)"#
    }
}