#!/usr/bin/env bash
set -euo pipefail
usage() {
printf 'usage: %s <raw_dir> <output_dir> --owner <raw_owner> --repo <raw_repo> --project <raw_project> --host <raw_host> [--host <raw_host>...]\n' "$0" >&2
}
if [[ $# -lt 10 ]]; then
usage
exit 2
fi
raw_dir=$1
output_dir=$2
shift 2
owner=
repo=
project=
hosts=()
while [[ $# -gt 0 ]]; do
case "$1" in
--owner)
[[ $# -ge 2 ]] || { usage; exit 2; }
owner=$2
shift 2
;;
--repo)
[[ $# -ge 2 ]] || { usage; exit 2; }
repo=$2
shift 2
;;
--project)
[[ $# -ge 2 ]] || { usage; exit 2; }
project=$2
shift 2
;;
--host)
[[ $# -ge 2 ]] || { usage; exit 2; }
hosts+=("$2")
shift 2
;;
*)
usage
exit 2
;;
esac
done
if [[ -z "$owner" || -z "$repo" || -z "$project" || ${#hosts[@]} -eq 0 ]]; then
usage
exit 2
fi
if [[ ! -d "$raw_dir" ]]; then
printf 'raw_dir does not exist or is not a directory: %s\n' "$raw_dir" >&2
exit 1
fi
mkdir -p "$output_dir/api/v5"
python3 - "$raw_dir" "$output_dir" "$owner" "$repo" "$project" "${hosts[@]}" <<'PY'
import json
import os
import re
import shutil
import sys
from pathlib import Path
raw_dir = Path(sys.argv[1]).resolve()
output_dir = Path(sys.argv[2]).resolve()
owner = sys.argv[3]
repo = sys.argv[4]
project = sys.argv[5]
hosts = sys.argv[6:]
replacements = [
(owner, "example-owner"),
(repo, "example-repo"),
(project, "example-project"),
]
replacements.extend((host, "api.example.com") for host in hosts)
authorization_line = re.compile(r"(?im)^\s*authorization\s*:[^\r\n]*(?:\r?\n)?")
authorization_present = re.compile(r"(?i)authorization")
host_pattern = re.compile(r"(?i)\b(?:[a-z0-9-]+\.)+[a-z]{2,}\b")
allowed_suffixes = {".json", ".txt", ".http", ".har", ".headers", ".body", ".log"}
def replace_tokens(value):
for old, new in replacements:
if old:
value = value.replace(old, new)
return value
def strip_authorization(obj):
if isinstance(obj, dict):
return {k: strip_authorization(v) for k, v in obj.items() if k.lower() != "authorization"}
if isinstance(obj, list):
return [strip_authorization(item) for item in obj]
return obj
def sanitize_json(text, source):
stripped = authorization_line.sub("", text)
try:
decoded = json.loads(stripped)
except json.JSONDecodeError:
if authorization_present.search(stripped):
raise SystemExit(f"refusing to emit structurally ambiguous Authorization field in {source}")
return replace_tokens(stripped)
decoded = strip_authorization(decoded)
encoded = json.dumps(decoded, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
return replace_tokens(encoded)
def sanitize_text(text, source):
sanitized = authorization_line.sub("", text)
if authorization_present.search(sanitized):
raise SystemExit(f"refusing to emit structurally ambiguous Authorization field in {source}")
return replace_tokens(sanitized)
def output_relative_path(path):
relative = path.relative_to(raw_dir)
parts = list(relative.parts)
if "api" in parts:
idx = parts.index("api")
if idx + 1 < len(parts) and parts[idx + 1] == "v5":
relative = Path(*parts[idx + 2:]) if idx + 2 < len(parts) else Path(path.name)
elif "v5" in parts:
idx = parts.index("v5")
relative = Path(*parts[idx + 1:]) if idx + 1 < len(parts) else Path(path.name)
rel_text = replace_tokens(relative.as_posix())
return Path("api") / "v5" / rel_text
def verify_output(path, content):
text_path = path.relative_to(output_dir).as_posix()
check = f"{text_path}\n{content}"
forbidden = [token for token in [owner, repo, project, *hosts] if token and token in check]
if forbidden:
raise SystemExit(f"sanitized output contains raw token {forbidden[0]!r}: {path}")
if authorization_present.search(check):
raise SystemExit(f"sanitized output contains Authorization: {path}")
for host in host_pattern.findall(content):
if host != "api.example.com":
raise SystemExit(f"sanitized output contains disallowed hostname {host!r}: {path}")
for source in sorted(raw_dir.rglob("*")):
if not source.is_file():
continue
if source.suffix.lower() not in allowed_suffixes:
continue
try:
raw_text = source.read_text(encoding="utf-8")
except UnicodeDecodeError as exc:
raise SystemExit(f"fixture capture is not UTF-8 text: {source}") from exc
if source.suffix.lower() == ".json":
sanitized = sanitize_json(raw_text, source)
else:
sanitized = sanitize_text(raw_text, source)
rel = output_relative_path(source)
dest = output_dir / rel
dest.parent.mkdir(parents=True, exist_ok=True)
verify_output(dest, sanitized)
dest.write_text(sanitized, encoding="utf-8")
if not any((output_dir / "api" / "v5").rglob("*")):
raise SystemExit("no supported fixture files were sanitized")
PY