Skip to content

Commit 2d64770

Browse files
committed
Add HTML corpus decoder and publish to GitHub Pages
1 parent c2abc91 commit 2d64770

File tree

9 files changed

+955
-2
lines changed

9 files changed

+955
-2
lines changed

.github/workflows/pages-ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Playwright browser test for corpus decoder
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
test:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.12"
24+
25+
- name: Install dependencies
26+
run: |
27+
python -m pip install --upgrade pip
28+
pip install .[browser-tests]
29+
pip install pytest pytest-playwright
30+
python -m playwright install
31+
32+
- name: Run Playwright browser test
33+
run: pytest tests/browser/test_corpus_decoder.py

.github/workflows/pages.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: Deploy decoder to GitHub Pages
2+
3+
on:
4+
push:
5+
branches: [master]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: read
10+
pages: write
11+
id-token: write
12+
13+
concurrency:
14+
group: "pages"
15+
cancel-in-progress: false
16+
17+
jobs:
18+
build:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- name: Checkout
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Python
25+
uses: actions/setup-python@v5
26+
with:
27+
python-version: "3.12"
28+
29+
- name: Install dependencies
30+
run: |
31+
python -m pip install --upgrade pip
32+
pip install .
33+
34+
- name: Generate decoder HTML
35+
run: python -m curl_fuzzer_tools.generate_decoder_html
36+
37+
- name: Upload artifact
38+
uses: actions/upload-pages-artifact@v3
39+
with:
40+
path: docs
41+
42+
deploy:
43+
needs: build
44+
runs-on: ubuntu-latest
45+
environment:
46+
name: github-pages
47+
url: ${{ steps.deployment.outputs.page_url }}
48+
steps:
49+
- name: Deploy to GitHub Pages
50+
id: deployment
51+
uses: actions/deploy-pages@v4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ libstandaloneengine.a
3434
__pycache__/
3535
logs/
3636
*.egg-info/
37+
38+
# Docs
39+
docs/

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,34 @@ read_corpus <path/to/file>
8080
```
8181
This will print out a list of contents inside the file.
8282

83+
## I want an HTML decoder for corpus files
84+
85+
Generate a standalone HTML page that can inspect TLV corpora directly in your browser:
86+
87+
```shell
88+
python -m curl_fuzzer_tools.generate_decoder_html
89+
```
90+
91+
By default the generator writes to `docs/corpus-decoder/index.html`. The page is entirely client-side; it never uploads the selected file. You can open the output straight from the filesystem, for example `file:///.../docs/corpus-decoder/index.html`.
92+
93+
**View the latest published decoder:**
94+
95+
[curl corpus decoder (GitHub Pages)](https://curl.github.io/curl-fuzzer/corpus-decoder/index.html)
96+
97+
GitHub Pages is configured to deploy automatically from the `docs/` folder whenever the `main` branch is updated. Use the command above locally before pushing if you need to refresh the published site.
98+
99+
### Optional browser smoke-test (Playwright)
100+
101+
The Playwright regression test is opt-in so the default install stays light. If you want to run it:
102+
103+
```shell
104+
pip install -e '.[browser-tests]'
105+
playwright install chromium
106+
pytest tests/browser/test_corpus_decoder.py
107+
```
108+
109+
These commands exercise the generated HTML by uploading a sample TLV corpus in a headless Chromium run.
110+
83111
## I want to generate a new testcase
84112

85113
To generate a new testcase, run

pyproject.toml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,23 @@ classifiers = [
2222
"Topic :: Software Development :: Testing",
2323
"Typing :: Typed",
2424
]
25-
dependencies = ["scapy (>=2.6.1,<3.0.0)"]
25+
dependencies = [
26+
"scapy (>=2.6.1,<3.0.0)",
27+
"jinja2 (>=3.1.0,<4.0.0)",
28+
]
29+
30+
[project.optional-dependencies]
31+
browser-tests = [
32+
"pytest>=8.3,<9",
33+
"playwright>=1.46,<1.47",
34+
]
2635

2736
[project.scripts]
2837
read_corpus = "curl_fuzzer_tools.read_corpus:run"
2938
generate_corpus = "curl_fuzzer_tools.generate_corpus:run"
3039
corpus_to_pcap = "curl_fuzzer_tools.corpus_to_pcap:run"
3140
generate_matrix = "curl_fuzzer_tools.generate_matrix:run"
41+
generate_decoder_html = "curl_fuzzer_tools.generate_decoder_html:run"
3242

3343
[build-system]
3444
requires = ["setuptools>=61.0"]
@@ -39,6 +49,10 @@ dev = [
3949
"mypy==1.18.2",
4050
"ruff==0.14.1",
4151
]
52+
browser-tests = [
53+
"pytest>=8.3,<9",
54+
"playwright>=1.46,<1.47",
55+
]
4256

4357
[tool.mypy]
4458
warn_unused_configs = true

src/curl_fuzzer_tools/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Tooling for the curl-fuzzer repository."""
22

33
from .logger import common_logging
4+
from .generate_decoder_html import generate_html
45

56
# Import * imports
6-
__all__ = ["common_logging"]
7+
__all__ = ["common_logging", "generate_html"]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Generate an interactive HTML page for decoding curl corpus files."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
from datetime import datetime, timezone
7+
from collections.abc import Sequence
8+
from pathlib import Path
9+
10+
from jinja2 import Environment, FileSystemLoader, select_autoescape
11+
12+
from .corpus import BaseType
13+
from .logger import common_logging
14+
15+
_TEMPLATE_NAME = "corpus_decoder.html"
16+
_DEFAULT_OUTPUT = Path("docs/corpus-decoder/index.html")
17+
18+
19+
def _jinja_env() -> Environment:
20+
template_dir = Path(__file__).with_name("templates")
21+
if not template_dir.exists():
22+
raise FileNotFoundError(f"Template directory not found at {template_dir}")
23+
return Environment(
24+
loader=FileSystemLoader(template_dir),
25+
autoescape=select_autoescape(["html", "xml"]),
26+
trim_blocks=True,
27+
lstrip_blocks=True,
28+
)
29+
30+
31+
def _render_html(env: Environment) -> str:
32+
template = env.get_template(_TEMPLATE_NAME)
33+
generated_at = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ")
34+
typemap = {str(key): value for key, value in BaseType.TYPEMAP.items()}
35+
return template.render(generated_at=generated_at, typemap=typemap)
36+
37+
38+
def generate_html(output: Path) -> Path:
39+
"""Generate the HTML decoder page to the provided output path."""
40+
env = _jinja_env()
41+
html = _render_html(env)
42+
43+
output.parent.mkdir(parents=True, exist_ok=True)
44+
output.write_text(html, encoding="utf-8")
45+
return output
46+
47+
48+
def _parse_args() -> argparse.Namespace:
49+
parser = argparse.ArgumentParser(description=__doc__)
50+
parser.add_argument(
51+
"--output",
52+
type=Path,
53+
default=_DEFAULT_OUTPUT,
54+
help=f"Target path for the generated HTML file (default: {_DEFAULT_OUTPUT})",
55+
)
56+
return parser.parse_args()
57+
58+
59+
def main() -> Path:
60+
"""CLI entry point for generating the decoder HTML."""
61+
args = _parse_args()
62+
output_path = args.output
63+
generated_file = generate_html(output_path)
64+
print(f"Generated decoder HTML at {generated_file}")
65+
return generated_file
66+
67+
68+
def run() -> None:
69+
"""Wrapper to set up logging before running the tool."""
70+
common_logging(__name__, __file__)
71+
main()
72+
73+
74+
if __name__ == "__main__":
75+
run()

0 commit comments

Comments
 (0)