Skip to content

Commit aa6bada

Browse files
committed
pythongh-100538: Create a workflow for verifying bundled libexpat files
1 parent 199507b commit aa6bada

File tree

4 files changed

+167
-5
lines changed

4 files changed

+167
-5
lines changed

Diff for: .github/workflows/verify-expat.yml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Verify bundled libexpat
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
paths:
7+
- 'Modules/expat/**'
8+
- '.github/workflows/verify-expat.yml'
9+
- 'Tools/build/verify_expat.py'
10+
pull_request:
11+
paths:
12+
- 'Modules/expat/**'
13+
- '.github/workflows/verify-expat.yml'
14+
- 'Tools/build/verify_expat.py'
15+
16+
permissions:
17+
contents: read
18+
19+
concurrency:
20+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
verify:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v3
28+
- uses: actions/setup-python@v4
29+
with:
30+
python-version: '3'
31+
- name: Verify bundled libexpat files
32+
run: ./Tools/build/verify_expat.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Create a GitHub Actions workflow for verifying bundled libexpat files. Patch
2+
by Illia Volochii.

Diff for: Modules/expat/expat_external.h

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
/* Namespace external symbols to allow multiple libexpat version to
2+
co-exist. */
3+
#include "pyexpatns.h"
4+
15
/*
26
__ __ _
37
___\ \/ /_ __ __ _| |_
@@ -64,11 +68,6 @@
6468
compiled with the cdecl calling convention as the default since
6569
system headers may assume the cdecl convention.
6670
*/
67-
68-
/* Namespace external symbols to allow multiple libexpat version to
69-
co-exist. */
70-
#include "pyexpatns.h"
71-
7271
#ifndef XMLCALL
7372
# if defined(_MSC_VER)
7473
# define XMLCALL __cdecl

Diff for: Tools/build/verify_expat.py

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#! /usr/bin/env python3
2+
"""
3+
Verify that bundled libexpat files come from a verified release.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import json
9+
import os
10+
import re
11+
import tarfile
12+
from hashlib import sha3_256
13+
from pathlib import Path
14+
from typing import Literal
15+
from urllib.request import Request, urlopen
16+
17+
GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true"
18+
19+
EXPAT_REL_PATH = "Modules/expat/"
20+
EXPAT_PATH = Path(__file__).parent.parent.parent / EXPAT_REL_PATH
21+
22+
23+
def log(
24+
level: Literal["debug", "notice", "error"], file_path: str | None, message: str
25+
) -> None:
26+
if GITHUB_ACTIONS:
27+
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
28+
if file_path:
29+
message = f"::{level} file={file_path}::{message}"
30+
else:
31+
message = f"::{level}::{message}"
32+
print(message)
33+
34+
35+
def verify_expat() -> bool:
36+
has_failed = False
37+
38+
with open(EXPAT_PATH / "expat.h") as headers_file:
39+
headers = headers_file.read()
40+
version_pieces = re.findall(
41+
r"(?<=^#define XML_(?:MAJOR|MINOR|MICRO)_VERSION ).+$", headers, re.MULTILINE
42+
)
43+
44+
expat_version = ".".join(version_pieces)
45+
log("debug", None, f"Verifying bundled files from libexpat {expat_version}.")
46+
47+
# Offload verifying the release tag to GitHub.
48+
# https://docs.github.com/en/rest/git/commits?apiVersion=2022-11-28#get-a-commit
49+
tag_name = f"R_{'_'.join(version_pieces)}"
50+
tag_data_request = Request(
51+
f"https://api.github.com/repos/libexpat/libexpat/commits/{tag_name}",
52+
headers={"X-GitHub-Api-Version": "2022-11-28"},
53+
)
54+
with urlopen(tag_data_request) as tag_data_io:
55+
tag_data = json.load(tag_data_io)
56+
if tag_data["commit"]["verification"]["verified"]:
57+
log(
58+
"debug",
59+
None,
60+
f"The signature in libexpat {expat_version} is considered to be verified.",
61+
)
62+
else:
63+
log(
64+
"error",
65+
None,
66+
f"The signature in libexpat {expat_version} is not considered to be verified.",
67+
)
68+
has_failed = True
69+
70+
# Download tarball from GitHub and generate hashes for files that
71+
# have to be bundled.
72+
tarball_url = (
73+
f"https://github.com/libexpat/libexpat/archive/refs/tags/{tag_name}.tar.gz"
74+
)
75+
expected_hashes = {}
76+
with urlopen(tarball_url) as tarball_io:
77+
with tarfile.open(fileobj=tarball_io, mode="r:gz") as tarball:
78+
for member in tarball:
79+
if re.search(
80+
r"/expat/lib/\w+\.[ch]$", member.name
81+
) or member.name.endswith("/COPYING"):
82+
file_name = os.path.basename(member.name)
83+
content = tarball.extractfile(member).read() # type: ignore[union-attr]
84+
expected_hashes[file_name] = sha3_256(content).hexdigest()
85+
86+
# Compare hashes of bundled libexpat files to the actually released
87+
# files.
88+
with os.scandir(EXPAT_PATH) as expat_dir:
89+
for entry in expat_dir:
90+
with open(entry, "rb") as expat_file:
91+
# Skip files that are not a part of libexpat.
92+
if entry.name in (
93+
"expat_config.h",
94+
"pyexpatns.h",
95+
) or entry.name.endswith((".a", ".o")):
96+
continue
97+
# Skip a few known lines added to expat_external.h.
98+
elif entry.name == "expat_external.h":
99+
for _ in range(4):
100+
expat_file.readline()
101+
file_path = os.path.join(EXPAT_REL_PATH, entry.name)
102+
content = expat_file.read()
103+
if sha3_256(content).hexdigest() == expected_hashes[entry.name]:
104+
log(
105+
"debug",
106+
file_path,
107+
f"{entry.name} is the same as in libexpat {expat_version}.",
108+
)
109+
else:
110+
log(
111+
"error",
112+
file_path,
113+
f"{entry.name} is not the same as in libexpat {expat_version}.",
114+
)
115+
has_failed = True
116+
del expected_hashes[entry.name]
117+
118+
if expected_hashes:
119+
log("error", None, f"{expected_hashes.keys()} files were not bundled.")
120+
has_failed = True
121+
122+
has_succeeded = not has_failed
123+
if has_succeeded:
124+
log("notice", None, "Successfully verified bundled libexpat files.")
125+
return has_succeeded
126+
127+
128+
if __name__ == "__main__":
129+
raise SystemExit(0 if verify_expat() else 1)

0 commit comments

Comments
 (0)