From d2568415af1d3a57e0525f2272160c48cb482b74 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 8 Nov 2024 11:29:33 -0500 Subject: [PATCH 1/3] Handle hash characters (#) in netrc fields 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 Issue --- Sources/Basics/Netrc.swift | 21 +++++++--- Tests/BasicsTests/NetrcTests.swift | 62 +++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/Sources/Basics/Netrc.swift b/Sources/Basics/Netrc.swift index fdfe4db25e0..0d946d02854 100644 --- a/Sources/Basics/Netrc.swift +++ b/Sources/Basics/Netrc.swift @@ -132,8 +132,11 @@ public struct NetrcParser { let matches = regex.matches(in: text, range: range) var trimmedCommentsText = text matches.forEach { - trimmedCommentsText = trimmedCommentsText - .replacingOccurrences(of: nsString.substring(with: $0.range), with: "") + let matchedString = nsString.substring(with: $0.range) + if !matchedString.starts(with: "\"") { + trimmedCommentsText = trimmedCommentsText + .replacingOccurrences(of: matchedString, with: "") + } } return trimmedCommentsText } @@ -151,12 +154,18 @@ private enum RegexUtil { 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]) + if let quotedRange = Range(match.range(withName: prefix + rawValue + quotedIdentifier), in: string) { + return String(string[quotedRange]) + } else if let range = Range(match.range(withName: prefix + rawValue), in: string) { + return String(string[range]) + } else { + return nil + } } } - static let comments: String = "\\#[\\s\\S]*?.*$" + private static let quotedIdentifier = "quoted" + static let comments: String = "(\"[^\"]*\"|\\s#.*$)" static let `default`: String = #"(?:\s*(?default))"# static let accountOptional: String = #"(?:\s*account\s+\S++)?"# static let loginPassword: String = @@ -171,6 +180,6 @@ private enum RegexUtil { } static func namedTrailingCapture(_ string: String, prefix: String = "") -> String { - #"\s*\#(string)\s+(?<\#(prefix + string)>\S++)"# + #"\s*\#(string)\s+(?:"(?<\#(prefix + string + quotedIdentifier)>[^"]*)"|(?<\#(prefix + string)>\S+))"# } } diff --git a/Tests/BasicsTests/NetrcTests.swift b/Tests/BasicsTests/NetrcTests.swift index d9cde729ce2..6c9bce14859 100644 --- a/Tests/BasicsTests/NetrcTests.swift +++ b/Tests/BasicsTests/NetrcTests.swift @@ -431,5 +431,65 @@ class NetrcTests: XCTestCase { XCTAssertEqual(netrc.machines[1].login, "fred") XCTAssertEqual(netrc.machines[1].password, "sunshine4ever") } -} + func testComments() throws { + let content = """ + # A comment at the beginning of the line + machine example.com # Another comment + login anonymous + password qw#erty + """ + + let netrc = try NetrcParser.parse(content) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qw#erty") + } + + func testHashSymbolInPassword() throws { + let content = """ + machine example.com + login anonymous + password qw#erty + """ + + let netrc = try NetrcParser.parse(content) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qw#erty") + } + + func testQuotedPasswordWithSpace() throws { + let content = """ + machine example.com + login anonymous + password "qw erty" + """ + + let netrc = try NetrcParser.parse(content) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qw erty") + } + + func testQuotedPasswordWithSpaceAndHash() throws { + let content = """ + machine example.com + login anonymous + password "qw #erty" + """ + + let netrc = try NetrcParser.parse(content) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qw #erty") + } +} From a290fec9decc24f556717197700e479fbe07659c Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 8 Nov 2024 12:04:40 -0500 Subject: [PATCH 2/3] Add one more test --- Tests/BasicsTests/NetrcTests.swift | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Tests/BasicsTests/NetrcTests.swift b/Tests/BasicsTests/NetrcTests.swift index 6c9bce14859..a51064b645a 100644 --- a/Tests/BasicsTests/NetrcTests.swift +++ b/Tests/BasicsTests/NetrcTests.swift @@ -492,4 +492,19 @@ class NetrcTests: XCTestCase { XCTAssertEqual(machine?.login, "anonymous") XCTAssertEqual(machine?.password, "qw #erty") } + + func testQuotedPasswordWithSpaceAndHashAndCommentAtEndOfLine() throws { + let content = """ + machine example.com + login anonymous + password "qw #erty" # A comment + """ + + let netrc = try NetrcParser.parse(content) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, "anonymous") + XCTAssertEqual(machine?.password, "qw #erty") + } } From 0b756bbafb4d2d830e838d49ae1eb83250bf9e90 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Mon, 11 Nov 2024 08:46:21 -0500 Subject: [PATCH 3/3] Rework tests to cover more cases --- Tests/BasicsTests/NetrcTests.swift | 98 ++++++++++++++++-------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/Tests/BasicsTests/NetrcTests.swift b/Tests/BasicsTests/NetrcTests.swift index a51064b645a..72e69bcbd29 100644 --- a/Tests/BasicsTests/NetrcTests.swift +++ b/Tests/BasicsTests/NetrcTests.swift @@ -436,8 +436,8 @@ class NetrcTests: XCTestCase { let content = """ # A comment at the beginning of the line machine example.com # Another comment - login anonymous - password qw#erty + login anonymous # Another comment + password qw#erty # Another comment """ let netrc = try NetrcParser.parse(content) @@ -448,56 +448,60 @@ class NetrcTests: XCTestCase { XCTAssertEqual(machine?.password, "qw#erty") } - func testHashSymbolInPassword() throws { - let content = """ - machine example.com - login anonymous - password qw#erty - """ - - let netrc = try NetrcParser.parse(content) - - let machine = netrc.machines.first - XCTAssertEqual(machine?.name, "example.com") - XCTAssertEqual(machine?.login, "anonymous") - XCTAssertEqual(machine?.password, "qw#erty") - } - - func testQuotedPasswordWithSpace() throws { - let content = """ - machine example.com - login anonymous - password "qw erty" - """ - - let netrc = try NetrcParser.parse(content) - - let machine = netrc.machines.first - XCTAssertEqual(machine?.name, "example.com") - XCTAssertEqual(machine?.login, "anonymous") - XCTAssertEqual(machine?.password, "qw erty") + // TODO: These permutation tests would be excellent swift-testing parameterized tests. + func testAllHashQuotingPermutations() throws { + let cases = [ + ("qwerty", "qwerty"), + ("qwe#rty", "qwe#rty"), + ("\"qwe#rty\"", "qwe#rty"), + ("\"qwe #rty\"", "qwe #rty"), + ("\"qwe# rty\"", "qwe# rty"), + ] + + for (testCase, expected) in cases { + let content = """ + machine example.com + login \(testCase) + password \(testCase) + """ + let netrc = try NetrcParser.parse(content) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, expected, "Expected login \(testCase) to parse as \(expected)") + XCTAssertEqual(machine?.password, expected, "Expected \(testCase) to parse as \(expected)") + } } - func testQuotedPasswordWithSpaceAndHash() throws { - let content = """ - machine example.com - login anonymous - password "qw #erty" - """ - - let netrc = try NetrcParser.parse(content) - - let machine = netrc.machines.first - XCTAssertEqual(machine?.name, "example.com") - XCTAssertEqual(machine?.login, "anonymous") - XCTAssertEqual(machine?.password, "qw #erty") + func testAllCommentPermutations() throws { + let cases = [ + ("qwerty # a comment", "qwerty"), + ("qwe#rty # a comment", "qwe#rty"), + ("\"qwe#rty\" # a comment", "qwe#rty"), + ("\"qwe #rty\" # a comment", "qwe #rty"), + ("\"qwe# rty\" # a comment", "qwe# rty"), + ] + + for (testCase, expected) in cases { + let content = """ + machine example.com + login \(testCase) + password \(testCase) + """ + let netrc = try NetrcParser.parse(content) + + let machine = netrc.machines.first + XCTAssertEqual(machine?.name, "example.com") + XCTAssertEqual(machine?.login, expected, "Expected login \(testCase) to parse as \(expected)") + XCTAssertEqual(machine?.password, expected, "Expected password \(testCase) to parse as \(expected)") + } } - func testQuotedPasswordWithSpaceAndHashAndCommentAtEndOfLine() throws { + func testQuotedMachine() throws { let content = """ - machine example.com + machine "example.com" login anonymous - password "qw #erty" # A comment + password qwerty """ let netrc = try NetrcParser.parse(content) @@ -505,6 +509,6 @@ class NetrcTests: XCTestCase { let machine = netrc.machines.first XCTAssertEqual(machine?.name, "example.com") XCTAssertEqual(machine?.login, "anonymous") - XCTAssertEqual(machine?.password, "qw #erty") + XCTAssertEqual(machine?.password, "qwerty") } }