mirror of
https://github.com/apple/pkl.git
synced 2026-09-13 13:21:47 +02:00
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:
+159
@@ -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\\
|
||||
"""))
|
||||
}
|
||||
}
|
||||
+211
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user