Compare commits

..

16 Commits

Author SHA1 Message Date
aclist
90c7d9f73e
Merge pull request #395 from aclist/chore/contributing-docs
Some checks are pending
Mirror to Codeberg / mirror-to-codeberg (push) Waiting to run
docs: add contributing docs
2026-07-07 16:52:16 +09:00
aclist
5923d5d935 fix: typo 2026-07-07 16:46:01 +09:00
aclist
9f73c545ef docs: port contributing file from dzgui6, with changes 2026-07-07 16:42:25 +09:00
aclist
041438cb7f docs: update readme to reflect current release workflow 2026-07-07 16:42:03 +09:00
aclist
c6e69f9aa0
Merge pull request #393 from aclist/feat/redaction-filter
feat: redaction filter
2026-07-07 16:33:22 +09:00
aclist
8f1e4ff9d0 chore: drop fixed comment
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-07 16:32:23 +09:00
aclist
92792b0b4f chore: drop extraneous comment 2026-07-07 16:29:10 +09:00
aclist
a7cfd4c346 fix: monkeypatch system env 2026-07-07 16:28:52 +09:00
aclist
fa5f638697 chore: add comment 2026-07-07 12:03:10 +09:00
aclist
78151b3bb3 fix: update regex
Some checks failed
Mirror to Codeberg / mirror-to-codeberg (push) Has been cancelled
2026-07-04 14:30:59 +09:00
aclist
2418d8be68 chore: update pytest marks 2026-07-04 14:12:46 +09:00
aclist
9fa7172056 chore: add log redaction test 2026-07-04 13:18:44 +09:00
aclist
d75f87768e chore: drop extraneous newline 2026-07-04 08:48:03 +09:00
aclist
5bb4271d36 feat: log-level redaction filter 2026-07-04 08:47:34 +09:00
aclist
6c86bff869 fix: drop branch from diagnostic log 2026-07-04 08:46:54 +09:00
aclist
92ba7c2267 chore: drop unused imports, vars 2026-07-04 08:42:53 +09:00
12 changed files with 176 additions and 44 deletions

