Skip to content

Commit bdd4cfc

Browse files
authored
Handle hash characters (#) in netrc fields (#8109)
Hash symbols were always treated as the beginning of a comment even if they were in the middle of a username or password. To address this require some whitespace before a hash character before a comment is parsed. That patch also implements support for quoted fields so that passwords can contain the sequence "foo #bar" without dropping "bar" as a comment. Issue: #8090
1 parent 42aaad6 commit bdd4cfc

File tree

2 files changed

+95
-7
lines changed

2 files changed

+95
-7
lines changed

Sources/Basics/Netrc.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,11 @@ public struct NetrcParser {
132132
let matches = regex.matches(in: text, range: range)
133133
var trimmedCommentsText = text
134134
matches.forEach {
135-
trimmedCommentsText = trimmedCommentsText
136-
.replacingOccurrences(of: nsString.substring(with: $0.range), with: "")
135+
let matchedString = nsString.substring(with: $0.range)
136+
if !matchedString.starts(with: "\"") {
137+
trimmedCommentsText = trimmedCommentsText
138+
.replacingOccurrences(of: matchedString, with: "")
139+
}
137140
}
138141
return trimmedCommentsText
139142
}
@@ -151,12 +154,18 @@ private enum RegexUtil {
151154
case machine, login, password, account, macdef, `default`
152155

153156
func capture(prefix: String = "", in match: NSTextCheckingResult, string: String) -> String? {
154-
guard let range = Range(match.range(withName: prefix + rawValue), in: string) else { return nil }
155-
return String(string[range])
157+
if let quotedRange = Range(match.range(withName: prefix + rawValue + quotedIdentifier), in: string) {
158+
return String(string[quotedRange])
159+
} else if let range = Range(match.range(withName: prefix + rawValue), in: string) {
160+
return String(string[range])
161+
} else {
162+
return nil
163+
}
156164
}
157165
}
158166

159-
static let comments: String = "\\#[\\s\\S]*?.*$"
167+
private static let quotedIdentifier = "quoted"
168+
static let comments: String = "(\"[^\"]*\"|\\s#.*$)"
160169
static let `default`: String = #"(?:\s*(?<default>default))"#
161170
static let accountOptional: String = #"(?:\s*account\s+\S++)?"#
162171
static let loginPassword: String =
@@ -171,6 +180,6 @@ private enum RegexUtil {
171180
}
172181

173182
static func namedTrailingCapture(_ string: String, prefix: String = "") -> String {
174-
#"\s*\#(string)\s+(?<\#(prefix + string)>\S++)"#
183+
#"\s*\#(string)\s+(?:"(?<\#(prefix + string + quotedIdentifier)>[^"]*)"|(?<\#(prefix + string)>\S+))"#
175184
}
176185
}

Tests/BasicsTests/NetrcTests.swift

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,5 +431,84 @@ class NetrcTests: XCTestCase {
431431
XCTAssertEqual(netrc.machines[1].login, "fred")
432432
XCTAssertEqual(netrc.machines[1].password, "sunshine4ever")
433433
}
434-
}
435434

435+
func testComments() throws {
436+
let content = """
437+
# A comment at the beginning of the line
438+
machine example.com # Another comment
439+
login anonymous # Another comment
440+
password qw#erty # Another comment
441+
"""
442+
443+
let netrc = try NetrcParser.parse(content)
444+
445+
let machine = netrc.machines.first
446+
XCTAssertEqual(machine?.name, "example.com")
447+
XCTAssertEqual(machine?.login, "anonymous")
448+
XCTAssertEqual(machine?.password, "qw#erty")
449+
}
450+
451+
// TODO: These permutation tests would be excellent swift-testing parameterized tests.
452+
func testAllHashQuotingPermutations() throws {
453+
let cases = [
454+
("qwerty", "qwerty"),
455+
("qwe#rty", "qwe#rty"),
456+
("\"qwe#rty\"", "qwe#rty"),
457+
("\"qwe #rty\"", "qwe #rty"),
458+
("\"qwe# rty\"", "qwe# rty"),
459+
]
460+
461+
for (testCase, expected) in cases {
462+
let content = """
463+
machine example.com
464+
login \(testCase)
465+
password \(testCase)
466+
"""
467+
let netrc = try NetrcParser.parse(content)
468+
469+
let machine = netrc.machines.first
470+
XCTAssertEqual(machine?.name, "example.com")
471+
XCTAssertEqual(machine?.login, expected, "Expected login \(testCase) to parse as \(expected)")
472+
XCTAssertEqual(machine?.password, expected, "Expected \(testCase) to parse as \(expected)")
473+
}
474+
}
475+
476+
func testAllCommentPermutations() throws {
477+
let cases = [
478+
("qwerty # a comment", "qwerty"),
479+
("qwe#rty # a comment", "qwe#rty"),
480+
("\"qwe#rty\" # a comment", "qwe#rty"),
481+
("\"qwe #rty\" # a comment", "qwe #rty"),
482+
("\"qwe# rty\" # a comment", "qwe# rty"),
483+
]
484+
485+
for (testCase, expected) in cases {
486+
let content = """
487+
machine example.com
488+
login \(testCase)
489+
password \(testCase)
490+
"""
491+
let netrc = try NetrcParser.parse(content)
492+
493+
let machine = netrc.machines.first
494+
XCTAssertEqual(machine?.name, "example.com")
495+
XCTAssertEqual(machine?.login, expected, "Expected login \(testCase) to parse as \(expected)")
496+
XCTAssertEqual(machine?.password, expected, "Expected password \(testCase) to parse as \(expected)")
497+
}
498+
}
499+
500+
func testQuotedMachine() throws {
501+
let content = """
502+
machine "example.com"
503+
login anonymous
504+
password qwerty
505+
"""
506+
507+
let netrc = try NetrcParser.parse(content)
508+
509+
let machine = netrc.machines.first
510+
XCTAssertEqual(machine?.name, "example.com")
511+
XCTAssertEqual(machine?.login, "anonymous")
512+
XCTAssertEqual(machine?.password, "qwerty")
513+
}
514+
}

0 commit comments

Comments
 (0)