| """ | |
| ID validators for path-building in the scene pipeline. | |
| Lives in its own leaf module so the validators can be imported (and | |
| tested) without dragging in the rest of the pipeline (which pulls in | |
| ffmpeg helpers, imagehash, transformers, etc.). | |
| """ | |
| from __future__ import annotations | |
| import re | |
| # JW.org natural keys are letters/digits/underscore/hyphen/period; language | |
| # codes are letters only. Anything outside these regexes is rejected before | |
| # os.path.join sees it, blocking traversal via ``..`` or absolute paths. | |
| # | |
| # The ID pattern additionally requires the first character to be alphanumeric | |
| # or underscore so a sneaky ``..`` or ``-foo`` is rejected even though dot | |
| # and hyphen are allowed mid-identifier. | |
| _SAFE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$") | |
| _SAFE_LANG_PATTERN = re.compile(r"^[A-Za-z]{1,8}$") | |
| def require_safe_id(name: str, value: str) -> str: | |
| if ( | |
| not isinstance(value, str) | |
| or not _SAFE_ID_PATTERN.match(value) | |
| or ".." in value | |
| ): | |
| raise ValueError( | |
| f"Refusing unsafe {name}={value!r}: must match {_SAFE_ID_PATTERN.pattern} " | |
| f"and contain no '..'" | |
| ) | |
| return value | |
| def require_safe_language(value: str) -> str: | |
| if not isinstance(value, str) or not _SAFE_LANG_PATTERN.match(value): | |
| raise ValueError( | |
| f"Refusing unsafe language={value!r}: must match {_SAFE_LANG_PATTERN.pattern}" | |
| ) | |
| return value | |