Compare commits
11
Commits
d7a428a4ce
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6660554e6 | ||
|
|
7bda874230 | ||
|
|
a4b091578d | ||
|
|
39df888412 | ||
|
|
697f647f8b | ||
|
|
14250cee03 | ||
|
|
cea3d53eb0 | ||
|
|
335281f1de | ||
|
|
7f14992e81 | ||
|
|
1f963a9a1c | ||
|
|
b353f4ad2a |
@@ -20,6 +20,16 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# The upstream name in a new string literal, typically brought in by an
|
||||
# upstream merge. Seconds, and needs no toolchain. tools/fork/name-check.py.
|
||||
name-check:
|
||||
runs-on: light
|
||||
container:
|
||||
image: python:3.13-slim@sha256:8d9d0b8bcf6506481eae4907c18f5e3e7902e629f5f6d684f9e7c32e85e3ddf0 # 3.13-slim
|
||||
steps:
|
||||
- uses: coffey-labs/actions/checkout@fab0c4d45e0162963965f1555df27b7bed5e20ec
|
||||
- run: python3 tools/fork/name-check.py
|
||||
|
||||
build:
|
||||
# Either runner (host1 or host2): the build needs no docker socket.
|
||||
runs-on: light
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Watch upstream for releases the fork hasn't imported yet, and open an issue
|
||||
# for each one so it waits in the tracker until someone strips it in.
|
||||
#
|
||||
# Reads metadata only -- the releases list from GitHub's API and the head of
|
||||
# this repo's `upstream` branch from Gitea's. Nothing of upstream's is fetched,
|
||||
# so none of its history (which carries the Enterprise code) can land here.
|
||||
# Importing is still by hand: tools/fork/strip.py onto `upstream`, then merge,
|
||||
# as docs/spec/SPEC.md §2.2 and §2.2a describe.
|
||||
#
|
||||
# The imported base is the tag in the `upstream` branch's head commit subject
|
||||
# ("Import upstream v0.16.22, stripped"). Drafts and pre-releases are ignored.
|
||||
# An issue is opened once per release: an existing one with the same title,
|
||||
# open or closed, stops a second.
|
||||
#
|
||||
# Daily 06:17 UTC; run it by hand with workflow_dispatch.
|
||||
name: upstream-watch
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '17 6 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: upstream-watch
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
upstream-watch:
|
||||
runs-on: light
|
||||
container:
|
||||
image: python:3.13-slim@sha256:8d9d0b8bcf6506481eae4907c18f5e3e7902e629f5f6d684f9e7c32e85e3ddf0 # 3.13-slim
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- shell: bash
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, os, re, sys, urllib.request
|
||||
|
||||
api = f"{os.environ['CI_SERVER_INTERNAL']}/api/v1/repos/{os.environ['REPO']}"
|
||||
def call(method, url, body=None, token=os.environ["TOKEN"]):
|
||||
headers = {"Content-Type": "application/json", "User-Agent": "inbuxa-upstream-watch"}
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
req = urllib.request.Request(url, method=method, headers=headers,
|
||||
data=json.dumps(body).encode() if body is not None else None)
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.load(r)
|
||||
SEMVER = re.compile(r"^v(\d+)\.(\d+)\.(\d+)$")
|
||||
def key(tag):
|
||||
return tuple(int(x) for x in SEMVER.match(tag).groups())
|
||||
|
||||
subject = call("GET", f"{api}/branches/upstream")["commit"]["message"].splitlines()[0]
|
||||
m = re.search(r"\bupstream (v\d+\.\d+\.\d+)\b", subject)
|
||||
if not m:
|
||||
print(f"Can't read the imported base from the upstream branch: {subject!r}", file=sys.stderr); sys.exit(1)
|
||||
base = m.group(1)
|
||||
|
||||
# Unauthenticated: a public repo, once a day, well inside the limit.
|
||||
rels = call("GET", "https://api.github.com/repos/stalwartlabs/stalwart/releases?per_page=30", token=None)
|
||||
newer = sorted((r for r in rels
|
||||
if not r["draft"] and not r["prerelease"] and SEMVER.match(r["tag_name"])
|
||||
and key(r["tag_name"]) > key(base)),
|
||||
key=lambda r: key(r["tag_name"]))
|
||||
if not newer:
|
||||
print(f"Up to date: {base} is the newest upstream release."); sys.exit(0)
|
||||
|
||||
# Titles and bodies stay free of the upstream project's name, as the
|
||||
# rest of the fork's user-visible text does.
|
||||
existing = {i["title"] for i in call("GET", f"{api}/issues?state=all&type=issues&q=Import+upstream&limit=50")}
|
||||
for r in newer:
|
||||
tag = r["tag_name"]
|
||||
title = f"Import upstream {tag}"
|
||||
if title in existing:
|
||||
print(f"{tag}: issue already exists."); continue
|
||||
body = (f"Upstream published {tag} on {r['published_at'][:10]}. "
|
||||
f"The fork's imported base is {base}.\n\n"
|
||||
"Import it as tools/fork/README.md describes:\n\n"
|
||||
"```bash\n"
|
||||
"git -C \"$UPSTREAM_CLONE\" fetch --tags\n"
|
||||
f"tools/fork/strip.py --upstream \"$UPSTREAM_CLONE\" --ref {tag} --out /tmp/strip-{tag}\n"
|
||||
"```\n\n"
|
||||
"Commit the stripped tree to `upstream` with the strip report in the message, "
|
||||
"add any new third-party notices to `THIRD-PARTY.md`, then merge `upstream` into `main`.")
|
||||
issue = call("POST", f"{api}/issues", {"title": title, "body": body})
|
||||
print(f"{tag}: opened #{issue['number']}.")
|
||||
PY
|
||||
@@ -1,50 +0,0 @@
|
||||
# CI on the self-hosted GitLab, ported from .github/workflows/ci.yml when the
|
||||
# GitHub account was suspended on 2026-09-20. The Actions file stays in the
|
||||
# tree: it is the reference this was written from and works unchanged if the
|
||||
# appeal succeeds.
|
||||
#
|
||||
# The image is pinned by digest, with its tag in the trailing comment. That
|
||||
# replaces the SHA-pinned `uses:` in the workflow -- GitLab has no action
|
||||
# allowlist, so the digest is the only thing fixing what actually runs.
|
||||
#
|
||||
# Not ported here:
|
||||
# * cleanup.yml pruned GHCR with dataaxiom/ghcr-cleanup-action. GitLab has
|
||||
# no equivalent action because it does not need one: the container
|
||||
# registry has a cleanup policy on the project itself, which is where that
|
||||
# job's settings now live.
|
||||
# * publish.yml and release.yml still need doing; they are larger and are
|
||||
# being handled separately.
|
||||
|
||||
stages: [build]
|
||||
|
||||
default:
|
||||
interruptible: true
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: rust:1-bookworm@sha256:93ce27a88655056a51dbdd8f5f2d7ddc071c7b0070fb288a37b5a285fc83971e # 1-bookworm
|
||||
# This is a big workspace and a cold build is expensive, so the registry and
|
||||
# the target directory are cached between runs. Both are kept inside the
|
||||
# project directory because that is the only path the runner will cache --
|
||||
# and deliberately not on /tmp, which on this host is a tmpfs that a Rust
|
||||
# build of this size has filled before.
|
||||
variables:
|
||||
CARGO_HOME: "$CI_PROJECT_DIR/.cargo"
|
||||
CARGO_TARGET_DIR: "$CI_PROJECT_DIR/target"
|
||||
CARGO_INCREMENTAL: "0"
|
||||
cache:
|
||||
key:
|
||||
files: [Cargo.lock]
|
||||
paths:
|
||||
- .cargo/registry/
|
||||
- target/
|
||||
before_script:
|
||||
- apt-get update -qq && apt-get install -y -qq --no-install-recommends clang >/dev/null
|
||||
script:
|
||||
- cargo build -p inbuxa --locked
|
||||
# --no-run: the workflow compiled every test target without running them,
|
||||
# which catches a test that no longer builds without paying for the suite.
|
||||
- cargo test --workspace --locked --no-run
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
@@ -583,10 +583,10 @@ impl Metrics {
|
||||
pub async fn parse(bp: &mut Bootstrap) -> Self {
|
||||
let metrics = bp.setting_infallible::<structs::Metrics>().await;
|
||||
let resource = Resource::builder()
|
||||
.with_service_name("stalwart")
|
||||
.with_service_name("inbuxa")
|
||||
.with_attribute(KeyValue::new(SERVICE_VERSION, types::brand_version_full!()))
|
||||
.build();
|
||||
let instrumentation = InstrumentationScope::builder("stalwart")
|
||||
let instrumentation = InstrumentationScope::builder("inbuxa")
|
||||
.with_version(types::brand_version_full!())
|
||||
.build();
|
||||
|
||||
|
||||
@@ -522,7 +522,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn fixture(name: &str, client_id: Option<&str>) -> (WebApplications, TempDir) {
|
||||
let dir = TempDir::new(std::env::temp_dir().join(format!("stalwart-app-{name}")));
|
||||
let dir = TempDir::new(std::env::temp_dir().join(format!("inbuxa-app-{name}")));
|
||||
dir.clean().await.unwrap();
|
||||
tokio::fs::write(dir.path.join("index.html"), INDEX)
|
||||
.await
|
||||
|
||||
@@ -298,7 +298,7 @@ mod tests {
|
||||
|
||||
fn web_interface() -> Application {
|
||||
Application {
|
||||
description: "Stalwart Web Interface".to_string(),
|
||||
description: "INBUXA Web Interface".to_string(),
|
||||
enabled: true,
|
||||
url_prefix: Map::new(vec!["/admin".into(), "/account".into()]),
|
||||
..Default::default()
|
||||
@@ -312,7 +312,7 @@ mod tests {
|
||||
clients,
|
||||
vec![FirstPartyClient {
|
||||
client_id: WEB_INTERFACE_CLIENT_ID.to_string(),
|
||||
description: "Stalwart Web Interface (served by this server)".to_string(),
|
||||
description: "INBUXA Web Interface (served by this server)".to_string(),
|
||||
redirect_uris: vec![
|
||||
"https://mail.example.org/admin/oauth/callback".to_string(),
|
||||
"https://mail.example.org/account/oauth/callback".to_string(),
|
||||
|
||||
@@ -29,11 +29,11 @@ pub(crate) fn spawn_otel_tracer(builder: SubscriberBuilder, mut otel: OtelTracer
|
||||
let (_, mut rx) = builder.register();
|
||||
tokio::spawn(async move {
|
||||
let resource = Resource::builder()
|
||||
.with_service_name("stalwart")
|
||||
.with_service_name("inbuxa")
|
||||
.with_attribute(KeyValue::new(SERVICE_VERSION, types::brand_version_full!()))
|
||||
.build();
|
||||
|
||||
let instrumentation = InstrumentationScope::builder("stalwart")
|
||||
let instrumentation = InstrumentationScope::builder("inbuxa")
|
||||
.with_version(types::brand_version_full!())
|
||||
.build();
|
||||
|
||||
|
||||
@@ -657,7 +657,7 @@ fn map_dns_server(dns_server: &DnsServerBootstrap) -> Option<registry::schema::s
|
||||
// FreeBSD keeps variable application data under /var/db (hier(7))
|
||||
// rather than FHS /var/lib.
|
||||
const DEFAULT_DATA_PATH: &str = if cfg!(target_os = "freebsd") {
|
||||
"/var/db/stalwart/"
|
||||
"/var/db/inbuxa/"
|
||||
} else {
|
||||
"/var/lib/inbuxa/"
|
||||
};
|
||||
|
||||
@@ -211,7 +211,7 @@ impl<T: SessionStream> Session<T> {
|
||||
Request::Help { .. } => {
|
||||
trc::event!(Smtp(SmtpEvent::Help), SpanId = self.data.session_id,);
|
||||
|
||||
self.write(b"250 2.0.0 Help can be found at https://stalw.art\r\n")
|
||||
self.write(concat!("250 2.0.0 Help can be found at ", types::brand_url!(), "\r\n").as_bytes())
|
||||
.await?;
|
||||
}
|
||||
Request::Helo { host } => {
|
||||
|
||||
@@ -80,7 +80,7 @@ const SPAN_MAX_HOLD: u64 = 60 * 60 * 24; // 1 day
|
||||
pub(crate) static COLLECTOR_THREAD: LazyLock<Arc<CollectorThread>> = LazyLock::new(|| {
|
||||
Arc::new(
|
||||
Builder::new()
|
||||
.name("stalwart-collector".to_string())
|
||||
.name("inbuxa-collector".to_string())
|
||||
.spawn(move || {
|
||||
Collector::default().collect();
|
||||
})
|
||||
|
||||
@@ -71,7 +71,7 @@ pub fn env_var(name: &str) -> Result<String, std::env::VarError> {
|
||||
#[macro_export]
|
||||
macro_rules! brand_version {
|
||||
() => {
|
||||
"2026.9.22"
|
||||
"2026.9.23"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,7 +82,11 @@ macro_rules! brand_version {
|
||||
/// this becomes just the version.
|
||||
#[macro_export]
|
||||
macro_rules! brand_version_full {
|
||||
// The upstream crate version, without naming the upstream project: this
|
||||
// string is user-visible (--version, the startup banner, the console,
|
||||
// telemetry and the JMAP session's "implementation" field), and the name
|
||||
// belongs only in copyright notices and the lineage line.
|
||||
() => {
|
||||
concat!($crate::brand_version!(), " (Stalwart ", env!("CARGO_PKG_VERSION"), ")")
|
||||
concat!($crate::brand_version!(), " (upstream ", env!("CARGO_PKG_VERSION"), ")")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,22 @@ at the repository root before the import is merged.
|
||||
|
||||
It needs Python 3.12+ (for `tarfile`'s `data` filter) and git.
|
||||
|
||||
## name-check.py
|
||||
|
||||
Fails when the upstream project's name appears in a Rust string literal that
|
||||
`name-allowlist.txt` doesn't list. CI runs it on every push and pull request,
|
||||
so an upstream merge can't bring the name back into what users and operators
|
||||
see. Comments, copyright headers and test directories aren't checked.
|
||||
|
||||
```bash
|
||||
tools/fork/name-check.py # exit 1 on anything new
|
||||
tools/fork/name-check.py --list # every finding, in allowlist format
|
||||
```
|
||||
|
||||
Rename what it reports. If a string has to stay, such as a key-derivation
|
||||
context or a wire-protocol identifier, add its `--list` line to the allowlist
|
||||
under the reason it stays.
|
||||
|
||||
## record-compat.py
|
||||
|
||||
Records what the `*_compat` tests compare against, from the Enterprise
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# String literals allowed to keep the upstream project's name.
|
||||
# Read by tools/fork/name-check.py. One per line: path<TAB>literal as a JSON
|
||||
# string, as `name-check.py --list` prints it. Each group says why it stays;
|
||||
# add a line only under a reason, or with a new one.
|
||||
|
||||
# Key-derivation contexts. Renaming them invalidates every sealed OAuth token
|
||||
# and client id already issued.
|
||||
crates/common/src/auth/oauth/client_id.rs "stalwart-oauth-client-id-sw1"
|
||||
crates/common/src/auth/oauth/token.rs "stalwart-oauth-token-sw1"
|
||||
|
||||
# Keys and prefixes of data already in the store.
|
||||
crates/common/src/manager/application.rs "STALWART_APP_"
|
||||
crates/common/src/manager/mod.rs "STALWART_SPAM_CLASSIFIER_MODEL.lz4"
|
||||
crates/common/src/manager/mod.rs "STALWART_SPAM_TRAIN_DATA.lz4"
|
||||
|
||||
# The web interface's OAuth client id, which existing installs and the admin
|
||||
# front end already use. The id itself, and the tests that check it.
|
||||
crates/common/src/manager/first_party.rs "stalwart-webui"
|
||||
crates/common/src/manager/application.rs "stalwart-webui"
|
||||
crates/common/src/manager/application.rs "<meta name=\\\"oauth-client-id\\\" content=\\\"stalwart-webui\\\" />"
|
||||
|
||||
# Wire-protocol identifiers clients already hold or negotiate: WebDAV lock and
|
||||
# sync tokens, the JMAP capability, Sieve extensions.
|
||||
crates/dav/src/common/lock.rs "urn:stalwart:davsync:"
|
||||
crates/dav/src/common/uri.rs "urn:stalwart:"
|
||||
crates/dav/src/common/uri.rs "urn:stalwart:davlock:{id:x}"
|
||||
crates/dav/src/common/uri.rs "urn:stalwart:davsync:"
|
||||
crates/dav/src/common/uri.rs "urn:stalwart:davsync:{id:x}"
|
||||
crates/dav/src/common/uri.rs "urn:stalwart:davsync:{id:x}:{seq:x}"
|
||||
crates/jmap-proto/src/request/capability.rs "urn:stalwart:jmap"
|
||||
crates/registry/src/schema/enums_impl.rs "vnd.stalwart.expressions"
|
||||
crates/registry/src/schema/enums_impl.rs "vnd.stalwart.while"
|
||||
|
||||
# Moving an existing upstream installation over: its environment variables,
|
||||
# and upstream's upgrade guide for the store conversion.
|
||||
crates/types/src/branding.rs "STALWART_{name}"
|
||||
crates/types/src/branding.rs "Warning: STALWART_{name} is deprecated; set INBUXA_{name} instead."
|
||||
crates/store/src/build/registry.rs "⚠️ INBUXA_RECOVERY_ADMIN (or STALWART_RECOVERY_ADMIN) is set, but the"
|
||||
crates/jmap/src/registry/mapping/bootstrap.rs "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md"
|
||||
crates/migration/src/lib.rs "https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md"
|
||||
|
||||
# Upstream's published spam-filter rules, fetched at runtime.
|
||||
crates/registry/src/schema/structs_impl.rs "https://github.com/stalwartlabs/spam-filter/releases/latest/download/spam-filter-rules.json.gz"
|
||||
|
||||
# Test fixtures: web-push contact address parsing.
|
||||
crates/common/src/network/webpush.rs " [email protected] "
|
||||
crates/common/src/network/webpush.rs "MAILTO:[email protected]"
|
||||
crates/common/src/network/webpush.rs "[email protected]"
|
||||
crates/common/src/network/webpush.rs "http://stalw.art"
|
||||
crates/common/src/network/webpush.rs "https://stalw.art/contact"
|
||||
crates/common/src/network/webpush.rs "mailto:[email protected]"
|
||||
crates/common/src/network/webpush.rs "stalw.art"
|
||||
|
||||
# OPEN, not yet decided (2026-09-22): operator-visible defaults. The log file
|
||||
# prefix (TracerLog, and the bootstrap's tracer) names files stalwart.* in
|
||||
# /var/log/inbuxa/, and the SQL stores default their database and user to
|
||||
# "stalwart". Changing the SQL defaults would break an install relying on them.
|
||||
crates/jmap/src/registry/mapping/bootstrap.rs "stalwart"
|
||||
crates/registry/src/schema/structs_impl.rs "stalwart"
|
||||
Executable
+145
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
"""
|
||||
Fail when the upstream project's name turns up in a new Rust string literal.
|
||||
|
||||
tools/fork/name-check.py # check; exit 1 on anything new
|
||||
tools/fork/name-check.py --list # print every finding, allowlist format
|
||||
|
||||
The name belongs only in copyright notices and the lineage line. Everything
|
||||
else a user or operator can see -- messages, the version string, service
|
||||
names, descriptions -- carries INBUXA's. Merging an upstream release brings
|
||||
new strings in with the name, and the merge itself can't tell, so this runs
|
||||
in CI on every push and pull request.
|
||||
|
||||
Scope: string literals in `crates/**/*.rs`, test directories excluded.
|
||||
Comments are skipped, so copyright headers and doc comments never match.
|
||||
Some literals have to keep the name -- key-derivation contexts, wire-protocol
|
||||
identifiers, defaults that read an upstream installation -- and those are
|
||||
listed in `name-allowlist.txt` beside this script, each under the reason it
|
||||
stays. A finding is matched by file and literal text, not line number, so
|
||||
the allowlist survives code moving around.
|
||||
|
||||
When the check fails, rename the string. If it genuinely has to stay, add
|
||||
the line `--list` prints for it to the allowlist under a reason.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
ALLOWLIST = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'name-allowlist.txt')
|
||||
NAME = re.compile(r'stalwart|stalw\.art', re.IGNORECASE)
|
||||
SKIP_DIRS = {'tests', 'benches', 'target', '.git'}
|
||||
CHAR = re.compile(r"'(?:\\u\{[0-9a-fA-F]+\}|\\x[0-9a-fA-F]{2}|\\.|[^\\'\n])'")
|
||||
RAW = re.compile(r'b?r(#*)"')
|
||||
|
||||
|
||||
def literals(src):
|
||||
"""Yield the text of every string literal in `src`, comments skipped."""
|
||||
i, n = 0, len(src)
|
||||
while i < n:
|
||||
c = src[i]
|
||||
if src.startswith('//', i):
|
||||
i = src.find('\n', i)
|
||||
if i < 0:
|
||||
return
|
||||
elif src.startswith('/*', i):
|
||||
depth, i = 1, i + 2
|
||||
while i < n and depth:
|
||||
if src.startswith('/*', i):
|
||||
depth, i = depth + 1, i + 2
|
||||
elif src.startswith('*/', i):
|
||||
depth, i = depth - 1, i + 2
|
||||
else:
|
||||
i += 1
|
||||
elif c in 'br' and (m := RAW.match(src, i)) and (i == 0 or not (src[i - 1].isalnum() or src[i - 1] == '_')):
|
||||
end = '"' + m.group(1)
|
||||
j = src.find(end, m.end())
|
||||
if j < 0:
|
||||
return
|
||||
yield src[m.end():j]
|
||||
i = j + len(end)
|
||||
elif c == '"':
|
||||
j = i + 1
|
||||
while j < n and src[j] != '"':
|
||||
j += 2 if src[j] == '\\' else 1
|
||||
yield src[i + 1:j]
|
||||
i = j + 1
|
||||
elif c == "'":
|
||||
# A char literal, or else a lifetime / label, which is skipped.
|
||||
m = CHAR.match(src, i)
|
||||
i = m.end() if m else i + 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
|
||||
def findings():
|
||||
found = set()
|
||||
crates = os.path.join(ROOT, 'crates')
|
||||
for dirpath, dirnames, filenames in os.walk(crates):
|
||||
dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS)
|
||||
for f in sorted(filenames):
|
||||
if not f.endswith('.rs'):
|
||||
continue
|
||||
path = os.path.join(dirpath, f)
|
||||
with open(path, encoding='utf-8', errors='replace') as fh:
|
||||
for lit in literals(fh.read()):
|
||||
if NAME.search(lit):
|
||||
found.add((os.path.relpath(path, ROOT), lit))
|
||||
return found
|
||||
|
||||
|
||||
def fmt(entry):
|
||||
return f'{entry[0]}\t{json.dumps(entry[1], ensure_ascii=False)}'
|
||||
|
||||
|
||||
def allowlist():
|
||||
allowed = set()
|
||||
with open(ALLOWLIST, encoding='utf-8') as fh:
|
||||
for n, line in enumerate(fh, 1):
|
||||
line = line.rstrip('\n')
|
||||
if not line.strip() or line.lstrip().startswith('#'):
|
||||
continue
|
||||
path, sep, lit = line.partition('\t')
|
||||
try:
|
||||
allowed.add((path, json.loads(lit)))
|
||||
except (ValueError, TypeError):
|
||||
sys.exit(f'{ALLOWLIST}:{n}: expected "path<TAB>json string", got {line!r}')
|
||||
if not sep:
|
||||
sys.exit(f'{ALLOWLIST}:{n}: expected "path<TAB>json string", got {line!r}')
|
||||
return allowed
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__.split('\n\n')[0].strip())
|
||||
ap.add_argument('--list', action='store_true', help='print every finding in allowlist format and exit')
|
||||
args = ap.parse_args()
|
||||
|
||||
found = findings()
|
||||
if args.list:
|
||||
for entry in sorted(found):
|
||||
print(fmt(entry))
|
||||
return 0
|
||||
|
||||
allowed = allowlist()
|
||||
new = sorted(found - allowed)
|
||||
stale = sorted(allowed - found)
|
||||
for entry in stale:
|
||||
# Gone from the code: harmless, but the list should shrink with it.
|
||||
print(f'stale allowlist entry, no longer in the code: {fmt(entry)}')
|
||||
if new:
|
||||
print(f'\n{len(new)} string literal(s) carry the upstream name. Rename them, or if one must stay,')
|
||||
print(f'add its line to {os.path.relpath(ALLOWLIST, ROOT)} under the reason:\n')
|
||||
for entry in new:
|
||||
print(fmt(entry))
|
||||
return 1
|
||||
print(f'name check: clean ({len(found)} allowlisted literal(s)).')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user