Add netrc helpers in EvaluatorSettings.pkl (#1817)

This adds the `netRcHeaders` helper method that parses netrc file contents, and returns a mapping suitable to be used as Http headers.
This commit is contained in:
Yarden Bar
2026-09-09 14:25:06 -07:00
committed by GitHub
parent 713adac824
commit c95b3d11f4
9 changed files with 1007 additions and 0 deletions
@@ -98,6 +98,7 @@ public record PklSettings(Editor editor, PklEvaluatorSettings.@Nullable Http htt
.addModuleKeyFactory(ModuleKeyFactories.standardLibrary)
.addModuleKeyFactory(ModuleKeyFactories.file)
.addResourceReader(ResourceReaders.environmentVariable())
.addResourceReader(ResourceReaders.file())
.addEnvironmentVariables(System.getenv())
.build()) {
var module = evaluator.evaluateOutputValueAs(moduleSource, PClassInfo.Settings);
@@ -0,0 +1,82 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.stdlib.evaluatorsettings;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.nodes.IndirectCallNode;
import java.util.List;
import java.util.Map;
import org.pkl.core.runtime.Identifier;
import org.pkl.core.runtime.VmList;
import org.pkl.core.runtime.VmMapping;
import org.pkl.core.runtime.VmObjectBuilder;
import org.pkl.core.runtime.VmTyped;
import org.pkl.core.runtime.VmUtils;
import org.pkl.core.stdlib.ExternalMethod1Node;
import org.pkl.core.util.Netrc;
public class HttpNodes {
private HttpNodes() {}
public abstract static class netRcHeaders extends ExternalMethod1Node {
@Specialization
@TruffleBoundary
protected VmMapping eval(VmTyped self, String text) {
return doParse(text);
}
@Specialization
@TruffleBoundary
protected VmMapping eval(
VmTyped self, VmTyped resource, @Cached("create()") IndirectCallNode callNode) {
var text = (String) VmUtils.readMember(resource, Identifier.TEXT, callNode);
return doParse(text);
}
private VmMapping doParse(String text) {
List<Netrc.Entry> entries;
try {
entries = Netrc.parse(text);
} catch (Netrc.NetrcParseException e) {
throw exceptionBuilder().evalError("cannotParseNetrc", e.getMessage()).build();
}
var headersMap = Netrc.toHeadersMap(entries);
return toMapping(headersMap);
}
private static VmMapping toMapping(Map<String, Map<String, List<String>>> headersMap) {
var outerBuilder = new VmObjectBuilder(headersMap.size());
for (var entry : headersMap.entrySet()) {
var globPattern = entry.getKey();
var innerMap = entry.getValue();
var innerBuilder = new VmObjectBuilder(innerMap.size());
for (var headerEntry : innerMap.entrySet()) {
var headerName = headerEntry.getKey();
var headerValuesList = headerEntry.getValue();
innerBuilder.addEntry(headerName, VmList.create(headerValuesList).toListing());
}
outerBuilder.addEntry(globPattern, innerBuilder.toMapping());
}
return outerBuilder.toMapping();
}
}
}
@@ -0,0 +1,278 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.util;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.jspecify.annotations.Nullable;
public final class Netrc {
private static final String NULL_LINE = "\0null_line\0";
public static final class NetrcParseException extends Exception {
public NetrcParseException(String message) {
super(message);
}
}
private Netrc() {}
public record Entry(
@Nullable String machine,
boolean isDefault,
@Nullable String login,
@Nullable String password,
@Nullable String account) {}
/**
* Parses the content of a .netrc file into a list of {@link Entry}.
*
* @throws NetrcParseException if the content contains an unclosed quote or invalid escape.
*/
public static List<Entry> parse(String content) throws NetrcParseException {
var tokens = tokenize(content);
var entries = new ArrayList<Entry>();
String currentMachine = null;
var isDefault = false;
String currentLogin = null;
String currentPassword = null;
String currentAccount = null;
var i = 0;
while (i < tokens.size()) {
var token = tokens.get(i++);
switch (token.toLowerCase(Locale.ROOT)) {
case "machine" -> {
if (currentMachine != null) {
entries.add(
new Entry(
currentMachine, isDefault, currentLogin, currentPassword, currentAccount));
}
isDefault = false;
currentLogin = null;
currentPassword = null;
currentAccount = null;
currentMachine = i < tokens.size() ? tokens.get(i++) : null;
}
case "default" -> {
if (currentMachine != null) {
entries.add(
new Entry(
currentMachine, isDefault, currentLogin, currentPassword, currentAccount));
}
currentLogin = null;
currentPassword = null;
currentAccount = null;
currentMachine = "default";
isDefault = true;
}
case "login" -> {
if (i < tokens.size()) {
currentLogin = tokens.get(i++);
}
}
case "password" -> {
if (i < tokens.size()) {
currentPassword = tokens.get(i++);
}
}
case "account" -> {
if (i < tokens.size()) {
currentAccount = tokens.get(i++);
}
}
case "macdef" -> {
while (i < tokens.size()) {
if (tokens.get(i).equals(NULL_LINE)) {
i++;
break;
}
i++;
}
}
}
}
if (currentMachine != null || isDefault) {
entries.add(
new Entry(currentMachine, isDefault, currentLogin, currentPassword, currentAccount));
}
return entries;
}
/**
* Converts a list of {@link Entry} into a map of host glob pattern to header map (header name to
* list of header values).
*/
public static Map<String, Map<String, List<String>>> toHeadersMap(List<Entry> entries) {
var result = new LinkedHashMap<String, Map<String, List<String>>>();
for (var entry : entries) {
var authHeaderValue = computeAuthHeaderValue(entry.login(), entry.password());
if (authHeaderValue == null) {
continue;
}
var headerMap = Map.of("Authorization", List.of(authHeaderValue));
if (entry.isDefault()) {
continue;
}
if (entry.machine() != null && !entry.machine().contains("/")) {
result.putIfAbsent("http{,s}://" + escapeGlobPattern(entry.machine()) + "/**", headerMap);
}
}
return result;
}
private static String escapeGlobPattern(String value) {
var sb = new StringBuilder();
for (var i = 0; i < value.length(); i++) {
var c = value.charAt(i);
if (c == '?' || c == '*' || c == '[' || c == '{' || c == '\\') {
sb.append('[').append(c).append(']');
} else {
sb.append(c);
}
}
return sb.toString();
}
private static @Nullable String computeAuthHeaderValue(
@Nullable String login, @Nullable String password) {
if (password == null && login == null) {
return null;
}
var user = login == null ? "" : login;
var pass = password == null ? "" : password;
var credentials = user + ":" + pass;
var encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
return "Basic " + encoded;
}
public static List<String> tokenize(String content) throws NetrcParseException {
var tokens = new ArrayList<String>();
var len = content.length();
var i = 0;
var atLineStart = true;
while (i < len) {
var c = content.charAt(i);
if (c == '\n' || (c == '\r' && i < len - 1 && content.charAt(i + 1) == '\n')) {
if (atLineStart) {
tokens.add(NULL_LINE);
}
atLineStart = true;
if (c == '\r') {
i += 2;
} else {
i++;
}
continue;
} else if (isWhitespace(c)) {
i++;
continue;
}
// lines starting with `#` (after any leading blanks) are treated as comments
if (c == '#' && atLineStart) {
// Skip comment until end of line
i = consumeLineComment(i, content, len);
continue;
}
// Non-whitespace character encountered
atLineStart = false;
if (c == '"') {
i = consumeQuotedToken(i, content, len, tokens);
} else {
i = consumeUnquotedToken(i, content, len, tokens);
}
}
return tokens;
}
private static int consumeLineComment(int i, String content, int len) {
while (i < len && content.charAt(i) != '\n') {
i++;
}
return i;
}
private static int consumeQuotedToken(int i, String content, int len, List<String> tokens)
throws NetrcParseException {
i++; // skip opening quote
var escape = false;
var sb = new StringBuilder();
while (i < len) {
var ch = content.charAt(i);
if (escape) {
var escapedChar =
switch (ch) {
case 'n' -> '\n';
case 't' -> '\t';
case 'r' -> '\r';
default -> ch;
};
sb.append(escapedChar);
escape = false;
i++;
continue;
}
switch (ch) {
case '"':
{
tokens.add(sb.toString());
return i + 1;
}
case '\\':
{
escape = true;
break;
}
default:
{
sb.append(ch);
}
}
i++;
}
var reason = escape ? "invalid escape" : "unclosed quote";
throw new NetrcParseException(reason);
}
private static int consumeUnquotedToken(int i, String content, int len, List<String> tokens) {
var sb = new StringBuilder();
while (i < len) {
var ch = content.charAt(i);
if (isWhitespace(ch)) {
tokens.add(sb.toString());
return i;
}
sb.append(ch);
i++;
}
tokens.add(sb.toString());
return i;
}
private static boolean isWhitespace(char c) {
return c == ' ' || c == '\n' || c == '\t' || c == '\r';
}
}
@@ -1236,3 +1236,6 @@ Cannot reference `module` type within a type alias body.
invalidThisTypeInTypeAlias=\
Cannot reference `this` type within a type alias body.
cannotParseNetrc=\
Cannot parse .netrc: {0}.
@@ -0,0 +1,159 @@
amends "../snippetTest.pkl"
import "pkl:EvaluatorSettings"
local const http = new EvaluatorSettings.Http {}
examples {
["netRcHeaders - basic"] {
http.netRcHeaders("""
machine github.com
login octocat
password secret_pass
""")
}
["netRcHeaders - quotes"] {
http.netRcHeaders("""
machine foo.com
login "bar bar"
password baz
""")
}
["netRcHeaders - trailing comments"] {
http.netRcHeaders("""
machine foo.com
login bar
password foo # some comment
machine bar.com
login bar2
password foo#some comment
""")
}
["netRcHeaders - hash inside quote"] {
http.netRcHeaders("""
machine foo.com
login "foo # bar"
password "pass#1"
""")
}
["netRcHeaders - tabs as whitespace"] {
http.netRcHeaders("machine\tfoo.com\tlogin\tbar\tpassword\tbaz")
}
["netRcHeaders - crlf line endings"] {
http.netRcHeaders("machine crlf.example.com\r\n login foo\r\n password bar\r\n")
}
["netRcHeaders - entry with login but no password"] {
http.netRcHeaders("""
machine nologin.com
password onlypass
machine nopass.com
login onlylogin
machine valid.com
login user
password pass
""")
}
["netRcHeaders - duplicate machines"] {
http.netRcHeaders("""
machine foo.com login firstUser password firstPass
machine foo.com login secondUser password secondPass
""")
}
["netRcHeaders - machine that collides with keyword"] {
http.netRcHeaders("""
machine machine login user password pass
""")
}
["netRcHeaders - machine default and default keyword"] {
http.netRcHeaders("""
default login defUser password defPass
machine default login machDefUser password machDefPass
""")
}
["netRcHeaders - machine with slash omitted"] {
http.netRcHeaders("""
machine foo.com/bar login user password pass
machine valid.com login user password pass
""")
}
["netRcHeaders - machine with glob characters escaped"] {
http.netRcHeaders("""
machine foo[bar].com login user password pass
machine star*.com login user password pass
""")
}
["netRcHeaders - files with only comments or whitespace"] {
http.netRcHeaders("")
http.netRcHeaders("# only comments\n# second comment")
http.netRcHeaders(" \t \n \n ")
}
["netRcHeaders - skipped macdef"] {
http.netRcHeaders("""
macdef init
echo hello
echo world
machine foo.com login user1 password pass1
""")
}
["netRcHeaders - macdef as value"] {
http.netRcHeaders("""
machine foo.com login macdef password pass1
""")
}
["netRcHeaders - empty quotes"] {
http.netRcHeaders("""
machine foo.com login "" password ""
""")
}
["netRcHeaders - escaped quotes and chars"] {
http.netRcHeaders(#"""
machine foo.com login "foo\"bar" password "foo \z"
"""#)
}
["netRcHeaders - consecutive quotes"] {
http.netRcHeaders("""
machine foo.com login "foo""bar" password pass
""")
}
["netRcHeaders - quoted keywords"] {
http.netRcHeaders("""
"machine" foo.com "login" user "password" pass
""")
}
["netRcHeaders - single line entry"] {
http.netRcHeaders("machine foo.com login bar password pass1")
}
["netRcHeaders - trailing machine at EOF"] {
http.netRcHeaders("machine foo.com login bar password pass1 machine")
}
["netRcHeaders - unterminated quote error"] {
module.catch(() -> http.netRcHeaders("""
machine foo.com login "foo
"""))
module.catch(() -> http.netRcHeaders("""
machine foo.com login "foo\\
"""))
}
}
@@ -0,0 +1,211 @@
examples {
["netRcHeaders - basic"] {
new {
["http{,s}://github.com/**"] {
["Authorization"] {
"Basic b2N0b2NhdDpzZWNyZXRfcGFzcw=="
}
}
}
}
["netRcHeaders - quotes"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic YmFyIGJhcjpiYXo="
}
}
}
}
["netRcHeaders - trailing comments"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic YmFyOmZvbw=="
}
}
["http{,s}://bar.com/**"] {
["Authorization"] {
"Basic YmFyMjpmb28jc29tZQ=="
}
}
}
}
["netRcHeaders - hash inside quote"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic Zm9vICMgYmFyOnBhc3MjMQ=="
}
}
}
}
["netRcHeaders - tabs as whitespace"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic YmFyOmJheg=="
}
}
}
}
["netRcHeaders - crlf line endings"] {
new {
["http{,s}://crlf.example.com/**"] {
["Authorization"] {
"Basic Zm9vOmJhcg=="
}
}
}
}
["netRcHeaders - entry with login but no password"] {
new {
["http{,s}://nologin.com/**"] {
["Authorization"] {
"Basic Om9ubHlwYXNz"
}
}
["http{,s}://nopass.com/**"] {
["Authorization"] {
"Basic b25seWxvZ2luOg=="
}
}
["http{,s}://valid.com/**"] {
["Authorization"] {
"Basic dXNlcjpwYXNz"
}
}
}
}
["netRcHeaders - duplicate machines"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic Zmlyc3RVc2VyOmZpcnN0UGFzcw=="
}
}
}
}
["netRcHeaders - machine that collides with keyword"] {
new {
["http{,s}://machine/**"] {
["Authorization"] {
"Basic dXNlcjpwYXNz"
}
}
}
}
["netRcHeaders - machine default and default keyword"] {
new {
["http{,s}://default/**"] {
["Authorization"] {
"Basic bWFjaERlZlVzZXI6bWFjaERlZlBhc3M="
}
}
}
}
["netRcHeaders - machine with slash omitted"] {
new {
["http{,s}://valid.com/**"] {
["Authorization"] {
"Basic dXNlcjpwYXNz"
}
}
}
}
["netRcHeaders - machine with glob characters escaped"] {
new {
["http{,s}://foo[[]bar].com/**"] {
["Authorization"] {
"Basic dXNlcjpwYXNz"
}
}
["http{,s}://star[*].com/**"] {
["Authorization"] {
"Basic dXNlcjpwYXNz"
}
}
}
}
["netRcHeaders - files with only comments or whitespace"] {
new {}
new {}
new {}
}
["netRcHeaders - skipped macdef"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic dXNlcjE6cGFzczE="
}
}
}
}
["netRcHeaders - macdef as value"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic bWFjZGVmOnBhc3Mx"
}
}
}
}
["netRcHeaders - empty quotes"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic Og=="
}
}
}
}
["netRcHeaders - escaped quotes and chars"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic Zm9vImJhcjpmb28geg=="
}
}
}
}
["netRcHeaders - consecutive quotes"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic Zm9vOnBhc3M="
}
}
}
}
["netRcHeaders - quoted keywords"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic dXNlcjpwYXNz"
}
}
}
}
["netRcHeaders - single line entry"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic YmFyOnBhc3Mx"
}
}
}
}
["netRcHeaders - trailing machine at EOF"] {
new {
["http{,s}://foo.com/**"] {
["Authorization"] {
"Basic YmFyOnBhc3Mx"
}
}
}
}
["netRcHeaders - unterminated quote error"] {
"Cannot parse .netrc: unclosed quote."
"Cannot parse .netrc: invalid escape."
}
}
@@ -17,6 +17,7 @@ package org.pkl.core.settings
import java.net.URI
import java.nio.file.Path
import java.util.Base64
import kotlin.io.path.createParentDirectories
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
@@ -175,6 +176,60 @@ class PklSettingsTest {
)
}
@Test
fun `load user settings with netRcHeaders`(@TempDir tempDir: Path) {
val netrcFile = tempDir.resolve(".netrc")
netrcFile.writeString(
"""
machine github.com
login octocat
password secret_pass
"""
.trimIndent()
)
val settingsPath = tempDir.resolve("settings.pkl")
settingsPath.writeString(
"""
amends "pkl:settings"
http {
headers = netRcHeaders(read("${netrcFile.toUri()}"))
}
"""
.trimIndent()
)
val settings = PklSettings.load(ModuleSource.path(settingsPath))
val expectedAuth =
"Basic " + Base64.getEncoder().encodeToString("octocat:secret_pass".toByteArray())
val expectedHttp =
PklEvaluatorSettings.Http(
null,
null,
mapOf("http{,s}://github.com/**" to mapOf("Authorization" to listOf(expectedAuth))),
)
assertThat(settings.http()).isEqualTo(expectedHttp)
}
@Test
fun `test import EvaluatorSettings`() {
val evaluator = Evaluator.preconfigured()
val module =
evaluator.evaluate(
ModuleSource.text(
"""
import "pkl:EvaluatorSettings"
res = (new EvaluatorSettings.Http {}).netRcHeaders("")
"""
.trimIndent()
)
)
assertThat(module.getProperty("res")).isNotNull
}
private fun checkEquals(expected: Editor, actual: PObject) {
assertThat(actual.getProperty("urlScheme") as String).isEqualTo(expected.urlScheme())
}
@@ -0,0 +1,207 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.util
import java.util.Base64
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.Test
class NetrcTest {
@Test
fun `parse netrc file content`() {
val content =
"""
# Comment line
machine github.com
login octocat
password secret_token_123
machine my-artifactory.internal.net
login user
password my_token
default
login defaultuser
password defaultpass
"""
.trimIndent()
val entries = Netrc.parse(content)
assertThat(entries)
.containsExactly(
Netrc.Entry("github.com", false, "octocat", "secret_token_123", null),
Netrc.Entry("my-artifactory.internal.net", false, "user", "my_token", null),
Netrc.Entry("default", true, "defaultuser", "defaultpass", null),
)
val headersMap = Netrc.toHeadersMap(entries)
val basicGithub =
"Basic " + Base64.getEncoder().encodeToString("octocat:secret_token_123".toByteArray())
val basicArtifactory =
"Basic " + Base64.getEncoder().encodeToString("user:my_token".toByteArray())
assertThat(headersMap["http{,s}://github.com/**"])
.isEqualTo(mapOf("Authorization" to listOf(basicGithub)))
assertThat(headersMap["http{,s}://my-artifactory.internal.net/**"])
.isEqualTo(mapOf("Authorization" to listOf(basicArtifactory)))
assertThat(headersMap["**"]).isNull()
}
@Test
fun `parse quotes and comments`() {
val content =
"""
# Header comment
machine example.com login "user with spaces" password "pass#with#hash" # trailing comment
machine "quoted.machine.com" login "quoteduser" password "token_with_quotes"
"""
.trimIndent()
val entries = Netrc.parse(content)
assertThat(entries)
.containsExactly(
Netrc.Entry("example.com", false, "user with spaces", "pass#with#hash", null),
Netrc.Entry("quoted.machine.com", false, "quoteduser", "token_with_quotes", null),
)
val headersMap = Netrc.toHeadersMap(entries)
val expectedBasic =
"Basic " + Base64.getEncoder().encodeToString("user with spaces:pass#with#hash".toByteArray())
val expectedQuoted =
"Basic " + Base64.getEncoder().encodeToString("quoteduser:token_with_quotes".toByteArray())
assertThat(headersMap["http{,s}://example.com/**"])
.isEqualTo(mapOf("Authorization" to listOf(expectedBasic)))
assertThat(headersMap["http{,s}://quoted.machine.com/**"])
.isEqualTo(mapOf("Authorization" to listOf(expectedQuoted)))
}
@Test
fun `escape sequences in quoted strings`() {
val content =
"""
machine example.com login "user\nwith\tcontrol\rchars" password "pass\"with\\escapes"
"""
.trimIndent()
val entries = Netrc.parse(content)
assertThat(entries)
.containsExactly(
Netrc.Entry("example.com", false, "user\nwith\tcontrol\rchars", "pass\"with\\escapes", null)
)
}
@Test
fun `case-insensitive keywords`() {
val content =
"""
MACHINE example.com LOGIN myUser PASSWORD myPass ACCOUNT myAccount
DEFAULT LOGIN defUser PASSWORD defPass
"""
.trimIndent()
val entries = Netrc.parse(content)
assertThat(entries)
.containsExactly(
Netrc.Entry("example.com", false, "myUser", "myPass", "myAccount"),
Netrc.Entry("default", true, "defUser", "defPass", null),
)
}
@Test
fun `discard unexpected tokens`() {
val content =
"""
machine example.com login foo bar baz password qux extra1 extra2
"""
.trimIndent()
val entries = Netrc.parse(content)
assertThat(entries).containsExactly(Netrc.Entry("example.com", false, "foo", "qux", null))
}
@Test
fun `skip macdef sections`() {
val content =
"""
macdef init
echo hello
echo world
machine foo.com login user1 password pass1
"""
.trimIndent()
val entries = Netrc.parse(content)
assertThat(entries).containsExactly(Netrc.Entry("foo.com", false, "user1", "pass1", null))
}
@Test
fun `skip macdef sections with crlf`() {
val content =
"macdef init\r\n echo hello\r\n echo world\r\n\r\nmachine foo.com login user1 password pass1\r\n"
val entries = Netrc.parse(content)
assertThat(entries).containsExactly(Netrc.Entry("foo.com", false, "user1", "pass1", null))
}
@Test
fun `ignore duplicate machine entries after first in headers map`() {
val content =
"""
machine foo.com login firstUser password firstPass
machine foo.com login secondUser password secondPass
"""
.trimIndent()
val entries = Netrc.parse(content)
val headersMap = Netrc.toHeadersMap(entries)
val expectedBasic =
"Basic " + Base64.getEncoder().encodeToString("firstUser:firstPass".toByteArray())
assertThat(headersMap["http{,s}://foo.com/**"])
.isEqualTo(mapOf("Authorization" to listOf(expectedBasic)))
}
@Test
fun `handle empty or comment-only content`() {
assertThat(Netrc.parse("")).isEmpty()
assertThat(Netrc.parse("# only comments\n# second line")).isEmpty()
assertThat(Netrc.toHeadersMap(emptyList())).isEmpty()
}
@Test
fun `discard default entry in headers map`() {
val content =
"""
default login defUser password defPass
machine foo.com login fooUser password fooPass
"""
.trimIndent()
val entries = Netrc.parse(content)
val headersMap = Netrc.toHeadersMap(entries)
assertThat(headersMap).doesNotContainKey("**")
assertThat(headersMap).containsKey("http{,s}://foo.com/**")
}
@Test
fun `handle unclosed quotes and invalid escapes`() {
assertThatThrownBy { Netrc.parse("machine foo.com login \"unclosed") }
.isInstanceOf(Netrc.NetrcParseException::class.java)
assertThatThrownBy { Netrc.parse("machine foo.com login \"invalid\\") }
.isInstanceOf(Netrc.NetrcParseException::class.java)
}
}