mirror of
https://github.com/rommapp/romm.git
synced 2026-06-27 22:35:57 +00:00
Tolerate forward-compat values and malformed YAML in config loader
Sample configs shipped with newer releases can reference media types or options unknown to older versions; previously the loader called sys.exit(3) and refused to boot. Now drop unknown scan.media entries, fall back to defaults for unknown gamelist media thumbnail/image values, and recover from YAML parse errors with a clear critical log instead of a traceback. Type-mismatch validation still hard-fails since those are real misconfigurations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,8 @@ lint:
|
||||
- frontend/src/__generated__/**
|
||||
- docker/Dockerfile
|
||||
- docker/nginx/js/**
|
||||
# Intentionally malformed YAML, used as a test fixture
|
||||
- backend/tests/config/fixtures/config/malformed_config.yml
|
||||
files:
|
||||
- name: vue
|
||||
extensions: [vue]
|
||||
|
||||
@@ -174,7 +174,7 @@ class ConfigManager:
|
||||
# Check if the config file is mounted
|
||||
with open(self.config_file, "r") as cf:
|
||||
self._config_file_mounted = True
|
||||
self._raw_config = yaml.load(cf, Loader=SafeLoader) or {}
|
||||
self._raw_config = self._safe_load_yaml(cf)
|
||||
|
||||
# Also check if the config file is writable
|
||||
self._config_file_writable = os.access(self.config_file, os.W_OK)
|
||||
@@ -189,6 +189,19 @@ class ConfigManager:
|
||||
self._parse_config()
|
||||
self._validate_config()
|
||||
|
||||
def _safe_load_yaml(self, cf) -> dict:
|
||||
"""Load YAML, falling back to an empty config on syntax errors so the
|
||||
app can still boot with defaults rather than crashing."""
|
||||
try:
|
||||
return yaml.load(cf, Loader=SafeLoader) or {}
|
||||
except yaml.YAMLError as exc:
|
||||
log.critical(
|
||||
f"Failed to parse {hl(self.config_file, BLUE)}: {exc}. "
|
||||
"Falling back to default configuration, fix the YAML "
|
||||
"syntax to apply your settings."
|
||||
)
|
||||
return {}
|
||||
|
||||
def _create_missing_config_file(self) -> None:
|
||||
log.warning(
|
||||
f"Config file not found, creating an empty config at {hl(self.config_file, BLUE)}"
|
||||
@@ -633,12 +646,19 @@ class ConfigManager:
|
||||
log.critical("Invalid config.yml: scan.media must be a list")
|
||||
sys.exit(3)
|
||||
|
||||
for media in self.config.SCAN_MEDIA:
|
||||
if media not in MetadataMediaType:
|
||||
log.critical(
|
||||
f"Invalid config.yml: scan.media.{media} is not a valid media type"
|
||||
)
|
||||
sys.exit(3)
|
||||
# Drop unknown media types rather than exiting — a newer release may
|
||||
# ship sample configs referencing media types this version doesn't know.
|
||||
unknown_media = [
|
||||
m for m in self.config.SCAN_MEDIA if m not in MetadataMediaType
|
||||
]
|
||||
if unknown_media:
|
||||
log.warning(
|
||||
f"Ignoring unknown values in scan.media: {unknown_media}. "
|
||||
"These may be from a newer RomM version — update to use them."
|
||||
)
|
||||
self.config.SCAN_MEDIA = [
|
||||
m for m in self.config.SCAN_MEDIA if m in MetadataMediaType
|
||||
]
|
||||
|
||||
valid_thumbnail_options = {
|
||||
MetadataMediaType.BOX2D,
|
||||
@@ -652,10 +672,12 @@ class ConfigManager:
|
||||
)
|
||||
sys.exit(3)
|
||||
if self.config.GAMELIST_MEDIA_THUMBNAIL not in valid_thumbnail_options:
|
||||
log.critical(
|
||||
f"Invalid config.yml: scan.gamelist.media.thumbnail must be one of {valid_thumbnail_options}"
|
||||
log.warning(
|
||||
f"Unknown scan.gamelist.media.thumbnail value "
|
||||
f"{self.config.GAMELIST_MEDIA_THUMBNAIL!r}; falling back to "
|
||||
f"{MetadataMediaType.BOX2D.value!r}. Valid options: {sorted(o.value for o in valid_thumbnail_options)}."
|
||||
)
|
||||
sys.exit(3)
|
||||
self.config.GAMELIST_MEDIA_THUMBNAIL = MetadataMediaType.BOX2D
|
||||
|
||||
valid_image_options = {
|
||||
MetadataMediaType.TITLE_SCREEN,
|
||||
@@ -671,15 +693,17 @@ class ConfigManager:
|
||||
sys.exit(3)
|
||||
|
||||
if self.config.GAMELIST_MEDIA_IMAGE not in valid_image_options:
|
||||
log.critical(
|
||||
f"Invalid config.yml: scan.gamelist.media.image must be one of {valid_image_options}"
|
||||
log.warning(
|
||||
f"Unknown scan.gamelist.media.image value "
|
||||
f"{self.config.GAMELIST_MEDIA_IMAGE!r}; falling back to "
|
||||
f"{MetadataMediaType.SCREENSHOT.value!r}. Valid options: {sorted(o.value for o in valid_image_options)}."
|
||||
)
|
||||
sys.exit(3)
|
||||
self.config.GAMELIST_MEDIA_IMAGE = MetadataMediaType.SCREENSHOT
|
||||
|
||||
def get_config(self) -> Config:
|
||||
try:
|
||||
with open(self.config_file, "r") as config_file:
|
||||
self._raw_config = yaml.load(config_file, Loader=SafeLoader) or {}
|
||||
self._raw_config = self._safe_load_yaml(config_file)
|
||||
except FileNotFoundError:
|
||||
log.debug("Config file not found!")
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Config file containing values that a future RomM release might ship but
|
||||
# which this version doesn't yet know about. Used to verify the loader
|
||||
# tolerates unknown enum/option values instead of refusing to boot.
|
||||
|
||||
scan:
|
||||
media:
|
||||
- box2d
|
||||
- some_future_media_type
|
||||
- screenshot
|
||||
gamelist:
|
||||
media:
|
||||
thumbnail: "some_future_thumbnail"
|
||||
image: "some_future_image"
|
||||
|
||||
future_top_level_section:
|
||||
enabled: true
|
||||
setting: "value"
|
||||
@@ -0,0 +1,9 @@
|
||||
# Intentionally malformed YAML — used to verify the loader falls back to
|
||||
# defaults rather than crashing on a parse error.
|
||||
|
||||
scan:
|
||||
media:
|
||||
- box2d
|
||||
- screenshot # bad indentation, will fail to parse
|
||||
gamelist:
|
||||
export: true
|
||||
@@ -125,3 +125,38 @@ def test_missing_config_file_is_created(tmp_path):
|
||||
assert config_file.read_text() == ""
|
||||
assert loader.config.CONFIG_FILE_MOUNTED
|
||||
assert loader.config.CONFIG_FILE_WRITABLE
|
||||
|
||||
|
||||
def test_forward_compat_unknown_values_are_tolerated():
|
||||
"""A newer release may ship sample configs that reference media types
|
||||
this version doesn't yet recognize. The loader should drop unknowns and
|
||||
fall back to defaults rather than exiting."""
|
||||
loader = ConfigManager(
|
||||
os.path.join(
|
||||
Path(__file__).resolve().parent,
|
||||
"fixtures",
|
||||
"config/forward_compat_config.yml",
|
||||
)
|
||||
)
|
||||
|
||||
# Unknown entries in scan.media are filtered out; known ones survive.
|
||||
assert loader.config.SCAN_MEDIA == ["box2d", "screenshot"]
|
||||
# Unknown thumbnail/image values fall back to their defaults.
|
||||
assert loader.config.GAMELIST_MEDIA_THUMBNAIL == "box2d"
|
||||
assert loader.config.GAMELIST_MEDIA_IMAGE == "screenshot"
|
||||
|
||||
|
||||
def test_malformed_yaml_falls_back_to_defaults():
|
||||
"""A YAML parse error should log critically and leave the app on
|
||||
defaults, not crash."""
|
||||
loader = ConfigManager(
|
||||
os.path.join(
|
||||
Path(__file__).resolve().parent,
|
||||
"fixtures",
|
||||
"config/malformed_config.yml",
|
||||
)
|
||||
)
|
||||
|
||||
assert loader.config.ROMS_FOLDER_NAME == "roms"
|
||||
assert loader.config.FIRMWARE_FOLDER_NAME == "bios"
|
||||
assert loader.config.SCAN_MEDIA == ["box2d", "screenshot", "manual"]
|
||||
|
||||
Reference in New Issue
Block a user