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",