feat: refuse a citation no repository can hold

kenji-nx cited tmp/es-de/ANDROID.md:470-474, a path from the machine of
whoever profiled it. No revision of any declared repository holds it, so
profile_sync could only report it missing, every pass, forever, and no
amount of reading would ever settle it.

validate_schemas now refuses a scratch directory, an absolute path, a
Windows drive path and one climbing out of the tree, and names the
offending citation rather than the scalar that carries it. Offline, so it
runs on every push and every pull request rather than waiting for a
network pass.

The ES-DE citation reads as external now, which is what it always was.
kenji-nx is at 37 refs, all anchored: the three changed blocks were var
giving way to explicit types.
This commit is contained in:
Abdessamad Derraz committed 2026-09-04 18:26:27 +02:00
1 parent d124f6c516
commit 6f27a296f7
3 files changed
+88 -8

No files matched your search

+8 -8
View File
@@ -5,7 +5,7 @@ source: "https://git.ryujinx.app/projects/Kenji-NX"
source_branch: libryujinx_bionic source_branch: libryujinx_bionic
upstream: "https://git.ryujinx.app/projects/Ryubing" upstream: "https://git.ryujinx.app/projects/Ryubing"
profiled_date: "2026-08-11" profiled_date: "2026-08-11"
source_commit: "8d7d8f344cb01257c5bc59bb02a74bb89ab9bb5f" source_commit: "53d32a761e61dc8e9b5c0744199a890fc8d934d5"
core_version: "2.1.0-pr.2" core_version: "2.1.0-pr.2"
display_name: "Nintendo - Switch (Kenji-NX)" display_name: "Nintendo - Switch (Kenji-NX)"
cores: cores:
@@ -35,7 +35,7 @@ notes: |
~/.switch fallback that ReloadKeySet consults in UserProfile mode never fires ~/.switch fallback that ReloadKeySet consults in UserProfile mode never fires
there. games, profiles and system are created under the root on every start. there. games, profiles and system are created under the root on every start.
ref: src/Ryujinx.Common/Configuration/AppDataManager.cs:11-17,47,50-104,223-230, ref: src/Ryujinx.Common/Configuration/AppDataManager.cs:11-17,47,50-104,223-230,
src/LibKenjinx/LibKenjinx.cs:54-95, src/LibKenjinx/LibKenjinx.cs:59-100,
src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt:264-298 src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/MainActivity.kt:264-298
Firmware is installed rather than placed. ContentManager takes a directory, a Firmware is installed rather than placed. ContentManager takes a directory, a
@@ -46,7 +46,7 @@ notes: |
homebrew NRO goes through ProcessLoader without touching the key set, and the homebrew NRO goes through ProcessLoader without touching the key set, and the
failure is deferred to the first service that needs a system archive. failure is deferred to the first service that needs a system archive.
ref: src/Ryujinx.HLE/FileSystem/ContentManager.cs:431-498,675-749,893-903, ref: src/Ryujinx.HLE/FileSystem/ContentManager.cs:431-498,675-749,893-903,
src/LibKenjinx/Android/JniExportedMethods.cs:459-495, src/LibKenjinx/Android/JniExportedMethods.cs:462-498,
src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs:189-205 src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs:189-205
Two read paths belong to the desktop build alone, because the Android build Two read paths belong to the desktop build alone, because the Android build
@@ -59,7 +59,7 @@ notes: |
key_retail.bin, while the Android entry buffers the raw tag in key_retail.bin, while the Android entry buffers the raw tag in
KenjinxAmiiboShim and INfp reads the figure id straight out of offset 0x1DC. KenjinxAmiiboShim and INfp reads the figure id straight out of offset 0x1DC.
ref: src/LibKenjinx/LibKenjinx.csproj:11-13, ref: src/LibKenjinx/LibKenjinx.csproj:11-13,
src/LibKenjinx/Android/JniExportedMethods.cs:347-348,1046-1060, src/LibKenjinx/Android/JniExportedMethods.cs:350-351,1049-1063,
src/LibKenjinx/LibKenjinx.Input.cs:28-39, src/LibKenjinx/LibKenjinx.Input.cs:28-39,
src/Ryujinx.HLE/Kenjinx/AmiiboBridge.cs:10-44, src/Ryujinx.HLE/Kenjinx/AmiiboBridge.cs:10-44,
src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/KenjinxAmiiboShim.cs:15-41, src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/NfpManager/KenjinxAmiiboShim.cs:15-41,
@@ -79,7 +79,7 @@ notes: |
under the Nintendo notice of 2026-02-12, and both the source and the builds under the Nintendo notice of 2026-02-12, and both the source and the builds
are served by the Forgejo instance, which is where ES-DE points as well. are served by the Forgejo instance, which is where ES-DE points as well.
ref: https://github.com/github/dmca/blob/master/2026/02/2026-02-12-nintendo.md, ref: https://github.com/github/dmca/blob/master/2026/02/2026-02-12-nintendo.md,
tmp/es-de/ANDROID.md:470-474 es-de ANDROID.md:470-474
files: files:
- name: prod.keys - name: prod.keys
@@ -142,7 +142,7 @@ files:
of title 0100000000000809. The NCAs are then registered under of title 0100000000000809. The NCAs are then registered under
bis/system/Contents/registered and read from there, never as loose files. bis/system/Contents/registered and read from there, never as loose files.
No digest of the package itself is compared against anything. No digest of the package itself is compared against anything.
source_ref: "src/Ryujinx.HLE/FileSystem/ContentManager.cs:431-498,675-749,751-1045, src/LibKenjinx/Android/JniExportedMethods.cs:459-495, src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/SettingsViewModel.kt:308-364" source_ref: "src/Ryujinx.HLE/FileSystem/ContentManager.cs:431-498, 675-749, 751-1045, src/LibKenjinx/Android/JniExportedMethods.cs:462-498, src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/SettingsViewModel.kt:308-364"
- name: key_retail.bin - name: key_retail.bin
required: false required: false
@@ -183,7 +183,7 @@ files:
Android slots read the bytes through the content resolver and buffer them Android slots read the bytes through the content resolver and buffer them
for the NFP service, which takes the figure id from offset 0x1DC without for the NFP service, which takes the figure id from offset 0x1DC without
decrypting anything. decrypting anything.
source_ref: "src/Ryujinx.HLE/HOS/Horizon.cs:355-363, src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs:22-44,151-283, src/LibKenjinx/LibKenjinx.cs:700-729, src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/GameViews.kt:328-349" source_ref: "src/Ryujinx.HLE/HOS/Horizon.cs:355-363, src/Ryujinx.HLE/HOS/Services/Nfc/AmiiboDecryption/AmiiboBinReader.cs:22-44, 151-283, src/LibKenjinx/LibKenjinx.cs:705-734, src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/views/GameViews.kt:328-349"
- name: "<CaCertificateId>.der" - name: "<CaCertificateId>.der"
required: false required: false
@@ -211,7 +211,7 @@ files:
because the external filesystem cannot carry an executable mapping, loaded because the external filesystem cannot carry an executable mapping, loaded
through the rootless Adreno driver loader, and the resulting handle is through the rootless Adreno driver loader, and the resulting handle is
passed to the Vulkan loader. An empty selection uses the system loader. passed to the Vulkan loader. An empty selection uses the system loader.
source_ref: "src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/VulkanDriverViewModel.kt:13-167, src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/MainViewModel.kt:202-243, src/LibKenjinx/Android/JniExportedMethods.cs:532-555, src/LibKenjinx/VulkanLoader.cs:27-29" source_ref: "src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/VulkanDriverViewModel.kt:13-167, src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/viewmodels/MainViewModel.kt:202-243, src/LibKenjinx/Android/JniExportedMethods.cs:535-558, src/LibKenjinx/VulkanLoader.cs:27-29"
# System archives read out of the installed firmware, never as loose files. # System archives read out of the installed firmware, never as loose files.
firmware_titles: firmware_titles:
+33
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import argparse import argparse
import json import json
import re
import sys import sys
import zipfile import zipfile
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
@@ -55,6 +56,7 @@ def _validate_yaml_directory(
out.append(f"{path.relative_to(ROOT)}: {exc}") out.append(f"{path.relative_to(ROOT)}: {exc}")
continue continue
out.extend(_errors(validator, data, str(path.relative_to(ROOT)))) out.extend(_errors(validator, data, str(path.relative_to(ROOT))))
out.extend(_unreachable_citations(path, data))
return out return out
@@ -132,6 +134,37 @@ def _scan_pack_manifests(dist: Path) -> list[str]:
return out return out
_UNREACHABLE_REF = re.compile(
r"(?:^|[\s,;(])(?:tmp/|/|[A-Za-z]:[\\/]|\.{1,2}/)[\w.\\/+-]*[\w+-]:\d+(?:-\d+)?"
)
def _unreachable_citations(path: Path, document: object) -> list[str]:
"""Refs naming a place no repository can hold.
A citation is read against a revision of a declared repository, so a
scratch directory, an absolute path or one climbing out of the tree can
never resolve and never will. kenji-nx carried tmp/es-de/ANDROID.md:470,
a path from whoever profiled it, which profile_sync could only report
missing forever.
"""
out: list[str] = []
stack: list[object] = [document]
while stack:
node = stack.pop()
if isinstance(node, dict):
stack.extend(node.values())
elif isinstance(node, list):
stack.extend(node)
elif isinstance(node, str):
for match in _UNREACHABLE_REF.finditer(node):
out.append(
f"{path.relative_to(ROOT)}: citation names a path outside "
f"any repository: {match.group().strip(' ,;(')}"
)
return out
def _semantic_envelope_checks(path: Path, document: dict) -> list[str]: def _semantic_envelope_checks(path: Path, document: dict) -> list[str]:
if document.get("count") != len(document.get("items", [])): if document.get("count") != len(document.get("items", [])):
return [f"{path.relative_to(ROOT)}: count does not equal len(items)"] return [f"{path.relative_to(ROOT)}: count does not equal len(items)"]
+47
View File
@@ -184,6 +184,53 @@ class WorkflowRegressions(unittest.TestCase):
) )
class UnreachableCitationRegressions(unittest.TestCase):
"""A citation must name a place a declared repository can hold.
kenji-nx carried tmp/es-de/ANDROID.md:470-474, a path from the machine
of whoever profiled it. No revision of any declared repository holds it,
so profile_sync could only report it missing, every pass, forever. The
check is offline and runs on every push.
"""
def _hits(self, text: str) -> bool:
import validate_schemas
return bool(validate_schemas._UNREACHABLE_REF.search(" " + text))
def test_a_scratch_directory_is_rejected(self):
self.assertTrue(self._hits("tmp/es-de/ANDROID.md:470-474"))
def test_an_absolute_path_is_rejected(self):
self.assertTrue(self._hits("/home/someone/src/a.c:12"))
self.assertTrue(self._hits(r"C:\work\src\a.c:12"))
def test_a_path_climbing_out_of_the_tree_is_rejected(self):
self.assertTrue(self._hits("../outside/src/a.c:12"))
def test_ordinary_citations_pass(self):
for good in (
"src/core/main.cpp:210",
"libretro.c:5293-5337",
"PCE.emu/src/main/Main.cc:80-91",
"es-de ANDROID.md:470-474",
"see tmp files for details",
):
with self.subTest(citation=good):
self.assertFalse(self._hits(good))
def test_the_corpus_carries_none(self):
import validate_schemas
errors = []
for path in sorted((ROOT / "emulators").glob("*.yml")):
with path.open(encoding="utf-8") as handle:
errors.extend(
validate_schemas._unreachable_citations(path, yaml.safe_load(handle))
)
self.assertEqual(errors, [])
class FaqRegressions(unittest.TestCase): class FaqRegressions(unittest.TestCase):
"""The FAQ states facts the code owns; these tie it back to the source. """The FAQ states facts the code owns; these tie it back to the source.