blob: bcdfaecf0043c7f6dfd69275422ebd84acb7297b [file]
# 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,
)