aegis_gui.utilities.utilities

 1import dash
 2import re
 3import yaml
 4
 5from aegis_sim.utilities.container import Container
 6from aegis_gui.guisettings.GuiSettings import gui_settings
 7
 8
 9# Strict whitelist for sim names. Filesystem-safe characters only — no slashes,
10# no leading dots (would hide files), no whitespace, capped length.
11_SIM_NAME_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$")
12
13
14class UnsafeSimNameError(ValueError):
15    """Raised when a sim name fails the strict whitelist or escapes sim_dir."""
16
17
18def safe_sim_name(name) -> str:
19    """Validate `name` as a safe simulation identifier.
20
21    Accepts letters, digits, underscore, dash, and dot (not leading), max 200
22    chars. Rejects anything else — including slashes, traversal sequences,
23    whitespace, null bytes. Returns the validated string.
24    """
25    if not isinstance(name, str):
26        raise UnsafeSimNameError(f"sim name must be str, got {type(name).__name__}")
27    if not _SIM_NAME_PATTERN.fullmatch(name):
28        raise UnsafeSimNameError(f"rejected sim name: {name!r}")
29    return name
30
31
32def safe_sim_path(name) -> "pathlib.Path":
33    """Return the validated directory path for a sim, guaranteed to be inside
34    `gui_settings.sim_dir`. Raises UnsafeSimNameError if the resolved path
35    escapes the sim directory (defense in depth even after the regex check).
36    """
37    import pathlib
38
39    name = safe_sim_name(name)
40    base = pathlib.Path(gui_settings.sim_dir).resolve()
41    candidate = (base / name).resolve()
42    try:
43        candidate.relative_to(base)
44    except ValueError:
45        raise UnsafeSimNameError(f"sim path escapes sim_dir: {name!r}")
46    return candidate
47
48
49def read_yml(path):
50    with open(path, "r") as f:
51        return yaml.safe_load(f)
52
53
54def get_container(filename):
55    safe_sim_name(filename)
56    return Container(gui_settings.sim_dir / filename)
57
58
59def get_config_path(filename):
60    safe_sim_name(filename)
61    return gui_settings.sim_dir / f"{filename}.yml"
62
63
64def get_sim_paths(sim_dir=None, sort=True):
65    if sim_dir is None:
66        sim_dir = gui_settings.sim_dir
67    paths = [p for p in sim_dir.iterdir() if p.is_dir()]
68    if sort:
69        paths = sorted(paths, key=lambda path: path.name)
70    return paths
71
72
73def get_sims():
74    return [p.stem for p in get_sim_paths()]
75
76
77def sim_exists(filename: str) -> bool:
78    paths = get_sim_paths()
79    return any(path.stem == filename for path in paths)
80
81
82def get_icon(icon_name):
83    return dash.html.Img(
84        src=f"/aegis/assets/icons/{icon_name}.svg",
85        width="16",
86        height="16",
87    )
class UnsafeSimNameError(builtins.ValueError):
15class UnsafeSimNameError(ValueError):
16    """Raised when a sim name fails the strict whitelist or escapes sim_dir."""

Raised when a sim name fails the strict whitelist or escapes sim_dir.

def safe_sim_name(name) -> str:
19def safe_sim_name(name) -> str:
20    """Validate `name` as a safe simulation identifier.
21
22    Accepts letters, digits, underscore, dash, and dot (not leading), max 200
23    chars. Rejects anything else — including slashes, traversal sequences,
24    whitespace, null bytes. Returns the validated string.
25    """
26    if not isinstance(name, str):
27        raise UnsafeSimNameError(f"sim name must be str, got {type(name).__name__}")
28    if not _SIM_NAME_PATTERN.fullmatch(name):
29        raise UnsafeSimNameError(f"rejected sim name: {name!r}")
30    return name

Validate name as a safe simulation identifier.

Accepts letters, digits, underscore, dash, and dot (not leading), max 200 chars. Rejects anything else — including slashes, traversal sequences, whitespace, null bytes. Returns the validated string.

def safe_sim_path(name) -> pathlib.Path:
33def safe_sim_path(name) -> "pathlib.Path":
34    """Return the validated directory path for a sim, guaranteed to be inside
35    `gui_settings.sim_dir`. Raises UnsafeSimNameError if the resolved path
36    escapes the sim directory (defense in depth even after the regex check).
37    """
38    import pathlib
39
40    name = safe_sim_name(name)
41    base = pathlib.Path(gui_settings.sim_dir).resolve()
42    candidate = (base / name).resolve()
43    try:
44        candidate.relative_to(base)
45    except ValueError:
46        raise UnsafeSimNameError(f"sim path escapes sim_dir: {name!r}")
47    return candidate

Return the validated directory path for a sim, guaranteed to be inside gui_settings.sim_dir. Raises UnsafeSimNameError if the resolved path escapes the sim directory (defense in depth even after the regex check).

def read_yml(path):
50def read_yml(path):
51    with open(path, "r") as f:
52        return yaml.safe_load(f)
def get_container(filename):
55def get_container(filename):
56    safe_sim_name(filename)
57    return Container(gui_settings.sim_dir / filename)
def get_config_path(filename):
60def get_config_path(filename):
61    safe_sim_name(filename)
62    return gui_settings.sim_dir / f"{filename}.yml"
def get_sim_paths(sim_dir=None, sort=True):
65def get_sim_paths(sim_dir=None, sort=True):
66    if sim_dir is None:
67        sim_dir = gui_settings.sim_dir
68    paths = [p for p in sim_dir.iterdir() if p.is_dir()]
69    if sort:
70        paths = sorted(paths, key=lambda path: path.name)
71    return paths
def get_sims():
74def get_sims():
75    return [p.stem for p in get_sim_paths()]
def sim_exists(filename: str) -> bool:
78def sim_exists(filename: str) -> bool:
79    paths = get_sim_paths()
80    return any(path.stem == filename for path in paths)
def get_icon(icon_name):
83def get_icon(icon_name):
84    return dash.html.Img(
85        src=f"/aegis/assets/icons/{icon_name}.svg",
86        width="16",
87        height="16",
88    )