fix: convert hex from packageManager integrity to base64 format (#2717)
The hash on the `packageManager` field in `package.json` is specified as
a hex string for the sha512 checksum. This representation is what
corepack
[expects](https://github.com/nodejs/corepack?tab=readme-ov-file#when-authoring-packages)
and what something like `pnpm install` can understand. However, Bazel
needs an integrity hash in SRI format with a base64 representation of
the checksum. The repository code will now convert from hex to base64 on
the fly. This makes the `pnpm_version_from` field actually useful with a
hash. Before it was only useful without it since the hash format was
mismatched between corepack and Bazel and thus caused errors on one side
or the other.
Cherry-Pick from #2709.
---
### Changes are visible to end-users: yes
- Searched for relevant documentation and updated as needed: yes
- Breaking change (forces users to change their own code or config): no
- Suggested release notes appear below: no
### Test plan
- New test cases added
diff --git a/e2e/update_pnpm_lock/.aspect/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU= b/e2e/update_pnpm_lock/.aspect/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU=
index 2c03619..5d60cd1 100755
--- a/e2e/update_pnpm_lock/.aspect/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU=
+++ b/e2e/update_pnpm_lock/.aspect/external_repository_action_cache/npm_translate_lock_LTE4Nzc1MDcwNjU=
@@ -2,7 +2,7 @@
# Input hashes for repository rule npm_translate_lock(name = "npm", pnpm_lock = "@@//:pnpm-lock.yaml").
# This file should be checked into version control along with the pnpm-lock.yaml file.
.npmrc=664934919
-package.json=-1406666059
+package.json=68550237
pnpm-lock.yaml=-1664994187
pnpm-workspace.yaml=-79388955
workspace_package/package.json=-643240351
diff --git a/e2e/update_pnpm_lock/MODULE.bazel b/e2e/update_pnpm_lock/MODULE.bazel
index 848beb7..601a15f 100644
--- a/e2e/update_pnpm_lock/MODULE.bazel
+++ b/e2e/update_pnpm_lock/MODULE.bazel
@@ -9,7 +9,7 @@
pnpm = use_extension("@aspect_rules_js//npm:extensions.bzl", "pnpm")
pnpm.pnpm(
name = "pnpm",
- pnpm_version = "9.15.9",
+ pnpm_version_from = "//:package.json",
)
use_repo(pnpm, "pnpm", "pnpm__links")
diff --git a/e2e/update_pnpm_lock/package.json b/e2e/update_pnpm_lock/package.json
index 521c678..448a6c4 100644
--- a/e2e/update_pnpm_lock/package.json
+++ b/e2e/update_pnpm_lock/package.json
@@ -1,5 +1,6 @@
{
"private": true,
+ "packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
"pnpm": {
"onlyBuiltDependencies": []
},
diff --git a/npm/private/pnpm_extension.bzl b/npm/private/pnpm_extension.bzl
index 6366029..b0277e7 100644
--- a/npm/private/pnpm_extension.bzl
+++ b/npm/private/pnpm_extension.bzl
@@ -2,6 +2,7 @@
load("@aspect_bazel_lib//lib:lists.bzl", "unique")
load(":pnpm_repository.bzl", "DEFAULT_PNPM_VERSION", "LATEST_PNPM_VERSION")
+load(":utils.bzl", "utils")
DEFAULT_PNPM_REPO_NAME = "pnpm"
@@ -62,8 +63,10 @@
parts = v.rsplit("+sha512.", 1)
v = parts[0]
- # Store the integrity hash (prepend "sha512-" as that's the expected format)
- integrity[v] = "sha512-" + parts[1]
+ # Store the integrity hash. We need to convert the hex representation of the
+ # hash used by corepack, to the one Bazel understands: base64 encoded with a
+ # "sha512-" prefix.
+ integrity[v] = "sha512-" + utils.hex_to_base64(parts[1])
elif attr.pnpm_version == "latest":
v = LATEST_PNPM_VERSION
diff --git a/npm/private/test/pnpm_test.bzl b/npm/private/test/pnpm_test.bzl
index c2e76ec..7d3cd29 100644
--- a/npm/private/test/pnpm_test.bzl
+++ b/npm/private/test/pnpm_test.bzl
@@ -60,11 +60,12 @@
)
def _from_package_json_with_hash(ctx):
- # Test reading pnpm version from package.json with integrity hash.
- # packageManager: "pnpm@1.2.3+sha512.xxx" -> (version, integrity) tuple
+ # Test reading pnpm version from package.json with integrity hash in the hexadecimal format
+ # that is standard for corepack. The integrity needs to be in SRI format (base64).
+ # packageManager: "pnpm@1.2.3+sha512.<base64>" -> (version, integrity) tuple
return _resolve_test(
ctx,
- repositories = {"pnpm": ("1.2.3", "sha512-97462997561378b6f52ac5c614f3a3b923a652ad5ac987100286e4aa2d84a6a0642e9e45f3d01d30c46b12b20beb0f86aeb790bf9a82bc59db42b67fe69d1a25")},
+ repositories = {"pnpm": ("1.2.3", "sha512-l0Ypl1YTeLb1KsXGFPOjuSOmUq1ayYcQAobkqi2EpqBkLp5F89AdMMRrErIL6w+GrreQv5qCvFnbQrZ/5p0aJQ==")},
modules = [
_fake_mod(True, _fake_pnpm_tag(pnpm_version_from = "//:package.json")),
],
diff --git a/npm/private/test/utils_tests.bzl b/npm/private/test/utils_tests.bzl
index 63ed354..113c125 100644
--- a/npm/private/test/utils_tests.bzl
+++ b/npm/private/test/utils_tests.bzl
@@ -126,6 +126,24 @@
)
return unittest.end(env)
+# buildifier: disable=function-docstring
+def test_hex_to_base64(ctx):
+ given_expected = {
+ "382877d089ed5e47e31d364e0dc88c163e8a8e5e8e6aeb6b537e9f77931394d89fb142f1d1d18d32536e3added79d98241048a700b1cfbce9d7167777fa8c502": "OCh30IntXkfjHTZODciMFj6Kjl6OautrU36fd5MTlNifsULx0dGNMlNuOt3tedmCQQSKcAsc+86dcWd3f6jFAg==",
+ "cf78dac1faa7faafffb89023bdf584c5cc4e219db349c376a7afc93f1dc00a08fa365732dae800646f8d99aeaa665ae85596d721c424efface8d25889f07c870": "z3jawfqn+q//uJAjvfWExcxOIZ2zScN2p6/JPx3ACgj6Nlcy2ugAZG+Nma6qZlroVZbXIcQk7/rOjSWInwfIcA==",
+ "bf28e5b00a825846c3b50e57ca6468d2a0f86ba9703be0193898d15ad5807203b7982408861e1f80275325dc6aa40bd06a7889c8b71166a072fc0e5fe0e5db29": "vyjlsAqCWEbDtQ5XymRo0qD4a6lwO+AZOJjRWtWAcgO3mCQIhh4fgCdTJdxqpAvQaniJyLcRZqBy/A5f4OXbKQ==",
+ "a3aeb971bcc746dd0c2c9b2050745833ee09c2c7d173f1d8e357b37239db2faf59deb5bab122754d874bd54a31c473c5af1b4096375de5501bc11e3f86e14392": "o665cbzHRt0MLJsgUHRYM+4JwsfRc/HY41ezcjnbL69Z3rW6sSJ1TYdL1UoxxHPFrxtAljdd5VAbwR4/huFDkg==",
+ "0f3707ebd2828c3e96e492629d6b3efcb4c03f71cb662fe4db432170a6fb622e14fb0647f5e1db307a282482ded220a2ccdfda92d6a652269e207e72c45ea5d6": "DzcH69KCjD6W5JJinWs+/LTAP3HLZi/k20MhcKb7Yi4U+wZH9eHbMHooJILe0iCizN/aktamUiaeIH5yxF6l1g==",
+ }
+ env = unittest.begin(ctx)
+ for given, expected in given_expected.items():
+ asserts.equals(
+ env,
+ expected,
+ utils.hex_to_base64(given),
+ )
+ return unittest.end(env)
+
t1_test = unittest.make(test_bazel_name)
t2_test = unittest.make(test_pnpm_name)
t3_test = unittest.make(test_friendly_name)
@@ -134,6 +152,7 @@
t7_test = unittest.make(test_npm_registry_download_url)
t8_test = unittest.make(test_npm_registry_url)
t9_test = unittest.make(test_link_version)
+t10_test = unittest.make(test_hex_to_base64)
def utils_tests(name):
unittest.suite(
@@ -146,4 +165,5 @@
t7_test,
t8_test,
t9_test,
+ t10_test,
)
diff --git a/npm/private/utils.bzl b/npm/private/utils.bzl
index 2b73f82..257ec2c 100644
--- a/npm/private/utils.bzl
+++ b/npm/private/utils.bzl
@@ -10,6 +10,9 @@
DEFAULT_REGISTRY_PROTOCOL = "https"
DEFAULT_EXTERNAL_REPOSITORY_ACTION_CACHE = ".aspect/rules/external_repository_action_cache"
+# Alphabet for base64 strings.
+_B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+
def _sorted_map(m):
# TODO(zbarsky): maybe faster as `dict(sorted(m.items()))`?
return {k: m[k] for k in sorted(m.keys())}
@@ -237,6 +240,64 @@
]
return ext in tarball_extensions
+def _hex_to_base64(hex_string):
+ """Converts a non-delimited hex string (like a SHA-512 checksum) to base64."""
+
+ # 1. Convert hex string to a list of integer bytes
+ bytes_list = []
+
+ # Loop with step 2 to grab hex pairs
+ for i in range(0, len(hex_string), 2):
+ bytes_list.append(int(hex_string[i:i + 2], 16))
+
+ output = []
+ length = len(bytes_list)
+
+ # 2. Process bytes in chunks of 3 using range(start, stop, step)
+ for i in range(0, length, 3):
+ b1 = bytes_list[i]
+
+ # Check if 2nd byte exists
+ if i + 1 < length:
+ b2 = bytes_list[i + 1]
+ else:
+ b2 = -1
+
+ # Check if 3rd byte exists
+ if i + 2 < length:
+ b3 = bytes_list[i + 2]
+ else:
+ b3 = -1
+
+ # Construct 24-bit buffer
+ # Use 0 for missing bytes during bitwise ops
+ val = (b1 << 16) | ((b2 if b2 != -1 else 0) << 8) | (b3 if b3 != -1 else 0)
+
+ # Extract 6-bit indices
+ c1 = (val >> 18) & 0x3F
+ c2 = (val >> 12) & 0x3F
+ c3 = (val >> 6) & 0x3F
+ c4 = val & 0x3F
+
+ output.append(_B64_CHARS[c1])
+ output.append(_B64_CHARS[c2])
+
+ # Handle Padding
+ if b2 == -1:
+ # Only 1 byte available -> Pad 2
+ output.append("=")
+ output.append("=")
+ elif b3 == -1:
+ # Only 2 bytes available -> Pad 1
+ output.append(_B64_CHARS[c3])
+ output.append("=")
+ else:
+ # All 3 bytes available -> No padding
+ output.append(_B64_CHARS[c3])
+ output.append(_B64_CHARS[c4])
+
+ return "".join(output)
+
utils = struct(
bazel_name = _bazel_name,
sorted_map = _sorted_map,
@@ -261,6 +322,7 @@
exists = _exists,
replace_npmrc_token_envvar = _replace_npmrc_token_envvar,
is_tarball_extension = _is_tarball_extension,
+ hex_to_base64 = _hex_to_base64,
)
# Exported only to be tested