69
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,69 @@
# Introduction
Thank you for your interest in DZGUI!
This guide goes over development conventions and best practices for contributors.
If you are a developer, you can skip to the end.
# Requesting help
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
(issue tracker)[https://github.com/aclist/dztui/issues] under the
`troubleshooting` tag.
# How can I help the project?
There are several ways to help this project.
1. Report bugs that you find
2. Request features that you would like
3. Contribute features and fixes to the codebase
4. Contribute documentation to the project
Before making any contribution, please read the `CODE_OF_CONDUCT.md` and act
accordingly.
## Submitting a ticket
Navigate to the GitHub (issue tracker)[https://github.com/aclist/dztui/issues].
From there, follow the onscreen prompts. You will be asked questions such as:
- What version are you using?
- What distribution are you using?
- What is the issue that you found?
- How can we reproduce the issue?
You can also attach screenshots, logs, or other data that can help us.
## Requesting a feature
You can also request features via the same issue tracker. It is good practice to
first search for your idea to see if a similar one has already been posted.
If not, open a ticket where you describe your feature and its possible benefits.
Please note that this is a community project, so it takes time for us to develop
features. Putting in a feature request does not mean that it will be implemented,
but we will do our best to support as many cool ideas as possible.
## Contributing code or documentation
If you would like to assist with an issue on the issue tracker or
contribute a new change, please follow the guidelines below.
Fork this repository and check out the code up to the `dzgui7` (development) branch.
The following naming conventions apply for PRs:
- fix/<your-fix> - patch/hotfix branches
- feat/<your-feature> - feature branches
- doc/<your-doc-branch> - documentation branches
- infra/<your-infra-branch> - infrastructure branches
Implement your changes and test them locally. If they work, you may
open a merge request, and then we review your changes. If everything is OK, it
will be merged to `dzgui7`. After sufficient changes are consolidated into a new release,
this branch will be tagged to a certain point in time and a binary release published.
It is recommended to follow
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), as this
integrates well with tooling and helps the project review your code.
Please see DEVELOPERS.md for further details.

View File

@ -24,13 +24,8 @@ Please refer to the documentation for installation and setup instructions:
Geolocation records from [DB-IP](https://db-ip.com) under [CC 4.0 license](https://creativecommons.org/licenses/by/4.0/) Geolocation records from [DB-IP](https://db-ip.com) under [CC 4.0 license](https://creativecommons.org/licenses/by/4.0/)
This tool uses [python-a2s](https://github.com/Yepoleb/python-a2s) and [dayzquery](https://github.com/Yepoleb/dayzquery) as submodules; licenses for these submodules can be found in the LICENSES file Executable versions of DZGUI published as release binaries ship with various runtime dependencies and the Python interpreter built in.
of the project root. Users wishing to review the licenses to these components can inspect the `LICENSE` file located in the release tarball.
Both the geolocation records and submodules listed above are not shipped with the source code, but are retrieved and assembled at runtime.
Finally, executable versions of DZGUI shipped as release binaries are thin wrappers around the Python interpreter, and also retrieve and assemble the above dependencies at runtime on the end-user's
machine, rather than using pre-compiled source code. Users wishing to review these dependencies can inspect the 'pyproject.toml' manifest in the project root.
## Disclaimer ## Disclaimer

View File

@ -4,7 +4,7 @@ import shutil
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER from dzgui.const.constants import APP_NAME
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
@ -21,6 +21,7 @@ from dzgui.strings import boot
# from dzgui.util.map_count import get_map_count # from dzgui.util.map_count import get_map_count
from dzgui.util.deck import is_steam_deck, is_game_mode from dzgui.util.deck import is_steam_deck, is_game_mode
from dzgui.util.localize import set_locale from dzgui.util.localize import set_locale
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
from dzgui.util.strings import init from dzgui.util.strings import init
from dzgui.views.base import App from dzgui.views.base import App
@ -40,13 +41,19 @@ def make_parents(path: "Path") -> None:
def setup_logger(log_path: "Path") -> None: def setup_logger(log_path: "Path") -> None:
# TODO: put in consts?
_format = ( _format = (
"%(asctime)s%(levelname)s%(filename)s::%(funcName)s::%(lineno)s%(message)s" "%(asctime)s%(levelname)s%(filename)s::%(funcName)s::%(lineno)s%(message)s"
) )
fh = logging.FileHandler(log_path) fh = logging.FileHandler(log_path)
formatter = logging.Formatter(_format) formatter = logging.Formatter(_format)
fh.setFormatter(formatter) fh.setFormatter(formatter)
fh.setLevel(logging.DEBUG) fh.setLevel(logging.DEBUG)
_filter = RedactionFilter(patterns=REDACTION_PATTERNS)
fh.addFilter(_filter)
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
logger.addHandler(fh) logger.addHandler(fh)

View File

@ -2,7 +2,6 @@ from dataclasses import dataclass
from typing import Any, Sequence, TYPE_CHECKING from typing import Any, Sequence, TYPE_CHECKING
from dzgui.const.enum import HELP_MENU_ROWS from dzgui.const.enum import HELP_MENU_ROWS
from dzgui.util.redact import redact_log
from dzgui.util.strings import delimiter from dzgui.util.strings import delimiter
import gi import gi
@ -134,9 +133,7 @@ class ModelFactory:
with open(path, "r") as f: with open(path, "r") as f:
lines = [line.split(delimiter) for line in f.read().splitlines()] lines = [line.split(delimiter) for line in f.read().splitlines()]
for record in lines: for record in lines:
# NOTE: strips PII and API keys store.append(record)
clean = redact_log(record)
store.append(clean)
return store return store
def new_model_from_class(self, cls: type) -> FastInsertListStore: def new_model_from_class(self, cls: type) -> FastInsertListStore:

View File

@ -1,3 +1,4 @@
import logging
import os import os
import platform import platform
@ -9,7 +10,9 @@ from dzgui.const.constants import APP_NAME
from dzgui.const.enum import Preferences from dzgui.const.enum import Preferences
from dzgui.config.query import lookup from dzgui.config.query import lookup
from dzgui.init.prefix import get_version from dzgui.init.prefix import get_version
from dzgui.util.redact import redact from dzgui.util.redact import redact_home
logger = logging.getLogger(APP_NAME)
def get_cpu_model() -> str: def get_cpu_model() -> str:
@ -41,12 +44,16 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
# TODO: test availability on other distros # TODO: test availability on other distros
date = datetime.now().isoformat() date = datetime.now().isoformat()
try:
distro = platform.freedesktop_os_release()["ID_LIKE"] distro = platform.freedesktop_os_release()["ID_LIKE"]
except Exception as e:
logger.warn(e)
distro = "Unknown"
kernel = os.uname().release kernel = os.uname().release
cpu = get_cpu_model() cpu = get_cpu_model()
version = get_version() version = get_version()
branch = lookup(config, Preferences.BRANCH)
debug = lookup(config, Preferences.DEBUG) debug = lookup(config, Preferences.DEBUG)
install = lookup(config, Preferences.INSTALL) install = lookup(config, Preferences.INSTALL)
default = lookup(config, Preferences.DEFAULT) default = lookup(config, Preferences.DEFAULT)
@ -54,8 +61,8 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
steam_path = Path(default) steam_path = Path(default)
workshop_path = get_local_mod_path(steam_path) workshop_path = get_local_mod_path(steam_path)
steam_redacted = redact(default) steam_redacted = redact_home(default)
workshop_redacted = redact(str(workshop_path)) workshop_redacted = redact_home(str(workshop_path))
mods = get_local_mod_ids(steam_path) mods = get_local_mod_ids(steam_path)
mods_pretty = print_mods(mods) mods_pretty = print_mods(mods)
@ -65,7 +72,7 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
# FIXME: extraneous newlines in lists of mods # FIXME: extraneous newlines in lists of mods
template = f"""\ template = f"""\
{APP_NAME} version {version} ({branch}) {APP_NAME} version {version}
Date: {date} Date: {date}
=============================== ===============================
Distribution: {distro} Distribution: {distro}

View File

@ -1,20 +1,29 @@
import logging
import re import re
from typing import Literal
def redact(text: str) -> str: api_filter = r"(.*&key=)([^&]*)(.*)"
r = r"(/home/)([^/])*" home_filter = r"(/home/)([^\s'\/]*)(.*)"
cleaned = re.sub(r, r"/home/REDACTED", text) REDACTED = r"\1REDACTED\3"
REDACTION_PATTERNS = [api_filter, home_filter]
def redact_home(text: str) -> str:
pat = re.compile(home_filter)
cleaned = pat.sub(REDACTED, text)
return cleaned return cleaned
def redact_log(record: list) -> list[str]:
""" class RedactionFilter(logging.Filter):
requests library includes Steam API key in URL params def __init__(self, patterns: list[str] | None = None) -> None:
""" super().__init__()
clean = [] self._patterns = [re.compile(pat) for pat in (patterns or [])]
for item in record:
if "&key=" in item: def filter(self, record: logging.LogRecord) -> Literal[True]:
pat = r"(.*&key=)(\S+)(.*)" for pattern in self._patterns:
scrubbed = re.sub(pat, r"\1REDACTED\3", item) try:
clean.append(scrubbed) record.msg = pattern.sub(REDACTED, record.msg)
else: except TypeError:
clean.append(item) exception_text = f"{type(record.msg).__name__}: {record.msg}"
return clean record.msg = pattern.sub(REDACTED, exception_text)
return True

View File

@ -288,6 +288,7 @@ class Options(Gtk.Box):
def _on_api_change_failed(self, emitter: "Emitter") -> None: def _on_api_change_failed(self, emitter: "Emitter") -> None:
self.old_entry.set_text(self.old_text) self.old_entry.set_text(self.old_text)
# TODO: use popover
dialog = ExceptionDialog(self.controller, errors.api_validation_error) dialog = ExceptionDialog(self.controller, errors.api_validation_error)
dialog.run() dialog.run()

View File

@ -110,6 +110,7 @@ markers = [
"mods: tests mod metadata/link creation", "mods: tests mod metadata/link creation",
"pefile: validate PE files", "pefile: validate PE files",
"post_install: requires a completed installation", "post_install: requires a completed installation",
"redact: log redaction mechanisms",
"slow: long-running tests", "slow: long-running tests",
"webtest: checks remote endpoints" "webtest: checks remote endpoints"
] ]

View File

@ -4,7 +4,6 @@ import os
from dzgui.app_init import copy_bare_configs from dzgui.app_init import copy_bare_configs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from pathlib import Path
CONF_STRING = "DZGUI_CONF\n" CONF_STRING = "DZGUI_CONF\n"
@ -27,8 +26,7 @@ def state_files():
@pytest.fixture @pytest.fixture
def xdg_paths(): def xdg_paths(monkeypatch):
paths = []
routes = { routes = {
"XDG_CONFIG_HOME": "", "XDG_CONFIG_HOME": "",
"XDG_STATE_HOME": "", "XDG_STATE_HOME": "",
@ -39,7 +37,7 @@ def xdg_paths():
tmp = tempfile.TemporaryDirectory(delete=False) tmp = tempfile.TemporaryDirectory(delete=False)
routes[route] = tmp.name routes[route] = tmp.name
for k, v in routes.items(): for k, v in routes.items():
os.environ[k] = v monkeypatch.setenv(k, v)
env = get_xdg_paths() env = get_xdg_paths()
return parse_filepaths(env) return parse_filepaths(env)

View File

@ -95,8 +95,3 @@ def test_missing_values(unset_values):
j = convert.rc2json(unset_values) j = convert.rc2json(unset_values)
j = json.loads(j) j = json.loads(j)
assert j.keys() == config_boilerplate.keys() assert j.keys() == config_boilerplate.keys()
# TODO: test that when a config file is created from scratch, it contains all values

View File

@ -0,0 +1,54 @@
import logging
import pytest
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
class RecordsListHandler(logging.Handler):
def __init__(self) -> None:
super().__init__()
self.records_list = []
def emit(self, record: logging.LogRecord) -> None:
self.records_list.append(record)
def pop(self) -> None:
return self.records_list[-1].msg
@pytest.mark.redact
@pytest.mark.parametrize(
"log_error, expect",
[
("/home/SENSITIVE_USERNAME/subdir", "/home/REDACTED/subdir"),
(
"https://url.com/?api&key=SENSITIVE_KEY&results=10",
"https://url.com/?api&key=REDACTED&results=10",
),
(
"https://url.com/?api&key=SENSITIVE_KEY",
"https://url.com/?api&key=REDACTED",
),
(
"Error in directory: '/home/SENSITIVE_USERNAME/'",
"Error in directory: '/home/REDACTED/'",
),
(
"Error in directory: '/home/SENSITIVE_USERNAME'",
"Error in directory: '/home/REDACTED'",
),
],
)
def test_log_redaction(log_error: str, expect: str) -> None:
logger = logging.getLogger("TEST")
handler = RecordsListHandler()
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
_filter = RedactionFilter(patterns=REDACTION_PATTERNS)
logger.addFilter(_filter)
logger.critical(log_error)
redacted = handler.pop()
assert expect == redacted

View File

@ -20,7 +20,6 @@ def test_steam(config):
key = config["steam_api"] key = config["steam_api"]
assert probe.test_steam_api(key) assert probe.test_steam_api(key)
def test_bm(config): def test_bm(config):
key = config["bm_api"] key = config["bm_api"]
assert probe.test_bm_api(key) assert probe.test_bm_api(key)