| # Copyright 2026 The Bazel 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 |
| # |
| # http://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. |
| |
| """Helpers for working with distribution hash digests. |
| |
| Both the requirement `--hash=<algo>:<digest>` option (PEP 508 tooling) and the |
| Simple API `#<algo>=<digest>` URL fragments (PEP 503) or `hashes` dicts |
| (PEP 691) may use any algorithm from `hashlib.algorithms_guaranteed`, even |
| though `sha256` is the most common one. |
| |
| Internally a single digest is always represented as the canonical |
| `<algo>:<hex digest>` string (the same shape as the pip `--hash` values and |
| the uv.lock `hash` values) and only converted to the Subresource Integrity |
| format at the `ctx.download(integrity = ...)` boundary. |
| """ |
| |
| # The names from `hashlib.algorithms_guaranteed`, which PEP 691 uses as the |
| # set of valid hash names and PEP 503 strongly recommends fragments to come |
| # from. |
| _ALGOS = [ |
| "blake2b", |
| "blake2s", |
| "md5", |
| "sha1", |
| "sha224", |
| "sha256", |
| "sha384", |
| "sha3_224", |
| "sha3_256", |
| "sha3_384", |
| "sha3_512", |
| "sha512", |
| "shake_128", |
| "shake_256", |
| ] |
| |
| # The algorithms supported by the Subresource Integrity format understood by |
| # `ctx.download(integrity = ...)`, strongest first. |
| _SRI_ALGOS = ["sha512", "sha384", "sha256"] |
| |
| # The order used to pick the digest identifying an artifact when several are |
| # available: `sha256` first so that repo names stay stable for the common |
| # case, then the remaining algorithms that the bazel downloader can verify. |
| _PREFERRED_ALGOS = ["sha256", "sha512", "sha384"] |
| |
| _HEX = "0123456789abcdef" |
| _B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" |
| |
| def _hex_to_b64(hex_digest): |
| if len(hex_digest) % 2: |
| return None |
| |
| data = [] |
| for i in range(0, len(hex_digest), 2): |
| hi = _HEX.find(hex_digest[i]) |
| lo = _HEX.find(hex_digest[i + 1]) |
| if hi < 0 or lo < 0: |
| return None |
| data.append(hi * 16 + lo) |
| |
| out = [] |
| for i in range(0, len(data) // 3 * 3, 3): |
| n = (data[i] << 16) | (data[i + 1] << 8) | data[i + 2] |
| out.append(_B64[n >> 18]) |
| out.append(_B64[(n >> 12) & 63]) |
| out.append(_B64[(n >> 6) & 63]) |
| out.append(_B64[n & 63]) |
| |
| rem = len(data) % 3 |
| if rem == 1: |
| n = data[-1] << 16 |
| out.append(_B64[n >> 18]) |
| out.append(_B64[(n >> 12) & 63]) |
| out.append("==") |
| elif rem == 2: |
| n = (data[-2] << 16) | (data[-1] << 8) |
| out.append(_B64[n >> 18]) |
| out.append(_B64[(n >> 12) & 63]) |
| out.append(_B64[(n >> 6) & 63]) |
| out.append("=") |
| |
| return "".join(out) |
| |
| def _digest(algo, hex_digest): |
| """Construct a canonical `<algo>:<hex digest>` digest string. |
| |
| Args: |
| algo: {type}`str` the hash algorithm name, e.g. `sha256`. |
| hex_digest: {type}`str` the hex encoded digest. |
| |
| Returns: |
| {type}`str` the canonical digest string or an empty string if the |
| algorithm is not a known `hashlib` algorithm or the digest is empty. |
| """ |
| algo = algo.lower() |
| if not hex_digest or algo not in _ALGOS: |
| return "" |
| |
| return "{}:{}".format(algo, hex_digest) |
| |
| def _hex_to_sri(algo, hex_digest): |
| """Convert a hex digest to a Subresource Integrity value. |
| |
| Args: |
| algo: {type}`str` the hash algorithm name, e.g. `sha512`. |
| hex_digest: {type}`str` the hex encoded digest. |
| |
| Returns: |
| {type}`str` the SRI value (e.g. `sha512-...`) that can be passed to |
| `ctx.download(integrity = ...)` or an empty string if the algorithm |
| cannot be expressed as SRI or the digest is not valid hex. |
| """ |
| if algo not in _SRI_ALGOS or not hex_digest: |
| return "" |
| |
| b64 = _hex_to_b64(hex_digest.lower()) |
| if b64 == None: |
| return "" |
| |
| return "{}-{}".format(algo, b64) |
| |
| def _integrity(digest): |
| """Get the SRI value for a canonical `<algo>:<hex digest>` string. |
| |
| Args: |
| digest: {type}`str` the canonical digest string. |
| |
| Returns: |
| {type}`str` the SRI value (e.g. `sha256-...`) that can be passed to |
| `ctx.download(integrity = ...)` or an empty string if the algorithm |
| cannot be expressed as SRI. Fails if the algorithm is SRI supported |
| but the digest is not valid hex, because silently returning an empty |
| string would disable the download verification for an artifact that |
| should be verifiable. |
| """ |
| algo, _, hex_digest = digest.partition(":") |
| if algo not in _SRI_ALGOS or not hex_digest: |
| return "" |
| |
| sri = _hex_to_sri(algo, hex_digest) |
| if not sri: |
| fail("Invalid {} digest: {}".format(algo, hex_digest)) |
| |
| return sri |
| |
| def _preferred_digest(digests): |
| """Pick the digest that identifies an artifact, e.g. for repo naming. |
| |
| Args: |
| digests: {type}`list[str]` canonical `<algo>:<hex digest>` strings. |
| |
| Returns: |
| {type}`str` the canonical digest string of the most preferred |
| algorithm present or an empty string if there are no digests. |
| """ |
| by_algo = {} |
| for digest in digests: |
| if not digest: |
| continue |
| algo, _, _ = digest.partition(":") |
| by_algo.setdefault(algo, digest) |
| |
| if not by_algo: |
| return "" |
| |
| for algo in _PREFERRED_ALGOS: |
| if algo in by_algo: |
| return by_algo[algo] |
| |
| return by_algo[sorted(by_algo.keys())[0]] |
| |
| hash = struct( |
| ALGOS = _ALGOS, |
| digest = _digest, |
| hex_to_sri = _hex_to_sri, |
| integrity = _integrity, |
| preferred_digest = _preferred_digest, |
| ) |