Revision control
Copy as Markdown
Other Tools
#!/usr/bin/env python3
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
"""Vendor the axe-core browser bundle from the published npm package."""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import os
import posixpath
import shutil
import sys
import tarfile
import tempfile
import urllib.request
PACKAGE_NAME = "axe-core"
VENDORED_FILES = (
"LICENSE",
"LICENSE-3RD-PARTY.txt",
"README.md",
"axe.min.js",
"package.json",
)
class AxeCoreNpmHost:
"""Source-host adapter used by mach vendor for npm-hosted axe-core."""
def __init__(self, manifest):
self.manifest = manifest
def upstream_commit(self, revision):
"""Return (version, release timestamp) for the requested npm version."""
metadata = fetch_package_metadata()
version = normalize_revision(revision, metadata)
if version not in metadata["versions"]:
raise ValueError(f"{PACKAGE_NAME} version {version} was not found")
return version, metadata["time"][version]
def upstream_tag(self, revision):
return self.upstream_commit(revision)
def fetch_json(url):
with urllib.request.urlopen(url) as response:
return json.load(response)
def fetch_bytes(url):
with urllib.request.urlopen(url) as response:
return response.read()
def fetch_package_metadata():
return fetch_json(REGISTRY_URL)
def normalize_revision(revision, metadata):
if not revision or revision in {"HEAD", "latest", "tip"}:
return metadata["dist-tags"]["latest"]
return revision[1:] if revision.startswith("v") else revision
def verify_integrity(data, integrity):
algorithm, encoded_hash = integrity.split("-", 1)
if algorithm != "sha512":
raise ValueError(f"Unsupported npm integrity algorithm: {algorithm}")
expected = base64.b64decode(encoded_hash)
actual = hashlib.sha512(data).digest()
if not hmac.compare_digest(expected, actual):
raise ValueError("Downloaded axe-core tarball failed integrity check")
def package_member_name(filename):
return posixpath.join("package", filename)
def copy_vendored_files(tarball_path, vendor_dir):
with tarfile.open(tarball_path, "r:gz") as archive:
members = {member.name: member for member in archive.getmembers()}
for filename in VENDORED_FILES:
member_name = package_member_name(filename)
if member_name not in members:
raise ValueError(f"{member_name} was not found in npm package")
member = members[member_name]
if not member.isfile():
raise ValueError(f"{member_name} is not a regular file")
source = archive.extractfile(member)
if source is None:
raise ValueError(f"Could not read {member_name}")
destination_path = os.path.join(vendor_dir, filename)
with source, open(destination_path, "wb") as destination:
shutil.copyfileobj(source, destination)
def vendor(version):
metadata = fetch_package_metadata()
normalized_version = normalize_revision(version, metadata)
version_metadata = metadata["versions"][normalized_version]
tarball_url = version_metadata["dist"]["tarball"]
integrity = version_metadata["dist"]["integrity"]
tarball = fetch_bytes(tarball_url)
verify_integrity(tarball, integrity)
vendor_dir = os.path.dirname(os.path.abspath(__file__))
with tempfile.NamedTemporaryFile(suffix=".tgz") as temp_file:
temp_file.write(tarball)
temp_file.flush()
copy_vendored_files(temp_file.name, vendor_dir)
print(f"Vendored {PACKAGE_NAME} {normalized_version}")
def main(args):
if len(args) != 1:
print(f"Usage: {os.path.basename(__file__)} VERSION", file=sys.stderr)
return 1
vendor(args[0])
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))