fix: Handle email-style usernames in Coursier cache path credential stripping (#1538)
When a repository uses an email address as the username (e.g.,
user@domain.com), Coursier encodes the cache path as
"user%40domain.com%40host". The existing credential stripping logic
uses find("%40") which locates the first %40 (the email's @), leaving
"domain.com%40host" in the path instead of just "host".
This change:
- Replaces find("%40") with rfind("%40") to locate the last %40,
which is always the user@host separator
- Extracts the stripping logic into a public
strip_credentials_from_cache_path() function for testability
- Adds unit tests covering: no credentials, simple username, and
email-style username cases
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>diff --git a/private/rules/coursier.bzl b/private/rules/coursier.bzl
index 16f29de..26c18cc 100644
--- a/private/rules/coursier.bzl
+++ b/private/rules/coursier.bzl
@@ -225,6 +225,22 @@
def _normalize_to_unix_path(path):
return path.replace("\\", "/")
+# Strip credential prefixes from a Coursier cache relative path.
+#
+# Coursier prefixes private repository paths with the URL-encoded username
+# (e.g., "user%40host" for "user@host"). This obfuscates changes to the
+# pinned json, so we remove the prefix.
+#
+# Uses rfind to locate the *last* %40, which is always the user@host separator.
+# This correctly handles email-style usernames (e.g., "user%40domain.com%40host")
+# where the email's @ also becomes %40 in the path.
+def strip_credentials_from_cache_path(relative_path):
+ credential_marker = relative_path.rfind("%40")
+ if credential_marker > -1:
+ user_prefix = relative_path[:credential_marker + 3].split("/")[-1]
+ relative_path = relative_path.replace(user_prefix, "")
+ return relative_path
+
# Relativize an absolute path to an artifact in coursier's default cache location.
# After relativizing, also symlink the path into the workspace's output base.
# Then return the relative path for further processing
@@ -238,14 +254,7 @@
if len(absolute_path_parts) != 2:
fail("Error while trying to parse the path of file in the coursier cache: " + absolute_path)
else:
- relative_path = absolute_path_parts[1]
-
- # Coursier prefixes private repositories with the username, which obfuscates
- # changes to the pinned json so we remove it from the relative path.
- credential_marker = relative_path.find("%40")
- if credential_marker > -1:
- user_prefix = relative_path[:credential_marker + 3].split("/")[-1]
- relative_path = relative_path.replace(user_prefix, "")
+ relative_path = strip_credentials_from_cache_path(absolute_path_parts[1])
# Make a symlink from the absolute path of the artifact to the relative
# path within the output_base/external.
diff --git a/tests/unit/coursier_test.bzl b/tests/unit/coursier_test.bzl
index 7ea0dfc..3cfecc3 100644
--- a/tests/unit/coursier_test.bzl
+++ b/tests/unit/coursier_test.bzl
@@ -7,6 +7,7 @@
"get_coursier_sha256",
"get_direct_dependencies",
"get_netrc_lines_from_entries",
+ "strip_credentials_from_cache_path",
infer = "infer_artifact_path_from_primary_and_repos",
)
load("//private/rules:v1_lock_file.bzl", "add_netrc_entries_from_mirror_urls")
@@ -681,6 +682,39 @@
get_direct_dependencies_test = add_test(_get_direct_dependencies_test_impl)
+def _strip_credentials_no_credentials_test_impl(ctx):
+ env = unittest.begin(ctx)
+ asserts.equals(
+ env,
+ "/https/c1/group/artifact/version/foo.jar",
+ strip_credentials_from_cache_path("/https/c1/group/artifact/version/foo.jar"),
+ )
+ return unittest.end(env)
+
+strip_credentials_no_credentials_test = add_test(_strip_credentials_no_credentials_test_impl)
+
+def _strip_credentials_simple_username_test_impl(ctx):
+ env = unittest.begin(ctx)
+ asserts.equals(
+ env,
+ "/https/c1/group/artifact/version/foo.jar",
+ strip_credentials_from_cache_path("/https/a%40c1/group/artifact/version/foo.jar"),
+ )
+ return unittest.end(env)
+
+strip_credentials_simple_username_test = add_test(_strip_credentials_simple_username_test_impl)
+
+def _strip_credentials_email_username_test_impl(ctx):
+ env = unittest.begin(ctx)
+ asserts.equals(
+ env,
+ "/https/c1/group/artifact/version/foo.jar",
+ strip_credentials_from_cache_path("/https/a%40b%40c1/group/artifact/version/foo.jar"),
+ )
+ return unittest.end(env)
+
+strip_credentials_email_username_test = add_test(_strip_credentials_email_username_test_impl)
+
def coursier_test_suite():
unittest.suite(
"coursier_tests",