From 2ea79f76ec84d288ad847f5cb9c9235bc5ed198e Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 22:02:46 -0400 Subject: [PATCH 1/5] feat(display): add reviewed built-in profiles (DISP-04) --- .github/workflows/shell-test.yml | 2 + .gitignore | 1 + Makefile | 1 + build/dependencies.lock | 2 +- build/display-built-in-profiles.json | 164 +++++++++ build/display-profile-policy.json | 9 +- build/legacy-manifest.json | 8 +- docs/1.0_TEST_MATRIX.md | 1 + docs/ARCHITECTURE.md | 21 ++ docs/DISPLAY_PROFILE_POLICY.md | 35 +- docs/DISPLAY_SESSION_COMPOSER.md | 20 +- docs/SECURITY.md | 10 + src/bloomLaunch/bloom_launch.c | 7 +- src/bloomLaunch/bloom_launch.h | 5 +- src/bloomLaunch/bloom_retroarch_config.c | 126 ++++++- src/bloomLaunch/bloom_retroarch_config.h | 4 + src/bloomLaunch/main.c | 25 +- static/build/.tmp_update/bin/bloom-dev-deploy | 2 + .../bin/bloom-display-session-prepare | 99 +++++- test/shell/bloom_device.bats | 5 +- test/shell/dev_deploy.bats | 6 +- test/shell/display_built_in_profiles.bats | 92 +++++ test/shell/display_profile_policy.bats | 10 +- test/shell/display_session_prepare.bats | 38 +- test/test_bloom_launch.cpp | 1 + test/test_bloom_retroarch_config.cpp | 78 ++++ test/test_ci_scope.sh | 11 + tools/ci-scope.sh | 34 +- tools/dev-deploy-manifest.py | 1 + tools/display_built_in_profiles.py | 333 ++++++++++++++++++ tools/display_profile_policy.py | 21 +- 31 files changed, 1120 insertions(+), 52 deletions(-) create mode 100644 build/display-built-in-profiles.json create mode 100644 test/shell/display_built_in_profiles.bats create mode 100644 tools/display_built_in_profiles.py diff --git a/.github/workflows/shell-test.yml b/.github/workflows/shell-test.yml index db10ae525..e564aacb9 100644 --- a/.github/workflows/shell-test.yml +++ b/.github/workflows/shell-test.yml @@ -31,6 +31,7 @@ on: - 'tools/validate_core_selection_policy.py' - 'tools/display_auto_baseline.py' - 'tools/display_profile_policy.py' + - 'tools/display_built_in_profiles.py' - 'tools/validate_content_identity_policy.py' - 'tools/validate_libretro_artwork_policy.py' - 'tools/ci-scope.sh' @@ -42,6 +43,7 @@ on: - 'build/core-selection-policy*.json' - 'build/display-auto-baseline.json' - 'build/display-profile-policy.json' + - 'build/display-built-in-profiles.json' - 'build/content-identity-policy.json' - 'build/libretro-artwork-policy.json' - 'build/ra-core-policy.json' diff --git a/.gitignore b/.gitignore index 0f0228667..6112a4198 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ __pycache__/ !/build/core-selection-policy.json !/build/display-auto-baseline.json !/build/display-profile-policy.json +!/build/display-built-in-profiles.json !/build/libretro-artwork-policy.json !/build/themes.sha256 !/build/provenance-policy.json diff --git a/Makefile b/Makefile index ae8384e0e..24e55a339 100644 --- a/Makefile +++ b/Makefile @@ -185,6 +185,7 @@ core: $(CACHE)/.setup shared-libs @cp $(ROOT_DIR)/build/transfer-policy.json $(BUILD_DIR)/.tmp_update/config/transfer-policy.json @cp $(ROOT_DIR)/build/system-catalog.json $(BUILD_DIR)/.tmp_update/config/system-catalog.json @cp $(ROOT_DIR)/build/libretro-artwork-policy.json $(BUILD_DIR)/.tmp_update/config/libretro-artwork-policy.json + @cp $(ROOT_DIR)/build/display-built-in-profiles.json $(BUILD_DIR)/.tmp_update/config/display-built-in-profiles.json # Build dependencies for installer @mkdir -p $(INSTALLER_DIR)/bin diff --git a/build/dependencies.lock b/build/dependencies.lock index 8f4190c63..38db6f9d2 100644 --- a/build/dependencies.lock +++ b/build/dependencies.lock @@ -209,7 +209,7 @@ manifest = "build/legacy-manifest.json" schema = 1 component_count = 336 file_count = 2135 -byte_count = 314711917 +byte_count = 314716799 source_build_components = 232 unresolved_components = 104 inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b" diff --git a/build/display-built-in-profiles.json b/build/display-built-in-profiles.json new file mode 100644 index 000000000..4bb894e74 --- /dev/null +++ b/build/display-built-in-profiles.json @@ -0,0 +1,164 @@ +{ + "schema": 1, + "policy_id": "bloomos-display-built-in-profiles-v1", + "scope": { + "product": "ssd202d-only", + "devices": [ + "miyoo-mini-v1", + "miyoo-mini-v2-v3", + "miyoo-mini-v4", + "miyoo-mini-plus", + "miyoo-mini-flip-my285" + ], + "runtime_device": "miyoo-mini-plus", + "auto_selection": false, + "auto_behavior": "preserve-disp01-baseline", + "physical_certification": false + }, + "authorities": { + "core_selection_policy": { + "path": "build/core-selection-policy.json", + "hash_mode": "raw", + "sha256": "2754e459eba3270b910a0335d5f7212c01242030bb3734defec86cc849015871" + }, + "auto_baseline": { + "path": "build/display-auto-baseline.json", + "hash_mode": "raw", + "sha256": "d56c3ae130f61c28632a3a0aa9b9cc16ec1cae2195366240b0d92d9f78e09d06" + }, + "filter_contract": { + "path": "static/build/RetroArch/RetroArch_Dingux_forMiyooMini.txt", + "hash_mode": "raw", + "sha256": "952a2656351cec8e4aed103c0c8c79d7a3c80def71d7628651381aa88a6bc651" + }, + "retroarch_license": { + "path": "third-party/RetroArch-patch/submodules/RetroArch/COPYING", + "hash_mode": "raw", + "sha256": "0b383d5a63da644f628d99c33976ea6487ed89aaa59f0b3257992deac1171e6b" + } + }, + "activation_contract": { + "selection_key": "exact-device-system-core", + "review_status": "source-reviewed", + "missing_profile": "unavailable-without-mutation", + "filter_path": "fixed-signed-runtime-path-never-user-input", + "filter_integrity": "exact-size-and-sha256-before-session-composition", + "axis_independence": "profile-supplies-capabilities-without-changing-selections" + }, + "profiles": [ + { + "id": "plus-gb-gambatte-lcd3x-v1", + "device_id": "miyoo-mini-plus", + "system_id": "gb", + "candidate_id": "gambatte", + "core_binary": "gambatte_libretro.so", + "source_geometry": { + "width": 160, + "height": 144, + "rotation_quarter_turns": 0, + "display_aspect": {"numerator": 10, "denominator": 9}, + "authority": "reviewed-system-core-profile" + }, + "effect": { + "id": "lcd3x-light", + "class": "lcd", + "descriptor": { + "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", + "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", + "bytes": 22, + "sha256": "56bfda00e3439dc6d98102be2334f04592f57e1b6e99a7ea542879b4f129256a" + }, + "implementation": { + "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", + "bytes": 9588, + "sha256": "939c552b50b02472b438c35c3036c6173bbbf2f610c7abcc0f3c8e1a9d37b13e" + } + }, + "qualification": { + "review_status": "source-reviewed", + "device_evidence_status": "pending", + "evidence_ids": ["retroarch-miyoomini-filter-contract"] + } + }, + { + "id": "plus-gbc-gambatte-lcd3x-v1", + "device_id": "miyoo-mini-plus", + "system_id": "gbc", + "candidate_id": "gambatte", + "core_binary": "gambatte_libretro.so", + "source_geometry": { + "width": 160, + "height": 144, + "rotation_quarter_turns": 0, + "display_aspect": {"numerator": 10, "denominator": 9}, + "authority": "reviewed-system-core-profile" + }, + "effect": { + "id": "lcd3x-light", + "class": "lcd", + "descriptor": { + "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", + "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", + "bytes": 22, + "sha256": "56bfda00e3439dc6d98102be2334f04592f57e1b6e99a7ea542879b4f129256a" + }, + "implementation": { + "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", + "bytes": 9588, + "sha256": "939c552b50b02472b438c35c3036c6173bbbf2f610c7abcc0f3c8e1a9d37b13e" + } + }, + "qualification": { + "review_status": "source-reviewed", + "device_evidence_status": "pending", + "evidence_ids": ["retroarch-miyoomini-filter-contract"] + } + }, + { + "id": "plus-gba-gpsp-lcd2x-v1", + "device_id": "miyoo-mini-plus", + "system_id": "gba", + "candidate_id": "gpsp", + "core_binary": "gpsp_libretro.so", + "source_geometry": { + "width": 240, + "height": 160, + "rotation_quarter_turns": 0, + "display_aspect": {"numerator": 3, "denominator": 2}, + "authority": "reviewed-system-core-profile" + }, + "effect": { + "id": "lcd2x", + "class": "lcd", + "descriptor": { + "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD2x/LCD2x.filt", + "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD2x/LCD2x.filt", + "bytes": 16, + "sha256": "f8982fdc74c85d3bf999f86ef1aea155909f802a37254e75ca4235aa275815b1" + }, + "implementation": { + "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD2x/lcd2x.so", + "bytes": 5492, + "sha256": "31fec21ab644e300d2ce1367a1461da0eac4a8d1c369108edde2f493ef19b081" + } + }, + "qualification": { + "review_status": "source-reviewed", + "device_evidence_status": "pending", + "evidence_ids": ["retroarch-miyoomini-filter-contract"] + } + } + ], + "unsupported": { + "devices": [ + "miyoo-mini-v1", + "miyoo-mini-v2-v3", + "miyoo-mini-v4", + "miyoo-mini-flip-my285" + ], + "effect_classes": ["crt"], + "artwork": "no-reviewed-built-in-bezels", + "alternate_cores": "explicitly-unavailable-until-reviewed", + "behavior": "preserve-auto-or-report-unavailable-without-partial-config" + } +} diff --git a/build/display-profile-policy.json b/build/display-profile-policy.json index c9f24b0c4..0d12d65f2 100644 --- a/build/display-profile-policy.json +++ b/build/display-profile-policy.json @@ -10,8 +10,8 @@ "miyoo-mini-plus", "miyoo-mini-flip-my285" ], - "runtime_integration": "deferred-to-DISP-03", - "built_in_profiles": "deferred-to-DISP-04", + "runtime_integration": "enabled-in-DISP-03", + "built_in_profiles": "enabled-in-DISP-04", "custom_packs": "deferred-to-DISP-05", "permanent_retroarch_config_mutation": false, "physical_certification": false @@ -26,6 +26,11 @@ "path": "static/build/.tmp_update/bin/bloom-platform", "hash_mode": "text-lf", "sha256": "ccc3d1ad7efd29c8864a2b274c6af1ab6aff5efe9c5098dbe1b542ceaa11258d" + }, + "built_in_profiles": { + "path": "build/display-built-in-profiles.json", + "hash_mode": "raw", + "sha256": "c5ca0021f81575fd9cce66ec9e6fdae868f4651a1d9019427f6d718587dbf76d" } }, "public_controls": [ diff --git a/build/legacy-manifest.json b/build/legacy-manifest.json index 69b4567b3..413bd75ec 100644 --- a/build/legacy-manifest.json +++ b/build/legacy-manifest.json @@ -488,8 +488,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-dev-deploy", "file_count": 1, - "byte_count": 33011, - "tree_sha256": "4dd16ce6d73174d683d2b6b43161024a1694de4b48c58842ce89e80b15fa394e", + "byte_count": 33126, + "tree_sha256": "225e52c83020c0ebd88732688e8731125cc56fd912b77a010eb9696f2ade7ec9", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", @@ -527,8 +527,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-display-session-prepare", "file_count": 1, - "byte_count": 3190, - "tree_sha256": "5c4dff01fab14bac6d77ae7d0109d04e6b6eb736038a7d145f8d62db57b8a9bc", + "byte_count": 7957, + "tree_sha256": "6d5cb6579d1f096bb6114de3c8e4e627fda316cfabcf3dfab04b03a3e5c00a40", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", diff --git a/docs/1.0_TEST_MATRIX.md b/docs/1.0_TEST_MATRIX.md index ba28ce545..0da4203c7 100644 --- a/docs/1.0_TEST_MATRIX.md +++ b/docs/1.0_TEST_MATRIX.md @@ -23,6 +23,7 @@ observable properties and never substitutes for physical UX review. | Display Auto compatibility | Deterministic per-system/core effective visual fixture plus explicit no-selectable-core catalog dispositions; exact policy/catalog/manifest, core binary, core metadata, global, core-override, and content-directory hashes; representative legacy geometry/effect assertions; runtime user and per-game overrides excluded | | Display profile semantics | Exact Scaling, Screen Effect, and Bezel controls; disjoint geometry/sampling/effect/artwork key ownership; PLAT-01 framebuffer capability; bounded centered Auto/integer/aspect/fill geometry fixtures for 640x480 and 752x560; invalid geometry and stale/coupled/unsafe policy mutations fail without partial config | | Display settings and session composer | Canonical backward-compatible display selections; exact enum rejection; no Onion projection; observed framebuffer input; independent axis fallback; one private `0600` RetroArch append config shared with achievements; no permanent-config mutation; decorator failure and timeout preserve the immutable base launch | +| Built-in display profiles | Versioned signed authority; exact Mini Plus device/system/default-core joins for GB, GBC, and GBA; reviewed source geometry/aspect and packaged LCD2x/LCD3x-light descriptor plus implementation hashes; exact filter byte/hash verification before use; Auto unchanged; alternate devices/cores, CRT, custom effects, and bezels fail unavailable | | Performance | Input/render timings, 30-minute shell soak, 500 open/back cycles | | Privacy | Allowlisted health/support export contains no private data | | Developer deployment | Dual-marker/update/session preflight; deterministic allowlisted manifest; exact byte/hash/mode transfer; sealed slots; atomic activation; readiness auto-rollback; active/previous overlay; bounded cleanup; ordinary network-helper activation denied | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ad15b87ec..ee433fe43 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1410,6 +1410,27 @@ and shell punctuation remain data. The runner retains all safety boundaries: explicit developer mode, supported-system allowlisting, ROM confinement, bounded duration, structured launch/session handling, and save flushing. +## Built-in display profile authority + +`build/display-built-in-profiles.json` is the schema-1 DISP-04 authority for +reviewed display identities. It is included in the signed payload and joined at +session preparation by exact device, canonical system, and selected core. The +initial authority is deliberately narrow: Mini Plus with the default Gambatte +core for GB/GBC and default gpSP core for GBA. Each row owns reviewed source +geometry and aspect plus the exact packaged LCD filter descriptor and +implementation size and SHA-256. Auto remains the complete DISP-01 baseline; +the catalog never performs implicit selection. + +`bloom-display-session-prepare` trusts the canonical launch request for system +and core identity and `bloom-platform` for the device. It reads only the signed +catalog location, resolves one exact row, verifies the selected filter bytes, +and passes reviewed numeric geometry and one fixed filter path to +`bloom-launch`. The native composer uses checked integer geometry and emits only +its display-owned keys into the private session config. It accepts no arbitrary +path or raw RetroArch setting. Unsupported devices, alternate cores, missing or +stale filter bytes, CRT, custom effects, and bezels resolve unavailable without +partially applying that axis or mutating global RetroArch configuration. + ## Development deployment overlay `bloom-dev-deploy` provides the device half of the guarded Hot/Warm development diff --git a/docs/DISPLAY_PROFILE_POLICY.md b/docs/DISPLAY_PROFILE_POLICY.md index d69ae5efd..cf7d1f1df 100644 --- a/docs/DISPLAY_PROFILE_POLICY.md +++ b/docs/DISPLAY_PROFILE_POLICY.md @@ -58,13 +58,28 @@ python3 tools/display_profile_policy.py geometry \ ## Capability and delivery boundaries -Explicit LCD and CRT modes require a qualified signed built-in effect profile; -those assets belong to DISP-04. Auto effect and Auto bezel resolve to None when -no reviewed profile exists. Custom effect and bezel selections accept only a -validated bounded pack ID after DISP-05, never an arbitrary filesystem path. - -DISP-02 does not add settings, assets, runtime config writes, or device code. -DISP-03 will integrate the policy with `bloom-settings` and the one trusted -temporary session config composer. Permanent `retroarch.cfg` mutation remains -forbidden. This host-only authority is not evidence of ideal visual output, -LCD quality, gameplay, physical controls, or certification for any device. +DISP-04 adds one signed, versioned built-in profile authority without changing +Auto. It reviews only the Mini Plus default GB/GBC Gambatte and GBA gpSP +selectors: GB/GBC use the packaged LCD3x-light filter and GBA uses the packaged +LCD2x filter, with exact source geometry, aspect, descriptor and implementation +hashes. Selection is an exact device/system/core join. Mini V1-V4, Flip, +alternate cores, CRT, bezels, and custom packs remain unavailable rather than +inferred. + +DISP-03 integrated the policy with `bloom-settings` and the one trusted +temporary session config composer. DISP-04 supplies reviewed source geometry +and a fixed packaged filter identity to that composer. Permanent +`retroarch.cfg` mutation remains forbidden. Host and remote Mini Plus evidence +does not certify ideal LCD output, gameplay, physical controls, audio, or any +other device revision. + +Validate or resolve the built-in authority with: + +```sh +python3 tools/display_built_in_profiles.py validate \ + --repository . --policy build/display-built-in-profiles.json + +python3 tools/display_built_in_profiles.py resolve \ + --repository . --policy build/display-built-in-profiles.json \ + --device miyoo-mini-plus --system gba --core gpsp_libretro.so +``` diff --git a/docs/DISPLAY_SESSION_COMPOSER.md b/docs/DISPLAY_SESSION_COMPOSER.md index 9b59aa1a5..b6b227bef 100644 --- a/docs/DISPLAY_SESSION_COMPOSER.md +++ b/docs/DISPLAY_SESSION_COMPOSER.md @@ -26,16 +26,20 @@ the validated display contribution and atomically replace the same file. `auto` does not rewrite an axis from the DISP-01 baseline. `fill`, nearest or linear sampling, effect `none`, and bezel `none` have complete typed composer -implementations. Integer Pixels and Correct Aspect remain unavailable until a -trusted source raster/aspect is connected. LCD, CRT, and custom effect or -bezel selections remain unavailable until DISP-04/DISP-05 provide reviewed -identities. An unavailable selection resolves that axis to `auto`, records a -bounded reason, and emits no partial keys for that axis. +implementations. DISP-04 connects trusted source raster/aspect and a fixed +filter identity for three exact Mini Plus default-core selectors: GB/GBC with +Gambatte and GBA with gpSP. Integer Pixels and Correct Aspect can therefore be +composed for those selectors, and LCD selects the reviewed packaged LCD3x-light +or LCD2x filter. Other devices, systems, cores, CRT, custom effects, and bezels +remain unavailable. An unavailable selection resolves that axis to `auto`, +records a bounded reason, and emits no partial keys for that axis. The display owner can emit only the allowlisted display keys in `bloom_retroarch_config.c`; it cannot emit achievement, input-safety, driver, -network, or arbitrary filesystem settings. Custom selections are bounded IDs -in settings and never become raw paths in DISP-03. +network, or arbitrary filesystem settings. The session helper accepts only a +catalog path from the signed payload and verifies the selected filter's exact +size and SHA-256 before passing its fixed `/mnt/SDCARD/RetroArch/.retroarch/filters/video/` +path. Custom selections are bounded IDs in settings and never become raw paths. Validation: @@ -43,6 +47,8 @@ Validation: make test GTEST_FILTER='BloomSettingsTest.*:BloomRetroarchConfigTest.*:BloomLaunchTest.*' docker run --rm -v "$PWD:/workspace" -w /workspace \ ghcr.io/boburning/bloomos-shell-test@sha256:36adeca7c265345688ebf47b575c56e350b9016257e0a9ed15c63dc7e0447d63 \ + test/shell/display_built_in_profiles.bats \ + test/shell/display_profile_policy.bats \ test/shell/display_session_prepare.bats \ test/shell/bloom_session.bats \ test/shell/ra_session_prepare.bats diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 04aede49f..71065deed 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -330,4 +330,14 @@ older-schema status cannot invoke any primary network operation. This prevents stale or lossy Boolean projection from repeatedly restarting a connection in progress or treating an unknown backend contract as authorization. +Built-in display profiles are data, not a path-selection capability. The signed +schema-1 authority permits only exact Mini Plus device/system/default-core +selectors and repository-owned filter identities. Session preparation verifies +the selected descriptor and implementation size and SHA-256, and the native +composer accepts only a fixed path below +`/mnt/SDCARD/RetroArch/.retroarch/filters/video/` with no traversal or unsafe +characters. User settings choose typed semantic modes; they never supply a raw +RetroArch key, path, geometry, or filter. The resulting contribution is private +session state and cannot mutate global RetroArch configuration. + Security-sensitive migrations and repair operations must be bounded, logged, reversible where possible, and validated before activation. diff --git a/src/bloomLaunch/bloom_launch.c b/src/bloomLaunch/bloom_launch.c index d26b87f85..394a9f07a 100644 --- a/src/bloomLaunch/bloom_launch.c +++ b/src/bloomLaunch/bloom_launch.c @@ -972,7 +972,10 @@ int bloom_launch_copy_achievement_state(const char *source_path, const char *des int bloom_launch_write_display_config(const char *config_path, const char *scaling, const char *sampling, const char *screen_effect, const char *bezel, unsigned panel_width, - unsigned panel_height, char *effective_scaling, + unsigned panel_height, unsigned source_width, + unsigned source_height, unsigned aspect_numerator, + unsigned aspect_denominator, unsigned rotation_quarter_turns, + const char *effect_filter_path, char *effective_scaling, size_t effective_scaling_size, char *effective_effect, size_t effective_effect_size, char *effective_bezel, size_t effective_bezel_size, char *reason, @@ -985,7 +988,7 @@ int bloom_launch_write_display_config(const char *config_path, const char *scali BloomRetroarchConfig *composer = bloom_retroarch_config_create(); char config[4096]; size_t length = 0; - int composed = composer == NULL ? -1 : bloom_retroarch_config_apply_display(composer, scaling, sampling, screen_effect, bezel, panel_width, panel_height, effective_scaling, effective_scaling_size, effective_effect, effective_effect_size, effective_bezel, effective_bezel_size, reason, reason_size, error, error_size); + int composed = composer == NULL ? -1 : bloom_retroarch_config_apply_display(composer, scaling, sampling, screen_effect, bezel, panel_width, panel_height, source_width, source_height, aspect_numerator, aspect_denominator, rotation_quarter_turns, effect_filter_path, effective_scaling, effective_scaling_size, effective_effect, effective_effect_size, effective_bezel, effective_bezel_size, reason, reason_size, error, error_size); if (composer == NULL) set_error(error, error_size, "cannot allocate RetroArch config composer"); if (composed == 0) diff --git a/src/bloomLaunch/bloom_launch.h b/src/bloomLaunch/bloom_launch.h index 976332f0f..4390c5ea2 100644 --- a/src/bloomLaunch/bloom_launch.h +++ b/src/bloomLaunch/bloom_launch.h @@ -37,7 +37,10 @@ int bloom_launch_write_ra_config(const char *request_path, const char *config_pa int bloom_launch_write_display_config(const char *config_path, const char *scaling, const char *sampling, const char *screen_effect, const char *bezel, unsigned panel_width, - unsigned panel_height, char *effective_scaling, + unsigned panel_height, unsigned source_width, + unsigned source_height, unsigned aspect_numerator, + unsigned aspect_denominator, unsigned rotation_quarter_turns, + const char *effect_filter_path, char *effective_scaling, size_t effective_scaling_size, char *effective_effect, size_t effective_effect_size, char *effective_bezel, size_t effective_bezel_size, char *reason, diff --git a/src/bloomLaunch/bloom_retroarch_config.c b/src/bloomLaunch/bloom_retroarch_config.c index 349ad88fb..4ae2e51da 100644 --- a/src/bloomLaunch/bloom_retroarch_config.c +++ b/src/bloomLaunch/bloom_retroarch_config.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -206,10 +207,37 @@ static bool selection(const char *value, const char *const *allowed, size_t coun return false; } +static bool safe_filter_path(const char *path) +{ + static const char prefix[] = "/mnt/SDCARD/RetroArch/.retroarch/filters/video/"; + if (path == NULL || strncmp(path, prefix, sizeof(prefix) - 1) != 0 || + path[sizeof(prefix) - 1] == '\0' || strlen(path) >= 256 || strstr(path, "..") != NULL || + strcmp(path + strlen(path) - strlen(".filt"), ".filt") != 0) + return false; + for (const unsigned char *cursor = (const unsigned char *)path; *cursor; ++cursor) + if (*cursor < 0x21 || *cursor > 0x7e || *cursor == '"' || *cursor == '\\') + return false; + return true; +} + +static unsigned greatest_common_divisor(unsigned left, unsigned right) +{ + while (right != 0) { + unsigned remainder = left % right; + left = right; + right = remainder; + } + return left; +} + int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, const char *scaling, const char *sampling, const char *screen_effect, const char *bezel, unsigned panel_width, unsigned panel_height, + unsigned source_width, unsigned source_height, + unsigned aspect_numerator, unsigned aspect_denominator, + unsigned rotation_quarter_turns, + const char *effect_filter_path, char *effective_scaling, size_t effective_scaling_size, char *effective_effect, size_t effective_effect_size, char *effective_bezel, size_t effective_bezel_size, @@ -227,7 +255,12 @@ int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, !selection(sampling, sampling_values, sizeof(sampling_values) / sizeof(sampling_values[0])) || !selection(screen_effect, effect_values, sizeof(effect_values) / sizeof(effect_values[0])) || !selection(bezel, bezel_values, sizeof(bezel_values) / sizeof(bezel_values[0])) || - panel_width > 4096 || panel_height > 4096) { + panel_width > 4096 || panel_height > 4096 || source_width > 4096 || + source_height > 4096 || aspect_numerator > 65535 || aspect_denominator > 65535 || + rotation_quarter_turns > 3 || + ((source_width == 0 || source_height == 0 || aspect_numerator == 0 || aspect_denominator == 0) && + (source_width != 0 || source_height != 0 || aspect_numerator != 0 || aspect_denominator != 0)) || + (effect_filter_path != NULL && !safe_filter_path(effect_filter_path))) { set_error(error, error_size, "display config input is invalid"); return -1; } @@ -235,7 +268,14 @@ int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, snprintf(effective_effect, effective_effect_size, "%s", screen_effect); snprintf(effective_bezel, effective_bezel_size, "%s", bezel); reason[0] = '\0'; - if ((strcmp(scaling, "integer-pixels") == 0 || strcmp(scaling, "correct-aspect") == 0)) { + unsigned viewport_width = 0; + unsigned viewport_height = 0; + unsigned viewport_x = 0; + unsigned viewport_y = 0; + bool source_available = source_width != 0 && source_height != 0 && + aspect_numerator != 0 && aspect_denominator != 0; + if ((strcmp(scaling, "integer-pixels") == 0 || strcmp(scaling, "correct-aspect") == 0) && + (!source_available || panel_width == 0 || panel_height == 0)) { snprintf(effective_scaling, effective_scaling_size, "auto"); snprintf(reason, reason_size, "source_geometry_unavailable"); } @@ -243,8 +283,65 @@ int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, snprintf(effective_scaling, effective_scaling_size, "auto"); snprintf(reason, reason_size, "framebuffer_unavailable"); } - if (strcmp(screen_effect, "lcd") == 0 || strcmp(screen_effect, "crt") == 0 || - strcmp(screen_effect, "custom") == 0) { + if (strcmp(effective_scaling, "integer-pixels") == 0) { + unsigned raster_width = source_width; + unsigned raster_height = source_height; + if (rotation_quarter_turns % 2 != 0) { + raster_width = source_height; + raster_height = source_width; + } + unsigned integer_scale = panel_width / raster_width; + unsigned vertical_scale = panel_height / raster_height; + if (vertical_scale < integer_scale) + integer_scale = vertical_scale; + if (integer_scale == 0) { + snprintf(effective_scaling, effective_scaling_size, "auto"); + snprintf(reason, reason_size, "source_geometry_unavailable"); + } + else { + viewport_width = raster_width * integer_scale; + viewport_height = raster_height * integer_scale; + } + } + else if (strcmp(effective_scaling, "correct-aspect") == 0) { + unsigned numerator = aspect_numerator; + unsigned denominator = aspect_denominator; + unsigned divisor = greatest_common_divisor(numerator, denominator); + numerator /= divisor; + denominator /= divisor; + if (rotation_quarter_turns % 2 != 0) { + unsigned swap = numerator; + numerator = denominator; + denominator = swap; + } + if ((uint64_t)panel_width * denominator <= (uint64_t)panel_height * numerator) { + viewport_width = panel_width; + viewport_height = (unsigned)(((uint64_t)panel_width * denominator + numerator / 2) / numerator); + } + else { + viewport_height = panel_height; + viewport_width = (unsigned)(((uint64_t)panel_height * numerator + denominator / 2) / denominator); + } + if (viewport_width == 0) + viewport_width = 1; + if (viewport_height == 0) + viewport_height = 1; + if (viewport_width > panel_width) + viewport_width = panel_width; + if (viewport_height > panel_height) + viewport_height = panel_height; + } + if (viewport_width != 0 && viewport_height != 0) { + viewport_x = (panel_width - viewport_width) / 2; + viewport_y = (panel_height - viewport_height) / 2; + } + if ((strcmp(screen_effect, "lcd") == 0 || strcmp(screen_effect, "crt") == 0) && + effect_filter_path == NULL) { + snprintf(effective_effect, effective_effect_size, "auto"); + if (reason[0] == '\0') + snprintf(reason, reason_size, "effect_profile_unavailable"); + } + else if (strcmp(screen_effect, "custom") == 0) { snprintf(effective_effect, effective_effect_size, "auto"); if (reason[0] == '\0') snprintf(reason, reason_size, "effect_profile_unavailable"); @@ -288,6 +385,23 @@ int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, DISPLAY_BOOL("video_scale_integer", 0); DISPLAY_BOOL("video_scale_integer_overscale", 0); } + else if (strcmp(effective_scaling, "integer-pixels") == 0 || + strcmp(effective_scaling, "correct-aspect") == 0) { + DISPLAY_UINT("aspect_ratio_index", 23); + DISPLAY_UINT("custom_viewport_height", viewport_height); + DISPLAY_UINT("custom_viewport_width", viewport_width); + DISPLAY_UINT("custom_viewport_x", viewport_x); + DISPLAY_UINT("custom_viewport_y", viewport_y); + DISPLAY_BOOL("video_aspect_ratio_auto", 0); + DISPLAY_BOOL("video_crop_overscan", 0); + DISPLAY_BOOL("video_dingux_ipu_keep_aspect", 1); + DISPLAY_BOOL("video_force_aspect", 1); + DISPLAY_UINT("video_fullscreen_x", panel_width); + DISPLAY_UINT("video_fullscreen_y", panel_height); + DISPLAY_UINT("video_rotation", rotation_quarter_turns); + DISPLAY_BOOL("video_scale_integer", strcmp(effective_scaling, "integer-pixels") == 0); + DISPLAY_BOOL("video_scale_integer_overscale", 0); + } if (strcmp(sampling, "nearest") == 0) { DISPLAY_UINT("video_dingux_ipu_filter_type", 0); DISPLAY_BOOL("video_smooth", 0); @@ -300,6 +414,10 @@ int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, DISPLAY_STRING("video_filter", ""); DISPLAY_BOOL("video_shader_enable", 0); } + else if (strcmp(effective_effect, "lcd") == 0 || strcmp(effective_effect, "crt") == 0) { + DISPLAY_STRING("video_filter", effect_filter_path); + DISPLAY_BOOL("video_shader_enable", 0); + } if (strcmp(effective_bezel, "none") == 0) { DISPLAY_STRING("input_overlay", ""); DISPLAY_BOOL("input_overlay_enable", 0); diff --git a/src/bloomLaunch/bloom_retroarch_config.h b/src/bloomLaunch/bloom_retroarch_config.h index ef92e908f..b7c5ed262 100644 --- a/src/bloomLaunch/bloom_retroarch_config.h +++ b/src/bloomLaunch/bloom_retroarch_config.h @@ -29,6 +29,10 @@ int bloom_retroarch_config_apply_display(BloomRetroarchConfig *config, const char *scaling, const char *sampling, const char *screen_effect, const char *bezel, unsigned panel_width, unsigned panel_height, + unsigned source_width, unsigned source_height, + unsigned aspect_numerator, unsigned aspect_denominator, + unsigned rotation_quarter_turns, + const char *effect_filter_path, char *effective_scaling, size_t effective_scaling_size, char *effective_effect, size_t effective_effect_size, char *effective_bezel, size_t effective_bezel_size, diff --git a/src/bloomLaunch/main.c b/src/bloomLaunch/main.c index 08340d8ce..dbdface23 100644 --- a/src/bloomLaunch/main.c +++ b/src/bloomLaunch/main.c @@ -14,7 +14,7 @@ static void usage(void) " bloom-launch resolve-achievements REQUEST.json ENABLED MODE OFFLINE_CASUAL PROXY_READY RA_GAME_ID CERTIFICATION\n" " bloom-launch set-achievements-state REQUEST.json ENABLED REQUESTED EFFECTIVE TRANSPORT REASON|- RA_GAME_ID CERTIFICATION\n" " bloom-launch copy-achievements-state SOURCE.json DESTINATION.json\n" - " bloom-launch write-display-config CONFIG SCALING SAMPLING EFFECT BEZEL WIDTH HEIGHT\n" + " bloom-launch write-display-config CONFIG SCALING SAMPLING EFFECT BEZEL WIDTH HEIGHT SOURCE_WIDTH SOURCE_HEIGHT ASPECT_NUMERATOR ASPECT_DENOMINATOR ROTATION FILTER|-\n" " bloom-launch attach-config REQUEST.json CONFIG\n" " bloom-launch write-ra-config REQUEST.json CONFIG USERNAME PROXY_HOST|- < TOKEN\n" " bloom-launch write-legacy REQUEST.json COMMAND.sh\n"); @@ -76,22 +76,41 @@ int main(int argc, char **argv) result = bloom_launch_copy_achievement_state(argv[2], argv[3], error, sizeof(error)); else if (argc == 4 && strcmp(argv[1], "attach-config") == 0) result = bloom_launch_attach_session_config(argv[2], argv[3], error, sizeof(error)); - else if (argc == 9 && strcmp(argv[1], "write-display-config") == 0) { + else if (argc == 15 && strcmp(argv[1], "write-display-config") == 0) { char *width_end = NULL; char *height_end = NULL; + char *source_width_end = NULL; + char *source_height_end = NULL; + char *aspect_numerator_end = NULL; + char *aspect_denominator_end = NULL; + char *rotation_end = NULL; unsigned long width = strtoul(argv[7], &width_end, 10); unsigned long height = strtoul(argv[8], &height_end, 10); + unsigned long source_width = strtoul(argv[9], &source_width_end, 10); + unsigned long source_height = strtoul(argv[10], &source_height_end, 10); + unsigned long aspect_numerator = strtoul(argv[11], &aspect_numerator_end, 10); + unsigned long aspect_denominator = strtoul(argv[12], &aspect_denominator_end, 10); + unsigned long rotation = strtoul(argv[13], &rotation_end, 10); char effective_scaling[24] = {}; char effective_effect[16] = {}; char effective_bezel[16] = {}; char reason[48] = {}; if (width_end == argv[7] || *width_end != '\0' || height_end == argv[8] || - *height_end != '\0' || width > 4096 || height > 4096) { + *height_end != '\0' || source_width_end == argv[9] || *source_width_end != '\0' || + source_height_end == argv[10] || *source_height_end != '\0' || + aspect_numerator_end == argv[11] || *aspect_numerator_end != '\0' || + aspect_denominator_end == argv[12] || *aspect_denominator_end != '\0' || + rotation_end == argv[13] || *rotation_end != '\0' || width > 4096 || height > 4096 || + source_width > 4096 || source_height > 4096 || aspect_numerator > 65535 || + aspect_denominator > 65535 || rotation > 3) { usage(); return 2; } result = bloom_launch_write_display_config( argv[2], argv[3], argv[4], argv[5], argv[6], (unsigned)width, (unsigned)height, + (unsigned)source_width, (unsigned)source_height, (unsigned)aspect_numerator, + (unsigned)aspect_denominator, (unsigned)rotation, + strcmp(argv[14], "-") == 0 ? NULL : argv[14], effective_scaling, sizeof(effective_scaling), effective_effect, sizeof(effective_effect), effective_bezel, sizeof(effective_bezel), reason, sizeof(reason), error, sizeof(error)); diff --git a/static/build/.tmp_update/bin/bloom-dev-deploy b/static/build/.tmp_update/bin/bloom-dev-deploy index be05089f8..f76923abc 100755 --- a/static/build/.tmp_update/bin/bloom-dev-deploy +++ b/static/build/.tmp_update/bin/bloom-dev-deploy @@ -81,6 +81,7 @@ allowed_path() { launch-session:bin/bloom-launch-override|launch-session:bin/bloom-display-session-prepare|\ launch-session:bin/bloom-session|\ launch-session:bin/bloom-ra-session-prepare|\ + launch-session:config/display-built-in-profiles.json|\ launch-session:script/bloom-launch-trace.sh) return 0 ;; network-helpers:bin/bloom-settings|network-helpers:bin/bloom-network|\ network-helpers:bin/bloom-network-metadata|\ @@ -187,6 +188,7 @@ validate_manifest() { bin/bloom-ra-session-prepare \ bin/bloom-session \ bin/bloom-shell \ + config/display-built-in-profiles.json \ bin/gameSwitcher \ bin/keymon \ script/bloom-launch-trace.sh | sort)" diff --git a/static/build/.tmp_update/bin/bloom-display-session-prepare b/static/build/.tmp_update/bin/bloom-display-session-prepare index 0ae53e04b..6e3782713 100755 --- a/static/build/.tmp_update/bin/bloom-display-session-prepare +++ b/static/build/.tmp_update/bin/bloom-display-session-prepare @@ -6,6 +6,8 @@ LAUNCH_BIN=${BLOOM_LAUNCH_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-launch} SETTINGS_BIN=${BLOOM_SETTINGS_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-settings} PLATFORM_BIN=${BLOOM_PLATFORM_BIN:-/mnt/SDCARD/.tmp_update/bin/bloom-platform} JQ_BIN=${BLOOM_JQ_BIN:-/mnt/SDCARD/.tmp_update/bin/jq} +PROFILE_POLICY=${BLOOM_DISPLAY_BUILT_IN_PROFILES:-/mnt/SDCARD/.tmp_update/config/display-built-in-profiles.json} +SHA256_BIN=${BLOOM_SHA256_BIN:-sha256sum} SESSION_ROOT=${BLOOM_SESSION_ROOT:-/tmp/bloom-session} fail() { printf 'bloom-display-session-prepare: %s\n' "$*" >&2; exit 1; } @@ -18,6 +20,10 @@ request=$1 emulator_type=$("$LAUNCH_BIN" get "$request" emulator_type 2>/dev/null) || fail "launch backend is unavailable" [ "$emulator_type" = retroarch ] || fail "display config requires RetroArch" +system_id=$("$LAUNCH_BIN" get "$request" system_id 2>/dev/null) || + fail "launch system is unavailable" +core=$("$LAUNCH_BIN" get "$request" core 2>/dev/null) || + fail "launch core is unavailable" settings_json=$("$SETTINGS_BIN" values 2>/dev/null) || fail "canonical settings are unavailable" display_json=$(printf '%s\n' "$settings_json" | "$JQ_BIN" -cer ' @@ -53,15 +59,106 @@ case "$panel_height" in ''|*[!0-9]*) panel_height=0 ;; esac [ "$panel_width" -le 4096 ] 2>/dev/null || panel_width=0 [ "$panel_height" -le 4096 ] 2>/dev/null || panel_height=0 +device_id=unknown +if [ -x "$PLATFORM_BIN" ]; then + case $("$PLATFORM_BIN" model 2>/dev/null || printf 'unknown') in + mini_plus) device_id=miyoo-mini-plus ;; + mini_flip) device_id=miyoo-mini-flip-my285 ;; + esac +fi +profile_id= +source_width=0 +source_height=0 +aspect_numerator=0 +aspect_denominator=0 +rotation=0 +effect_class= +effect_filter_path=- +effect_filter_bytes=0 +effect_filter_sha256= +if [ -f "$PROFILE_POLICY" ] && [ ! -L "$PROFILE_POLICY" ]; then + profile_fields=$("$JQ_BIN" -er --arg device "$device_id" --arg system "$system_id" --arg core "$core" ' + select( + .schema == 1 and .policy_id == "bloomos-display-built-in-profiles-v1" and + .scope.product == "ssd202d-only" and .scope.runtime_device == "miyoo-mini-plus" and + .scope.auto_selection == false and .scope.auto_behavior == "preserve-disp01-baseline" + ) | + [.profiles[] | + select( + .device_id == $device and .system_id == $system and .core_binary == $core and + .qualification.review_status == "source-reviewed" and + (.id | type) == "string" and (.source_geometry.width | type) == "number" and + (.source_geometry.height | type) == "number" and + (.source_geometry.rotation_quarter_turns | type) == "number" and + (.source_geometry.display_aspect.numerator | type) == "number" and + (.source_geometry.display_aspect.denominator | type) == "number" and + (.effect.class == "lcd" or .effect.class == "crt") and + (.effect.descriptor.runtime_path | type) == "string" and + (.effect.descriptor.bytes | type) == "number" and + (.effect.descriptor.sha256 | test("^[0-9a-f]{64}$")) + ) + ] | + select(length == 1) | .[0] | + [.id, (.source_geometry.width | tostring), (.source_geometry.height | tostring), + (.source_geometry.display_aspect.numerator | tostring), + (.source_geometry.display_aspect.denominator | tostring), + (.source_geometry.rotation_quarter_turns | tostring), .effect.class, + .effect.descriptor.runtime_path, (.effect.descriptor.bytes | tostring), + .effect.descriptor.sha256] | @tsv' "$PROFILE_POLICY" 2>/dev/null || true) + if [ -n "$profile_fields" ]; then + tab=$(printf '\t') + IFS="$tab" read -r profile_id source_width source_height aspect_numerator \ + aspect_denominator rotation effect_class effect_path effect_filter_bytes \ + effect_filter_sha256 </dev/null || profile_id= + [ "$source_height" -le 4096 ] 2>/dev/null || profile_id= + [ "$aspect_numerator" -le 65535 ] 2>/dev/null || profile_id= + [ "$aspect_denominator" -le 65535 ] 2>/dev/null || profile_id= + if [ -z "$profile_id" ]; then + source_width=0 + source_height=0 + aspect_numerator=0 + aspect_denominator=0 + rotation=0 + elif [ "$screen_effect" = "$effect_class" ] && command -v "$SHA256_BIN" >/dev/null 2>&1 && + [ -f "$effect_path" ] && [ ! -L "$effect_path" ] && + [ "$(wc -c <"$effect_path" | tr -d ' ')" = "$effect_filter_bytes" ] && + [ "$("$SHA256_BIN" "$effect_path" | awk '{print $1}')" = "$effect_filter_sha256" ]; then + effect_filter_path=$effect_path + fi + fi +fi + +active_profile_id= +case "$scaling" in integer-pixels|correct-aspect) active_profile_id=$profile_id ;; esac +[ "$effect_filter_path" = - ] || active_profile_id=$profile_id + rm -f "$SESSION_ROOT/ra.cfg" result=$("$LAUNCH_BIN" write-display-config "$SESSION_ROOT/ra.cfg" "$scaling" "$sampling" \ - "$screen_effect" "$bezel" "$panel_width" "$panel_height") || + "$screen_effect" "$bezel" "$panel_width" "$panel_height" "$source_width" "$source_height" \ + "$aspect_numerator" "$aspect_denominator" "$rotation" "$effect_filter_path") || fail "display config could not be composed" +result=$(printf '%s\n' "$result" | "$JQ_BIN" -cer --arg profile "$active_profile_id" \ + '. + {profile_id: (if $profile == "" then null else $profile end)}') || + fail "display result could not be finalized" printf '%s\n' "$result" | "$JQ_BIN" -e \ '.schema == 1 and (.status == "applied" or .status == "degraded") and (.effective.scaling | type == "string") and (.effective.screen_effect | type == "string") and (.effective.bezel | type == "string") and + (.profile_id == null or (.profile_id | type) == "string") and (.reason == null or (.reason | type == "string"))' >/dev/null 2>&1 || fail "display result is invalid" printf '%s\n' "$result" diff --git a/test/shell/bloom_device.bats b/test/shell/bloom_device.bats index 381b116ec..ffa36fc97 100644 --- a/test/shell/bloom_device.bats +++ b/test/shell/bloom_device.bats @@ -316,6 +316,7 @@ PY bin/bloom-ra-session-prepare \ bin/bloom-session \ bin/bloom-shell \ + config/display-built-in-profiles.json \ bin/gameSwitcher \ bin/keymon \ script/bloom-launch-trace.sh; do @@ -323,12 +324,14 @@ PY printf '#!/bin/sh\nexit 0\n' >"$payload/$path" chmod 0755 "$payload/$path" done + cp /workspace/build/display-built-in-profiles.json \ + "$payload/config/display-built-in-profiles.json" export BLOOM_SOURCE_COMMIT=0123456789abcdef0123456789abcdef01234567 run "$DEVICE_TOOL" dev-stage test-plus launch-session "$payload" launch-bundle-1 [ "$status" -eq 0 ] - [ "$(jq '.files | length' "$BLOOM_TEST_ROOT/received-manifest.json")" -eq 11 ] + [ "$(jq '.files | length' "$BLOOM_TEST_ROOT/received-manifest.json")" -eq 12 ] rm "$payload/bin/keymon" : >"$MOCK_LOG" diff --git a/test/shell/dev_deploy.bats b/test/shell/dev_deploy.bats index fbfc885c0..08ea1020c 100644 --- a/test/shell/dev_deploy.bats +++ b/test/shell/dev_deploy.bats @@ -64,13 +64,14 @@ make_launch_manifest() { bin/bloom-ra-session-prepare \ bin/bloom-session \ bin/bloom-shell \ + config/display-built-in-profiles.json \ bin/gameSwitcher \ bin/keymon \ script/bloom-launch-trace.sh; do [ "$path" = "$omitted" ] || printf '%s\n' "$path" done | jq -Rn --arg id "$id" ' [inputs | select(length > 0) | - {path:.,size:0,sha256:"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",mode:"0755"}] + {path:.,size:0,sha256:"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",mode:(if startswith("bin/") or startswith("script/") then "0755" else "0644" end)}] | {schema:1,deployment_id:$id,component:"launch-session", base_commit:"0123456789abcdef0123456789abcdef01234567",files:.} ' >"$manifest" @@ -109,6 +110,7 @@ stage_launch_bundle() { bin/bloom-ra-session-prepare \ bin/bloom-session \ bin/bloom-shell \ + config/display-built-in-profiles.json \ bin/gameSwitcher \ bin/keymon \ script/bloom-launch-trace.sh; do @@ -119,6 +121,8 @@ exit 0 EOF chmod +x "$payload/$path" done + cp /workspace/build/display-built-in-profiles.json \ + "$payload/config/display-built-in-profiles.json" cat >"$payload/bin/bloom-session" <<'EOF' #!/bin/sh [ "$1:$2" = "status:--json" ] || exit 2 diff --git a/test/shell/display_built_in_profiles.bats b/test/shell/display_built_in_profiles.bats new file mode 100644 index 000000000..8dd5207b7 --- /dev/null +++ b/test/shell/display_built_in_profiles.bats @@ -0,0 +1,92 @@ +#!/usr/bin/env bats + +POLICY=/workspace/build/display-built-in-profiles.json +TOOL=/workspace/tools/display_built_in_profiles.py + +validate_profiles() { + python3 "$TOOL" validate --repository /workspace --policy "$1" +} + +resolve_profile() { + python3 "$TOOL" resolve --repository /workspace --policy "$POLICY" "$@" +} + +mutate_profiles() { + mode="$1" + output="$2" + python3 - "$POLICY" "$output" "$mode" <<'PY' +import json +import sys + +source, output, mode = sys.argv[1:] +policy = json.load(open(source, encoding="utf-8")) +if mode == "enable-auto": + policy["scope"]["auto_selection"] = True +elif mode == "unsupported-device": + policy["profiles"][0]["device_id"] = "rk3566-my355" +elif mode == "alternate-core": + policy["profiles"][0]["core_binary"] = "gearboy_libretro.so" +elif mode == "stale-filter": + policy["profiles"][0]["effect"]["descriptor"]["sha256"] = "0" * 64 +elif mode == "unsafe-path": + policy["profiles"][0]["effect"]["descriptor"]["runtime_path"] = "/tmp/evil.filt" +elif mode == "unreviewed": + policy["profiles"][0]["qualification"]["review_status"] = "assumed" +elif mode == "duplicate-selector": + policy["profiles"][1]["system_id"] = "gb" +elif mode == "unreduced-aspect": + policy["profiles"][0]["source_geometry"]["display_aspect"] = {"numerator": 20, "denominator": 18} +else: + raise SystemExit("unknown mutation") +json.dump(policy, open(output, "w", encoding="utf-8"), indent=2) +PY +} + +@test "built-in display authority validates exact reviewed Plus profiles and preserves Auto" { + run validate_profiles "$POLICY" + [ "$status" -eq 0 ] + [ "$output" = "built-in display profiles valid: 3 Plus profiles, 2 filter identities, Auto preserved" ] +} + +@test "signed payload packaging includes the exact built-in profile authority" { + grep -F '@cp $(ROOT_DIR)/build/display-built-in-profiles.json $(BUILD_DIR)/.tmp_update/config/display-built-in-profiles.json' /workspace/Makefile +} + +@test "built-in profile resolution is exact by device system and core" { + run resolve_profile --device miyoo-mini-plus --system gb --core gambatte_libretro.so + [ "$status" -eq 0 ] + printf '%s' "$output" | jq -e ' + .status == "reviewed" and .profile_id == "plus-gb-gambatte-lcd3x-v1" and + .source_geometry == {width:160,height:144,rotation_quarter_turns:0, + display_aspect:{numerator:10,denominator:9},authority:"reviewed-system-core-profile"} and + .effect.class == "lcd" and .effect.bytes == 22 and + .effect.runtime_path == "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt"' >/dev/null + + run resolve_profile --device miyoo-mini-plus --system gba --core gpsp_libretro.so + [ "$status" -eq 0 ] + printf '%s' "$output" | jq -e '.status == "reviewed" and .source_geometry.width == 240 and .effect.id == "lcd2x"' >/dev/null +} + +@test "unreviewed devices systems and alternate cores resolve unavailable" { + for selector in \ + "miyoo-mini-v4 gb gambatte_libretro.so" \ + "miyoo-mini-flip-my285 gba gpsp_libretro.so" \ + "miyoo-mini-plus gb gearboy_libretro.so" \ + "miyoo-mini-plus nes fceumm_libretro.so"; do + set -- $selector + run resolve_profile --device "$1" --system "$2" --core "$3" + [ "$status" -eq 0 ] + [ "$output" = '{"profile_id":null,"schema":1,"status":"unavailable"}' ] + done +} + +@test "unsafe stale guessed coupled and Auto-changing profile mutations fail closed" { + for mode in enable-auto unsupported-device alternate-core stale-filter unsafe-path unreviewed \ + duplicate-selector unreduced-aspect; do + candidate="$BATS_TEST_TMPDIR/$mode.json" + mutate_profiles "$mode" "$candidate" + run validate_profiles "$candidate" + [ "$status" -ne 0 ] + [[ "$output" == *"built-in display profiles:"* ]] + done +} diff --git a/test/shell/display_profile_policy.bats b/test/shell/display_profile_policy.bats index b32a093c9..1bdb7e9bf 100644 --- a/test/shell/display_profile_policy.bats +++ b/test/shell/display_profile_policy.bats @@ -35,7 +35,9 @@ elif mode == "unsafe-framebuffer-fallback": elif mode == "raw-custom-authority": policy["capability_contract"]["signed-custom-effect"]["authority"] = "raw-filesystem-path" elif mode == "premature-integration": - policy["scope"]["runtime_integration"] = "enabled" + policy["scope"]["runtime_integration"] = "deferred-to-DISP-03" +elif mode == "stale-built-ins": + policy["authorities"]["built_in_profiles"]["sha256"] = "0" * 64 else: raise SystemExit("unknown mutation") json.dump(policy, open(output, "w", encoding="utf-8"), indent=2) @@ -67,8 +69,8 @@ assert len(owned) == len(set(owned)) == 23 assert set(owned) == set(baseline["display_keys"]) assert policy["semantic_axes"]["sampling"]["public_control"] is None assert policy["scope"]["permanent_retroarch_config_mutation"] is False -assert policy["scope"]["runtime_integration"] == "deferred-to-DISP-03" -assert policy["scope"]["built_in_profiles"] == "deferred-to-DISP-04" +assert policy["scope"]["runtime_integration"] == "enabled-in-DISP-03" +assert policy["scope"]["built_in_profiles"] == "enabled-in-DISP-04" assert policy["scope"]["custom_packs"] == "deferred-to-DISP-05" payload = open(root + policy["authorities"]["auto_baseline"]["path"], "rb").read() assert hashlib.sha256(payload).hexdigest() == policy["authorities"]["auto_baseline"]["sha256"] @@ -146,7 +148,7 @@ PY @test "display profile policy rejects stale unsafe coupled and premature mutations" { for mode in stale-baseline unsupported-device cross-axis-key missing-mode custom-path \ - unsafe-framebuffer-fallback raw-custom-authority premature-integration; do + unsafe-framebuffer-fallback raw-custom-authority premature-integration stale-built-ins; do candidate="$BATS_TEST_TMPDIR/$mode.json" mutate_policy "$mode" "$candidate" run validate_policy "$candidate" diff --git a/test/shell/display_session_prepare.bats b/test/shell/display_session_prepare.bats index a7b517cb0..2f76c2787 100644 --- a/test/shell/display_session_prepare.bats +++ b/test/shell/display_session_prepare.bats @@ -8,6 +8,8 @@ setup() { export BLOOM_SETTINGS_BIN="$MOCK_BIN/bloom-settings" export BLOOM_PLATFORM_BIN="$MOCK_BIN/bloom-platform" export BLOOM_JQ_BIN="$(command -v jq)" + export BLOOM_SHA256_BIN="$(command -v sha256sum)" + export BLOOM_DISPLAY_BUILT_IN_PROFILES=/workspace/build/display-built-in-profiles.json mkdir -p "$BLOOM_SESSION_ROOT" export REQUEST="$BLOOM_TEST_ROOT/request.json" printf '%s\n' '{"schema":2}' >"$REQUEST" @@ -15,12 +17,19 @@ setup() { #!/bin/sh case "$1" in validate) [ -f "$2" ] ;; - get) [ "$3" = emulator_type ] && printf '%s\n' retroarch ;; + get) + case "$3" in + emulator_type) printf '%s\n' retroarch ;; + system_id) printf '%s\n' gb ;; + core) printf '%s\n' gambatte_libretro.so ;; + *) exit 1 ;; + esac + ;; write-display-config) printf '%s\n' "$*" >>"$MOCK_LOG" printf '%s\n' 'video_fullscreen = "true"' >"$2" chmod 600 "$2" - printf '%s\n' '{"schema":1,"status":"applied","effective":{"scaling":"fill","screen_effect":"none","bezel":"none"},"reason":null}' + printf '{"schema":1,"status":"applied","effective":{"scaling":"%s","screen_effect":"%s","bezel":"%s"},"reason":null}\n' "$3" "$5" "$6" ;; *) exit 1 ;; esac @@ -34,7 +43,7 @@ EOF chmod +x "$BLOOM_SETTINGS_BIN" cat >"$BLOOM_PLATFORM_BIN" <<'EOF' #!/bin/sh -case "$1" in display-width) printf '%s\n' 640 ;; display-height) printf '%s\n' 480 ;; *) exit 1 ;; esac +case "$1" in display-width) printf '%s\n' 640 ;; display-height) printf '%s\n' 480 ;; model) printf '%s\n' mini_plus ;; *) exit 1 ;; esac EOF chmod +x "$BLOOM_PLATFORM_BIN" } @@ -46,7 +55,7 @@ teardown() { teardown_bloom_fixture; } [ "$status" -eq 0 ] printf '%s' "$output" | "$BLOOM_JQ_BIN" -e '.status == "applied"' >/dev/null - grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg fill nearest none none 640 480" "$MOCK_LOG" + grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg fill nearest none none 640 480 160 144 10 9 0 -" "$MOCK_LOG" [ "$(stat -c '%a' "$BLOOM_SESSION_ROOT/ra.cfg")" = 600 ] } @@ -61,10 +70,29 @@ EOF run "$PREPARE" "$REQUEST" [ "$status" -eq 0 ] - grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg auto auto auto auto 640 480" "$MOCK_LOG" + grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg auto auto auto auto 640 480 160 144 10 9 0 -" "$MOCK_LOG" [ "$(stat -c '%a' "$BLOOM_SESSION_ROOT/ra.cfg")" = 600 ] } +@test "reviewed Plus geometry and exact LCD filter feed the composer without changing Auto" { + mkdir -p /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x + cp /workspace/static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt \ + /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt + cat >"$BLOOM_SETTINGS_BIN" <<'EOF' +#!/bin/sh +printf '%s\n' '{"schema":1,"service":"bloom-settings","display":{"scaling":"integer-pixels","sampling":"nearest","screen_effect":"lcd","bezel":"auto"}}' +EOF + chmod +x "$BLOOM_SETTINGS_BIN" + + run "$PREPARE" "$REQUEST" + + [ "$status" -eq 0 ] + printf '%s' "$output" | "$BLOOM_JQ_BIN" -e \ + '.profile_id == "plus-gb-gambatte-lcd3x-v1" and .effective.screen_effect == "lcd"' >/dev/null + grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg integer-pixels nearest lcd auto 640 480 160 144 10 9 0 /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt" "$MOCK_LOG" + rm -f /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt +} + @test "malformed canonical display settings fail without a session config" { cat >"$BLOOM_SETTINGS_BIN" <<'EOF' #!/bin/sh diff --git a/test/test_bloom_launch.cpp b/test/test_bloom_launch.cpp index c20ab47f4..0a83301f7 100644 --- a/test/test_bloom_launch.cpp +++ b/test/test_bloom_launch.cpp @@ -504,6 +504,7 @@ TEST_F(BloomLaunchTest, DisplayAndAchievementsShareOneTrustedSessionConfig) char reason[48] = {}; ASSERT_EQ(0, bloom_launch_write_display_config( config_path.c_str(), "fill", "linear", "none", "none", 640, 480, + 0, 0, 0, 0, 0, nullptr, scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), reason, sizeof(reason), error_, sizeof(error_))) << error_; diff --git a/test/test_bloom_retroarch_config.cpp b/test/test_bloom_retroarch_config.cpp index 8d0bf6387..5830c3feb 100644 --- a/test/test_bloom_retroarch_config.cpp +++ b/test/test_bloom_retroarch_config.cpp @@ -146,6 +146,7 @@ TEST_F(BloomRetroarchConfigTest, AppliesIndependentDisplayAxesAndSafeUnavailable char reason[48] = {}; ASSERT_EQ(0, bloom_retroarch_config_apply_display( config, "fill", "nearest", "none", "none", 640, 480, + 0, 0, 0, 0, 0, nullptr, scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), reason, sizeof(reason), error_, sizeof(error_))) << error_; @@ -172,6 +173,7 @@ TEST_F(BloomRetroarchConfigTest, AppliesIndependentDisplayAxesAndSafeUnavailable ASSERT_NE(nullptr, config); ASSERT_EQ(0, bloom_retroarch_config_apply_display( config, "integer-pixels", "auto", "lcd", "custom", 640, 480, + 0, 0, 0, 0, 0, nullptr, scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), reason, sizeof(reason), error_, sizeof(error_))) << error_; @@ -187,6 +189,82 @@ TEST_F(BloomRetroarchConfigTest, AppliesIndependentDisplayAxesAndSafeUnavailable bloom_retroarch_config_destroy(config); } +TEST_F(BloomRetroarchConfigTest, AppliesReviewedSourceGeometryAndFixedEffectPath) +{ + static const char filter[] = + "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt"; + BloomRetroarchConfig *config = bloom_retroarch_config_create(); + ASSERT_NE(nullptr, config); + char scaling[24] = {}; + char effect[16] = {}; + char bezel[16] = {}; + char reason[48] = {}; + ASSERT_EQ(0, bloom_retroarch_config_apply_display( + config, "integer-pixels", "nearest", "lcd", "auto", 640, 480, + 160, 144, 10, 9, 0, filter, + scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), + reason, sizeof(reason), error_, sizeof(error_))) + << error_; + EXPECT_STREQ("integer-pixels", scaling); + EXPECT_STREQ("lcd", effect); + EXPECT_STREQ("auto", bezel); + EXPECT_STREQ("", reason); + char output[4096] = {}; + size_t written = 0; + ASSERT_EQ(0, bloom_retroarch_config_render(config, output, sizeof(output), &written, + error_, sizeof(error_))) + << error_; + std::string rendered(output, written); + EXPECT_NE(rendered.find("custom_viewport_width = \"480\""), std::string::npos); + EXPECT_NE(rendered.find("custom_viewport_height = \"432\""), std::string::npos); + EXPECT_NE(rendered.find("custom_viewport_x = \"80\""), std::string::npos); + EXPECT_NE(rendered.find("custom_viewport_y = \"24\""), std::string::npos); + EXPECT_NE(rendered.find("video_scale_integer = \"true\""), std::string::npos); + EXPECT_NE(rendered.find(std::string("video_filter = \"") + filter + "\""), std::string::npos); + EXPECT_NE(rendered.find("video_shader_enable = \"false\""), std::string::npos); + bloom_retroarch_config_destroy(config); + + config = bloom_retroarch_config_create(); + ASSERT_NE(nullptr, config); + ASSERT_EQ(0, bloom_retroarch_config_apply_display( + config, "correct-aspect", "auto", "none", "none", 752, 560, + 256, 224, 4, 3, 0, nullptr, + scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), + reason, sizeof(reason), error_, sizeof(error_))) + << error_; + memset(output, 0, sizeof(output)); + written = 0; + ASSERT_EQ(0, bloom_retroarch_config_render(config, output, sizeof(output), &written, + error_, sizeof(error_))) + << error_; + rendered.assign(output, written); + EXPECT_NE(rendered.find("custom_viewport_width = \"747\""), std::string::npos); + EXPECT_NE(rendered.find("custom_viewport_height = \"560\""), std::string::npos); + EXPECT_NE(rendered.find("custom_viewport_x = \"2\""), std::string::npos); + EXPECT_NE(rendered.find("video_scale_integer = \"false\""), std::string::npos); + bloom_retroarch_config_destroy(config); +} + +TEST_F(BloomRetroarchConfigTest, RejectsUnreviewedEffectPathsBeforeAnyContribution) +{ + BloomRetroarchConfig *config = bloom_retroarch_config_create(); + ASSERT_NE(nullptr, config); + char scaling[24] = {}; + char effect[16] = {}; + char bezel[16] = {}; + char reason[48] = {}; + EXPECT_NE(0, bloom_retroarch_config_apply_display( + config, "auto", "auto", "lcd", "auto", 640, 480, + 160, 144, 10, 9, 0, "/tmp/unreviewed.filt", + scaling, sizeof(scaling), effect, sizeof(effect), bezel, sizeof(bezel), + reason, sizeof(reason), error_, sizeof(error_))); + char output[64] = {}; + size_t written = 0; + EXPECT_NE(0, bloom_retroarch_config_render(config, output, sizeof(output), &written, + error_, sizeof(error_))); + bloom_retroarch_config_destroy(config); +} + TEST_F(BloomRetroarchConfigTest, ImportsDisplayConfigBeforeAddingOtherOwnedContributions) { auto existing = root_ / "display.cfg"; diff --git a/test/test_ci_scope.sh b/test/test_ci_scope.sh index c6532c853..204bb1f51 100755 --- a/test/test_ci_scope.sh +++ b/test/test_ci_scope.sh @@ -79,6 +79,10 @@ expect "none" static-analysis "build/content-identity-policy.json tools/validate_content_identity_policy.py test/shell/content_identity_policy.bats" expect "bloomLaunch bloomShell gameSwitcher" build "src/bloomLaunch/bloom_launch.c" +expect "none" build "build/display-built-in-profiles.json +tools/display_built_in_profiles.py" +expect "none" static-analysis "build/display-built-in-profiles.json +tools/display_built_in_profiles.py" expect "playActivity gameSwitcher" build "src/playActivity/playActivityModel.c" expect "bloomSettings bloomShell" build "src/bloomSettings/bloom_settings.c src/bloomShell/main.c @@ -151,6 +155,13 @@ expect "/workspace/test/shell/libretro_artwork_policy.bats" shell-tests "build/l tools/validate_libretro_artwork_policy.py" expect "2" risk "build/libretro-artwork-policy.json tools/validate_libretro_artwork_policy.py" +expect "2" risk "build/display-built-in-profiles.json +tools/display_built_in_profiles.py" +expect "BloomRetroarchConfig*:BloomLaunch*" native-tests "build/display-built-in-profiles.json" +expect "/workspace/test/shell/display_built_in_profiles.bats /workspace/test/shell/display_profile_policy.bats /workspace/test/shell/display_session_prepare.bats" shell-tests "tools/display_built_in_profiles.py" +expect "BloomRetroarchConfig*:BloomLaunch*" native-tests "src/bloomLaunch/bloom_retroarch_config.c" +expect "/workspace/test/shell/display_session_prepare.bats" shell-tests "src/bloomLaunch/bloom_retroarch_config.h" +expect "/workspace/test/shell/display_built_in_profiles.bats /workspace/test/shell/display_session_prepare.bats" shell-tests "static/build/.tmp_update/bin/bloom-display-session-prepare" expect "full" native-tests "src/common/system/display.h" expect "none" native-tests "docs/ARCHITECTURE.md" expect "2" risk "src/sendkeys/sendkeys.c" diff --git a/tools/ci-scope.sh b/tools/ci-scope.sh index d86668af8..71bba9d6a 100755 --- a/tools/ci-scope.sh +++ b/tools/ci-scope.sh @@ -275,9 +275,11 @@ while IFS= read -r path; do build/core-inventory-disposition*.json|\ build/save-migration-policy*.json|\ build/transfer-policy*.json|\ + build/display-profile-policy.json|build/display-built-in-profiles.json|\ static/*|assets/ui/*|test/*|docs/*|tools/legacy_manifest.py|\ tools/generate_bloom_ui_assets.py|tools/project_bloom_lifecycle_assets.py|tools/validate_bloom_ui_assets.py|tools/requirements-ui-assets.txt|\ - tools/validate_core_selection_policy.py|tools/validate_core_inventory_disposition.py|tools/validate_save_migration_policy.py|tools/validate_transfer_policy.py|tools/ci-scope.sh) + tools/validate_core_selection_policy.py|tools/validate_core_inventory_disposition.py|tools/validate_save_migration_policy.py|tools/validate_transfer_policy.py|\ + tools/display_profile_policy.py|tools/display_built_in_profiles.py|tools/ci-scope.sh) ;; *) full=1 @@ -346,6 +348,29 @@ while IFS= read -r path; do add_native_filter 'BloomShellMedia*' add_shell_target /workspace/test/shell/libretro_artwork_policy.bats ;; + build/display-profile-policy.json|tools/display_profile_policy.py|\ + build/display-built-in-profiles.json|tools/display_built_in_profiles.py) + raise_risk 2 + add_native_filter 'BloomRetroarchConfig*' + add_native_filter 'BloomLaunch*' + add_shell_target /workspace/test/shell/display_built_in_profiles.bats + add_shell_target /workspace/test/shell/display_profile_policy.bats + add_shell_target /workspace/test/shell/display_session_prepare.bats + ;; + src/bloomLaunch/bloom_retroarch_config.c|src/bloomLaunch/bloom_retroarch_config.h|\ + test/test_bloom_retroarch_config.cpp) + raise_risk 2 + add_native_filter 'BloomRetroarchConfig*' + add_native_filter 'BloomLaunch*' + add_shell_target /workspace/test/shell/display_session_prepare.bats + ;; + static/build/.tmp_update/bin/bloom-display-session-prepare) + raise_risk 2 + add_native_filter 'BloomRetroarchConfig*' + add_native_filter 'BloomLaunch*' + add_shell_target /workspace/test/shell/display_built_in_profiles.bats + add_shell_target /workspace/test/shell/display_session_prepare.bats + ;; src/bloomLibrary/bloom_library_jobs.c|src/bloomLibrary/bloom_library_jobs.h|\ test/test_bloom_library_jobs.cpp) raise_risk 2 @@ -546,7 +571,12 @@ while IFS= read -r path; do raise_risk 2 add_shell_target /workspace/test/shell/bloom_device.bats ;; - static/build/.tmp_update/bin/bloom-dev-deploy|static/build/.tmp_update/runtime.sh) + static/build/.tmp_update/bin/bloom-dev-deploy) + raise_risk 3 + add_shell_target /workspace/test/shell/bloom_device.bats + add_shell_target /workspace/test/shell/dev_deploy.bats + ;; + static/build/.tmp_update/runtime.sh) raise_risk 3 ;; static/build/.tmp_update/script/network/*) diff --git a/tools/dev-deploy-manifest.py b/tools/dev-deploy-manifest.py index 075177fcb..dc13e9ddb 100755 --- a/tools/dev-deploy-manifest.py +++ b/tools/dev-deploy-manifest.py @@ -40,6 +40,7 @@ "bin/bloom-session", "bin/bloom-ra", "bin/bloom-ra-session-prepare", + "config/display-built-in-profiles.json", "script/bloom-launch-trace.sh", }, "network-helpers": { diff --git a/tools/display_built_in_profiles.py b/tools/display_built_in_profiles.py new file mode 100644 index 000000000..a99618009 --- /dev/null +++ b/tools/display_built_in_profiles.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +"""Validate and resolve the DISP-04 built-in display profile authority.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import math +import re +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional + + +DEVICES = [ + "miyoo-mini-v1", + "miyoo-mini-v2-v3", + "miyoo-mini-v4", + "miyoo-mini-plus", + "miyoo-mini-flip-my285", +] +TOP_LEVEL_KEYS = { + "schema", + "policy_id", + "scope", + "authorities", + "activation_contract", + "profiles", + "unsupported", +} +AUTHORITY_KEYS = {"core_selection_policy", "auto_baseline", "filter_contract", "retroarch_license"} +PROFILE_KEYS = { + "id", + "device_id", + "system_id", + "candidate_id", + "core_binary", + "source_geometry", + "effect", + "qualification", +} +SAFE_ID = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$") +SAFE_CORE = re.compile(r"^[a-z0-9_]+_libretro\.so$") +SAFE_SHA256 = re.compile(r"^[0-9a-f]{64}$") +RUNTIME_FILTER_PREFIX = "/mnt/SDCARD/RetroArch/.retroarch/filters/video/" +SOURCE_FILTER_PREFIX = "static/build/RetroArch/.retroarch/filters/video/" + + +def load_json(path: Path, label: str) -> Any: + if path.is_symlink() or not path.is_file(): + raise ValueError(f"{label} is unavailable or linked: {path}") + try: + return json.loads(path.read_text(encoding="utf-8")) + except (OSError, UnicodeError, json.JSONDecodeError) as error: + raise ValueError(f"{label} is unreadable: {error}") from error + + +def safe_repository_path(root: Path, relative: Any, label: str) -> Path: + if not isinstance(relative, str) or not relative or "\\" in relative: + raise ValueError(f"{label} path is invalid") + candidate = Path(relative) + if candidate.is_absolute() or any(part in ("", ".", "..") for part in candidate.parts): + raise ValueError(f"{label} path is unsafe") + resolved = root.joinpath(*candidate.parts) + if resolved.is_symlink() or not resolved.is_file(): + raise ValueError(f"{label} is unavailable or linked") + return resolved + + +def verified_file(root: Path, record: Any, label: str, include_size: bool = False) -> Path: + keys = {"source_path", "bytes", "sha256"} if include_size else {"path", "hash_mode", "sha256"} + if not isinstance(record, dict) or set(record) != keys: + raise ValueError(f"{label} record is invalid") + relative = record.get("source_path") if include_size else record.get("path") + path = safe_repository_path(root, relative, label) + payload = path.read_bytes() + if include_size: + size = record.get("bytes") + if not isinstance(size, int) or isinstance(size, bool) or size <= 0 or len(payload) != size: + raise ValueError(f"{label} byte count is stale") + else: + mode = record.get("hash_mode") + if mode == "text-lf": + payload = payload.replace(b"\r\n", b"\n") + if b"\r" in payload: + raise ValueError(f"{label} contains a bare carriage return") + elif mode != "raw": + raise ValueError(f"{label} hash mode is invalid") + expected = record.get("sha256") + if not isinstance(expected, str) or not SAFE_SHA256.fullmatch(expected) or hashlib.sha256(payload).hexdigest() != expected: + raise ValueError(f"{label} SHA-256 is stale") + return path + + +def core_candidate(policy: Any, system_id: str, candidate_id: str) -> Optional[Dict[str, Any]]: + if not isinstance(policy, dict): + return None + for system in policy.get("systems", []): + if isinstance(system, dict) and system.get("id") == system_id: + for candidate in system.get("candidates", []): + if isinstance(candidate, dict) and candidate.get("id") == candidate_id: + return candidate + return None + + +def validate(policy: Any, root: Path) -> List[str]: + failures: List[str] = [] + if not isinstance(policy, dict) or set(policy) != TOP_LEVEL_KEYS: + return ["exact schema-1 built-in profile fields are required"] + if policy.get("schema") != 1 or policy.get("policy_id") != "bloomos-display-built-in-profiles-v1": + failures.append("stable schema-1 built-in profile authority is required") + + if policy.get("scope") != { + "product": "ssd202d-only", + "devices": DEVICES, + "runtime_device": "miyoo-mini-plus", + "auto_selection": False, + "auto_behavior": "preserve-disp01-baseline", + "physical_certification": False, + }: + failures.append("scope must remain Plus-only at runtime and preserve Auto without certification claims") + if policy.get("activation_contract") != { + "selection_key": "exact-device-system-core", + "review_status": "source-reviewed", + "missing_profile": "unavailable-without-mutation", + "filter_path": "fixed-signed-runtime-path-never-user-input", + "filter_integrity": "exact-size-and-sha256-before-session-composition", + "axis_independence": "profile-supplies-capabilities-without-changing-selections", + }: + failures.append("activation, integrity, and axis-independence contracts drift") + if policy.get("unsupported") != { + "devices": ["miyoo-mini-v1", "miyoo-mini-v2-v3", "miyoo-mini-v4", "miyoo-mini-flip-my285"], + "effect_classes": ["crt"], + "artwork": "no-reviewed-built-in-bezels", + "alternate_cores": "explicitly-unavailable-until-reviewed", + "behavior": "preserve-auto-or-report-unavailable-without-partial-config", + }: + failures.append("unsupported device, CRT, bezel, and alternate-core dispositions must remain explicit") + + authorities = policy.get("authorities") + if not isinstance(authorities, dict) or set(authorities) != AUTHORITY_KEYS: + failures.append("complete core, Auto, filter-contract, and license authorities are required") + authorities = {} + core_policy: Any = {} + baseline: Any = {} + filter_contract = "" + license_text = "" + for key, label in ( + ("core_selection_policy", "core-selection policy"), + ("auto_baseline", "Auto baseline"), + ("filter_contract", "RetroArch filter contract"), + ("retroarch_license", "RetroArch license"), + ): + try: + path = verified_file(root, authorities.get(key), label) + if key == "core_selection_policy": + core_policy = load_json(path, label) + elif key == "auto_baseline": + baseline = load_json(path, label) + elif key == "filter_contract": + filter_contract = path.read_text(encoding="utf-8") + else: + license_text = path.read_text(encoding="utf-8") + except (OSError, UnicodeError, ValueError) as error: + failures.append(str(error)) + if core_policy.get("policy_id") != "bloomos-core-selection-v1": + failures.append("profiles must join the canonical core-selection authority") + if baseline.get("policy_id") != "bloomos-display-auto-baseline-v1": + failures.append("profiles must join the DISP-01 Auto authority") + if "LCD filters should always be used with Integer:ON Aspect:ON" not in filter_contract or "up to 320pixels width for 2x" not in filter_contract or "up to 213pixels width for 3x" not in filter_contract: + failures.append("packaged Miyoo filter constraints are unavailable") + if "GNU GENERAL PUBLIC LICENSE" not in license_text or "Version 3" not in license_text: + failures.append("RetroArch GPL-3.0 license authority is unavailable") + + profiles = policy.get("profiles") + if not isinstance(profiles, list) or not profiles: + return failures + ["at least one reviewed built-in profile is required"] + seen_ids = set() + seen_keys = set() + baseline_pairs = { + (row.get("system_id"), row.get("core_binary")) + for row in baseline.get("systems", []) + if isinstance(row, dict) + } + for index, profile in enumerate(profiles): + label = f"profile {index}" + if not isinstance(profile, dict) or set(profile) != PROFILE_KEYS: + failures.append(f"{label}: exact fields are required") + continue + profile_id = profile.get("id") + device_id = profile.get("device_id") + system_id = profile.get("system_id") + candidate_id = profile.get("candidate_id") + core_binary = profile.get("core_binary") + if not isinstance(profile_id, str) or not SAFE_ID.fullmatch(profile_id) or profile_id in seen_ids: + failures.append(f"{label}: profile ID is invalid or duplicated") + seen_ids.add(profile_id) + selection_key = (device_id, system_id, core_binary) + if selection_key in seen_keys: + failures.append(f"{label}: exact device/system/core selector is duplicated") + seen_keys.add(selection_key) + if device_id != "miyoo-mini-plus" or not isinstance(system_id, str) or not SAFE_ID.fullmatch(system_id) or not isinstance(candidate_id, str) or not SAFE_ID.fullmatch(candidate_id) or not isinstance(core_binary, str) or not SAFE_CORE.fullmatch(core_binary): + failures.append(f"{label}: selector is outside the reviewed Plus/system/core boundary") + candidate = core_candidate(core_policy, system_id, candidate_id) + if candidate is None or candidate.get("emulator_id") != "libretro:" + core_binary[:-len("_libretro.so")] or "miyoo-mini-plus" not in candidate.get("default_for_devices", []): + failures.append(f"{label}: selector does not name the canonical Plus default candidate") + if (system_id, core_binary) not in baseline_pairs: + failures.append(f"{label}: selector is absent from the exact Auto baseline") + + geometry = profile.get("source_geometry") + if not isinstance(geometry, dict) or set(geometry) != {"width", "height", "rotation_quarter_turns", "display_aspect", "authority"}: + failures.append(f"{label}: exact source geometry is required") + else: + width = geometry.get("width") + height = geometry.get("height") + rotation = geometry.get("rotation_quarter_turns") + aspect = geometry.get("display_aspect") + if not isinstance(width, int) or isinstance(width, bool) or not 1 <= width <= 4096 or not isinstance(height, int) or isinstance(height, bool) or not 1 <= height <= 4096 or rotation not in (0, 1, 2, 3) or geometry.get("authority") != "reviewed-system-core-profile": + failures.append(f"{label}: source geometry bounds or authority are invalid") + if not isinstance(aspect, dict) or set(aspect) != {"numerator", "denominator"}: + failures.append(f"{label}: reduced display aspect is required") + else: + numerator = aspect.get("numerator") + denominator = aspect.get("denominator") + if not isinstance(numerator, int) or isinstance(numerator, bool) or not isinstance(denominator, int) or isinstance(denominator, bool) or not 1 <= numerator <= 65535 or not 1 <= denominator <= 65535 or math.gcd(numerator, denominator) != 1: + failures.append(f"{label}: display aspect must be positive, bounded, and reduced") + + effect = profile.get("effect") + if not isinstance(effect, dict) or set(effect) != {"id", "class", "descriptor", "implementation"} or effect.get("class") not in ("lcd", "crt") or not isinstance(effect.get("id"), str) or not SAFE_ID.fullmatch(effect.get("id", "")): + failures.append(f"{label}: exact bounded LCD/CRT effect identity is required") + else: + descriptor = effect.get("descriptor") + implementation = effect.get("implementation") + if not isinstance(descriptor, dict) or set(descriptor) != {"source_path", "runtime_path", "bytes", "sha256"}: + failures.append(f"{label}: exact filter descriptor identity is required") + else: + source_path = descriptor.get("source_path") + runtime_path = descriptor.get("runtime_path") + expected_runtime = "/mnt/SDCARD/" + source_path[len("static/build/"):] if isinstance(source_path, str) and source_path.startswith(SOURCE_FILTER_PREFIX) else "" + if runtime_path != expected_runtime or not isinstance(runtime_path, str) or not runtime_path.startswith(RUNTIME_FILTER_PREFIX) or ".." in runtime_path or "\\" in runtime_path or any(ord(character) < 0x21 or ord(character) > 0x7e or character == '"' for character in runtime_path): + failures.append(f"{label}: runtime filter path is not fixed and safe") + try: + descriptor_path = verified_file(root, {"source_path": source_path, "bytes": descriptor.get("bytes"), "sha256": descriptor.get("sha256")}, f"{label} descriptor", True) + descriptor_text = descriptor_path.read_text(encoding="utf-8") + if not re.fullmatch(r"filter = [a-z0-9_]+\n", descriptor_text): + failures.append(f"{label}: filter descriptor is not a single bounded built-in identity") + except (OSError, UnicodeError, ValueError) as error: + failures.append(str(error)) + try: + implementation_path = verified_file(root, implementation, f"{label} implementation", True) + if implementation_path.suffix != ".so": + failures.append(f"{label}: implementation is not a packaged filter binary") + except ValueError as error: + failures.append(str(error)) + + qualification = profile.get("qualification") + if not isinstance(qualification, dict) or set(qualification) != {"review_status", "device_evidence_status", "evidence_ids"} or qualification.get("review_status") != "source-reviewed" or qualification.get("device_evidence_status") not in ("pending", "remote-pass", "physical-pass") or qualification.get("evidence_ids") != ["retroarch-miyoomini-filter-contract"]: + failures.append(f"{label}: source review and honest device-evidence status are required") + + if [profile.get("id") for profile in profiles if isinstance(profile, dict)] != [ + "plus-gb-gambatte-lcd3x-v1", + "plus-gbc-gambatte-lcd3x-v1", + "plus-gba-gpsp-lcd2x-v1", + ]: + failures.append("profiles must retain the stable GB, GBC, and GBA order") + required = { + ("miyoo-mini-plus", "gb", "gambatte_libretro.so"), + ("miyoo-mini-plus", "gbc", "gambatte_libretro.so"), + ("miyoo-mini-plus", "gba", "gpsp_libretro.so"), + } + if seen_keys != required: + failures.append("exact reviewed Plus GB, GBC, and GBA default profile set is required") + return failures + + +def resolve(policy: Dict[str, Any], device: str, system: str, core: str) -> Dict[str, Any]: + matches = [ + profile + for profile in policy["profiles"] + if profile["device_id"] == device and profile["system_id"] == system and profile["core_binary"] == core and profile["qualification"]["review_status"] == "source-reviewed" + ] + if len(matches) != 1: + return {"schema": 1, "status": "unavailable", "profile_id": None} + profile = matches[0] + return { + "schema": 1, + "status": "reviewed", + "profile_id": profile["id"], + "source_geometry": profile["source_geometry"], + "effect": { + "id": profile["effect"]["id"], + "class": profile["effect"]["class"], + "runtime_path": profile["effect"]["descriptor"]["runtime_path"], + "bytes": profile["effect"]["descriptor"]["bytes"], + "sha256": profile["effect"]["descriptor"]["sha256"], + }, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("command", choices=("validate", "resolve")) + parser.add_argument("--repository", type=Path, required=True) + parser.add_argument("--policy", type=Path, required=True) + parser.add_argument("--device") + parser.add_argument("--system") + parser.add_argument("--core") + args = parser.parse_args() + try: + root = args.repository.resolve() + if not root.is_dir(): + raise ValueError("repository root is unavailable") + policy = load_json(args.policy.resolve(), "built-in display profiles") + failures = validate(policy, root) + if failures: + for failure in failures: + print(f"built-in display profiles: {failure}", file=sys.stderr) + return 1 + if args.command == "validate": + print("built-in display profiles valid: 3 Plus profiles, 2 filter identities, Auto preserved") + return 0 + if not args.device or not args.system or not args.core: + raise ValueError("resolve requires device, system, and core") + print(json.dumps(resolve(policy, args.device, args.system, args.core), separators=(",", ":"), sort_keys=True)) + return 0 + except (OSError, UnicodeError, ValueError) as error: + print(f"built-in display profiles: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/display_profile_policy.py b/tools/display_profile_policy.py index 7ae81a7e9..c768af7df 100644 --- a/tools/display_profile_policy.py +++ b/tools/display_profile_policy.py @@ -219,8 +219,8 @@ def validate(policy: Any, root: Path) -> list[str]: expected_scope = { "product": "ssd202d-only", "devices": DEVICES, - "runtime_integration": "deferred-to-DISP-03", - "built_in_profiles": "deferred-to-DISP-04", + "runtime_integration": "enabled-in-DISP-03", + "built_in_profiles": "enabled-in-DISP-04", "custom_packs": "deferred-to-DISP-05", "permanent_retroarch_config_mutation": False, "physical_certification": False, @@ -229,11 +229,12 @@ def validate(policy: Any, root: Path) -> list[str]: failures.append("scope must remain SSD202D-only and preserve deferred integration boundaries") authorities = policy.get("authorities") - if not isinstance(authorities, dict) or set(authorities) != {"auto_baseline", "platform_helper"}: - failures.append("DISP-01 and PLAT-01 authority records are required") + if not isinstance(authorities, dict) or set(authorities) != {"auto_baseline", "platform_helper", "built_in_profiles"}: + failures.append("DISP-01, PLAT-01, and DISP-04 authority records are required") authorities = {} baseline = None platform_source = "" + built_in_profiles = None try: baseline_path = authority_file(root, authorities.get("auto_baseline"), "Auto baseline") baseline = load_json(baseline_path, "Auto baseline") @@ -244,6 +245,11 @@ def validate(policy: Any, root: Path) -> list[str]: platform_source = platform_path.read_text(encoding="utf-8") except (ValueError, OSError, UnicodeError) as error: failures.append(str(error)) + try: + profiles_path = authority_file(root, authorities.get("built_in_profiles"), "built-in profiles") + built_in_profiles = load_json(profiles_path, "built-in profiles") + except ValueError as error: + failures.append(str(error)) if not isinstance(baseline, dict) or baseline.get("policy_id") != "bloomos-display-auto-baseline-v1": failures.append("authority must join the DISP-01 Auto baseline") @@ -272,6 +278,13 @@ def validate(policy: Any, root: Path) -> list[str]: ): if token not in platform_source: failures.append(f"platform helper lacks required geometry contract: {token}") + if ( + not isinstance(built_in_profiles, dict) + or built_in_profiles.get("policy_id") != "bloomos-display-built-in-profiles-v1" + or built_in_profiles.get("scope", {}).get("auto_behavior") != "preserve-disp01-baseline" + or built_in_profiles.get("scope", {}).get("auto_selection") is not False + ): + failures.append("DISP-04 authority must preserve the DISP-01 Auto baseline") if policy.get("public_controls") != PUBLIC_CONTROLS: failures.append("public controls must be Scaling, Screen Effect, and Bezel with exact ordered values") From 272f6ff2d2a51ed85d5359421ce7f4f9999c7eb1 Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 22:13:34 -0400 Subject: [PATCH 2/5] fix(display): pin canonical filter payload bytes --- .gitattributes | 1 + build/display-built-in-profiles.json | 18 ++++++++++------ build/display-profile-policy.json | 2 +- test/shell/display_built_in_profiles.bats | 2 +- test/shell/display_session_prepare.bats | 5 +++-- tools/display_built_in_profiles.py | 26 ++++++++++++++--------- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/.gitattributes b/.gitattributes index 33a00e9d8..eb5c51758 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,7 @@ *.bats text eol=lf *.py text eol=lf *.json text eol=lf +*.filt text eol=lf build/openbor/*.patch text eol=lf whitespace=-trailing-space,-space-before-tab tools/patches/*.patch text eol=lf whitespace=-trailing-space,-space-before-tab tools/bloom-device text eol=lf diff --git a/build/display-built-in-profiles.json b/build/display-built-in-profiles.json index 4bb894e74..59973feae 100644 --- a/build/display-built-in-profiles.json +++ b/build/display-built-in-profiles.json @@ -65,11 +65,13 @@ "descriptor": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", - "bytes": 22, - "sha256": "56bfda00e3439dc6d98102be2334f04592f57e1b6e99a7ea542879b4f129256a" + "hash_mode": "text-lf", + "bytes": 21, + "sha256": "607c0fb49e6d3524972b9bfcbb88c5df644914e39f9474bcf9c9a86505e10bf9" }, "implementation": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", + "hash_mode": "raw", "bytes": 9588, "sha256": "939c552b50b02472b438c35c3036c6173bbbf2f610c7abcc0f3c8e1a9d37b13e" } @@ -99,11 +101,13 @@ "descriptor": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", - "bytes": 22, - "sha256": "56bfda00e3439dc6d98102be2334f04592f57e1b6e99a7ea542879b4f129256a" + "hash_mode": "text-lf", + "bytes": 21, + "sha256": "607c0fb49e6d3524972b9bfcbb88c5df644914e39f9474bcf9c9a86505e10bf9" }, "implementation": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", + "hash_mode": "raw", "bytes": 9588, "sha256": "939c552b50b02472b438c35c3036c6173bbbf2f610c7abcc0f3c8e1a9d37b13e" } @@ -133,11 +137,13 @@ "descriptor": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD2x/LCD2x.filt", "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD2x/LCD2x.filt", - "bytes": 16, - "sha256": "f8982fdc74c85d3bf999f86ef1aea155909f802a37254e75ca4235aa275815b1" + "hash_mode": "text-lf", + "bytes": 15, + "sha256": "ad1d54093dab88a56ca91138b794e35702fec9ebbe3a1d865f868227e9432b28" }, "implementation": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD2x/lcd2x.so", + "hash_mode": "raw", "bytes": 5492, "sha256": "31fec21ab644e300d2ce1367a1461da0eac4a8d1c369108edde2f493ef19b081" } diff --git a/build/display-profile-policy.json b/build/display-profile-policy.json index 0d12d65f2..11912f4d9 100644 --- a/build/display-profile-policy.json +++ b/build/display-profile-policy.json @@ -30,7 +30,7 @@ "built_in_profiles": { "path": "build/display-built-in-profiles.json", "hash_mode": "raw", - "sha256": "c5ca0021f81575fd9cce66ec9e6fdae868f4651a1d9019427f6d718587dbf76d" + "sha256": "887b9b8b667da08e78c0d46b0f7f1744116731cf454d51fd6b27677fcadc0bde" } }, "public_controls": [ diff --git a/test/shell/display_built_in_profiles.bats b/test/shell/display_built_in_profiles.bats index 8dd5207b7..9dd918627 100644 --- a/test/shell/display_built_in_profiles.bats +++ b/test/shell/display_built_in_profiles.bats @@ -59,7 +59,7 @@ PY .status == "reviewed" and .profile_id == "plus-gb-gambatte-lcd3x-v1" and .source_geometry == {width:160,height:144,rotation_quarter_turns:0, display_aspect:{numerator:10,denominator:9},authority:"reviewed-system-core-profile"} and - .effect.class == "lcd" and .effect.bytes == 22 and + .effect.class == "lcd" and .effect.bytes == 21 and .effect.runtime_path == "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt"' >/dev/null run resolve_profile --device miyoo-mini-plus --system gba --core gpsp_libretro.so diff --git a/test/shell/display_session_prepare.bats b/test/shell/display_session_prepare.bats index 2f76c2787..261c54660 100644 --- a/test/shell/display_session_prepare.bats +++ b/test/shell/display_session_prepare.bats @@ -76,8 +76,9 @@ EOF @test "reviewed Plus geometry and exact LCD filter feed the composer without changing Auto" { mkdir -p /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x - cp /workspace/static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt \ - /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt + tr -d '\r' \ + /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt cat >"$BLOOM_SETTINGS_BIN" <<'EOF' #!/bin/sh printf '%s\n' '{"schema":1,"service":"bloom-settings","display":{"scaling":"integer-pixels","sampling":"nearest","screen_effect":"lcd","bezel":"auto"}}' diff --git a/tools/display_built_in_profiles.py b/tools/display_built_in_profiles.py index a99618009..78e80928e 100644 --- a/tools/display_built_in_profiles.py +++ b/tools/display_built_in_profiles.py @@ -69,24 +69,25 @@ def safe_repository_path(root: Path, relative: Any, label: str) -> Path: def verified_file(root: Path, record: Any, label: str, include_size: bool = False) -> Path: - keys = {"source_path", "bytes", "sha256"} if include_size else {"path", "hash_mode", "sha256"} + keys = {"source_path", "hash_mode", "bytes", "sha256"} if include_size else {"path", "hash_mode", "sha256"} if not isinstance(record, dict) or set(record) != keys: raise ValueError(f"{label} record is invalid") relative = record.get("source_path") if include_size else record.get("path") path = safe_repository_path(root, relative, label) payload = path.read_bytes() + mode = record.get("hash_mode") + if mode == "text-lf": + payload = payload.replace(b"\r\n", b"\n") + if b"\r" in payload: + raise ValueError(f"{label} contains a bare carriage return") + elif mode != "raw": + raise ValueError(f"{label} hash mode is invalid") if include_size: size = record.get("bytes") if not isinstance(size, int) or isinstance(size, bool) or size <= 0 or len(payload) != size: raise ValueError(f"{label} byte count is stale") else: - mode = record.get("hash_mode") - if mode == "text-lf": - payload = payload.replace(b"\r\n", b"\n") - if b"\r" in payload: - raise ValueError(f"{label} contains a bare carriage return") - elif mode != "raw": - raise ValueError(f"{label} hash mode is invalid") + pass expected = record.get("sha256") if not isinstance(expected, str) or not SAFE_SHA256.fullmatch(expected) or hashlib.sha256(payload).hexdigest() != expected: raise ValueError(f"{label} SHA-256 is stale") @@ -232,7 +233,7 @@ def validate(policy: Any, root: Path) -> List[str]: else: descriptor = effect.get("descriptor") implementation = effect.get("implementation") - if not isinstance(descriptor, dict) or set(descriptor) != {"source_path", "runtime_path", "bytes", "sha256"}: + if not isinstance(descriptor, dict) or set(descriptor) != {"source_path", "runtime_path", "hash_mode", "bytes", "sha256"}: failures.append(f"{label}: exact filter descriptor identity is required") else: source_path = descriptor.get("source_path") @@ -241,7 +242,12 @@ def validate(policy: Any, root: Path) -> List[str]: if runtime_path != expected_runtime or not isinstance(runtime_path, str) or not runtime_path.startswith(RUNTIME_FILTER_PREFIX) or ".." in runtime_path or "\\" in runtime_path or any(ord(character) < 0x21 or ord(character) > 0x7e or character == '"' for character in runtime_path): failures.append(f"{label}: runtime filter path is not fixed and safe") try: - descriptor_path = verified_file(root, {"source_path": source_path, "bytes": descriptor.get("bytes"), "sha256": descriptor.get("sha256")}, f"{label} descriptor", True) + descriptor_path = verified_file(root, { + "source_path": source_path, + "hash_mode": descriptor.get("hash_mode"), + "bytes": descriptor.get("bytes"), + "sha256": descriptor.get("sha256"), + }, f"{label} descriptor", True) descriptor_text = descriptor_path.read_text(encoding="utf-8") if not re.fullmatch(r"filter = [a-z0-9_]+\n", descriptor_text): failures.append(f"{label}: filter descriptor is not a single bounded built-in identity") From b36e2eb660a306e03e9e869a07c67f8dbc51bebc Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 22:21:47 -0400 Subject: [PATCH 3/5] fix(display): verify filter implementation before use --- build/dependencies.lock | 2 +- build/display-built-in-profiles.json | 3 ++ build/display-profile-policy.json | 2 +- build/legacy-manifest.json | 4 +-- .../bin/bloom-display-session-prepare | 20 ++++++++++--- test/shell/display_built_in_profiles.bats | 5 +++- test/shell/display_session_prepare.bats | 23 +++++++++++++++ tools/display_built_in_profiles.py | 28 +++++++++++++++---- 8 files changed, 72 insertions(+), 15 deletions(-) diff --git a/build/dependencies.lock b/build/dependencies.lock index 38db6f9d2..2c427c820 100644 --- a/build/dependencies.lock +++ b/build/dependencies.lock @@ -209,7 +209,7 @@ manifest = "build/legacy-manifest.json" schema = 1 component_count = 336 file_count = 2135 -byte_count = 314716799 +byte_count = 314717659 source_build_components = 232 unresolved_components = 104 inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b" diff --git a/build/display-built-in-profiles.json b/build/display-built-in-profiles.json index 59973feae..780d72f8e 100644 --- a/build/display-built-in-profiles.json +++ b/build/display-built-in-profiles.json @@ -71,6 +71,7 @@ }, "implementation": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", + "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", "hash_mode": "raw", "bytes": 9588, "sha256": "939c552b50b02472b438c35c3036c6173bbbf2f610c7abcc0f3c8e1a9d37b13e" @@ -107,6 +108,7 @@ }, "implementation": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", + "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", "hash_mode": "raw", "bytes": 9588, "sha256": "939c552b50b02472b438c35c3036c6173bbbf2f610c7abcc0f3c8e1a9d37b13e" @@ -143,6 +145,7 @@ }, "implementation": { "source_path": "static/build/RetroArch/.retroarch/filters/video/Grids/LCD2x/lcd2x.so", + "runtime_path": "/mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD2x/lcd2x.so", "hash_mode": "raw", "bytes": 5492, "sha256": "31fec21ab644e300d2ce1367a1461da0eac4a8d1c369108edde2f493ef19b081" diff --git a/build/display-profile-policy.json b/build/display-profile-policy.json index 11912f4d9..f795e2b5f 100644 --- a/build/display-profile-policy.json +++ b/build/display-profile-policy.json @@ -30,7 +30,7 @@ "built_in_profiles": { "path": "build/display-built-in-profiles.json", "hash_mode": "raw", - "sha256": "887b9b8b667da08e78c0d46b0f7f1744116731cf454d51fd6b27677fcadc0bde" + "sha256": "c79a17aac3acd428cd21f1edffc0399e7917b73d38b12e490f7233368b53ee7b" } }, "public_controls": [ diff --git a/build/legacy-manifest.json b/build/legacy-manifest.json index 413bd75ec..6824ec7c3 100644 --- a/build/legacy-manifest.json +++ b/build/legacy-manifest.json @@ -527,8 +527,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-display-session-prepare", "file_count": 1, - "byte_count": 7957, - "tree_sha256": "6d5cb6579d1f096bb6114de3c8e4e627fda316cfabcf3dfab04b03a3e5c00a40", + "byte_count": 8817, + "tree_sha256": "89c91893f9f61d8c58853ad6f3602041bfc7935911745d4e5c8b216d6727bdc3", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", diff --git a/static/build/.tmp_update/bin/bloom-display-session-prepare b/static/build/.tmp_update/bin/bloom-display-session-prepare index 6e3782713..b3553ac69 100755 --- a/static/build/.tmp_update/bin/bloom-display-session-prepare +++ b/static/build/.tmp_update/bin/bloom-display-session-prepare @@ -76,6 +76,9 @@ effect_class= effect_filter_path=- effect_filter_bytes=0 effect_filter_sha256= +effect_implementation_path= +effect_implementation_bytes=0 +effect_implementation_sha256= if [ -f "$PROFILE_POLICY" ] && [ ! -L "$PROFILE_POLICY" ]; then profile_fields=$("$JQ_BIN" -er --arg device "$device_id" --arg system "$system_id" --arg core "$core" ' select( @@ -95,7 +98,10 @@ if [ -f "$PROFILE_POLICY" ] && [ ! -L "$PROFILE_POLICY" ]; then (.effect.class == "lcd" or .effect.class == "crt") and (.effect.descriptor.runtime_path | type) == "string" and (.effect.descriptor.bytes | type) == "number" and - (.effect.descriptor.sha256 | test("^[0-9a-f]{64}$")) + (.effect.descriptor.sha256 | test("^[0-9a-f]{64}$")) and + (.effect.implementation.runtime_path | type) == "string" and + (.effect.implementation.bytes | type) == "number" and + (.effect.implementation.sha256 | test("^[0-9a-f]{64}$")) ) ] | select(length == 1) | .[0] | @@ -104,12 +110,15 @@ if [ -f "$PROFILE_POLICY" ] && [ ! -L "$PROFILE_POLICY" ]; then (.source_geometry.display_aspect.denominator | tostring), (.source_geometry.rotation_quarter_turns | tostring), .effect.class, .effect.descriptor.runtime_path, (.effect.descriptor.bytes | tostring), - .effect.descriptor.sha256] | @tsv' "$PROFILE_POLICY" 2>/dev/null || true) + .effect.descriptor.sha256, .effect.implementation.runtime_path, + (.effect.implementation.bytes | tostring), .effect.implementation.sha256] | + @tsv' "$PROFILE_POLICY" 2>/dev/null || true) if [ -n "$profile_fields" ]; then tab=$(printf '\t') IFS="$tab" read -r profile_id source_width source_height aspect_numerator \ aspect_denominator rotation effect_class effect_path effect_filter_bytes \ - effect_filter_sha256 </dev/null 2>&1 && [ -f "$effect_path" ] && [ ! -L "$effect_path" ] && [ "$(wc -c <"$effect_path" | tr -d ' ')" = "$effect_filter_bytes" ] && - [ "$("$SHA256_BIN" "$effect_path" | awk '{print $1}')" = "$effect_filter_sha256" ]; then + [ "$("$SHA256_BIN" "$effect_path" | awk '{print $1}')" = "$effect_filter_sha256" ] && + [ -f "$effect_implementation_path" ] && [ ! -L "$effect_implementation_path" ] && + [ "$(wc -c <"$effect_implementation_path" | tr -d ' ')" = "$effect_implementation_bytes" ] && + [ "$("$SHA256_BIN" "$effect_implementation_path" | awk '{print $1}')" = "$effect_implementation_sha256" ]; then effect_filter_path=$effect_path fi fi diff --git a/test/shell/display_built_in_profiles.bats b/test/shell/display_built_in_profiles.bats index 9dd918627..2379671fb 100644 --- a/test/shell/display_built_in_profiles.bats +++ b/test/shell/display_built_in_profiles.bats @@ -28,6 +28,8 @@ elif mode == "alternate-core": policy["profiles"][0]["core_binary"] = "gearboy_libretro.so" elif mode == "stale-filter": policy["profiles"][0]["effect"]["descriptor"]["sha256"] = "0" * 64 +elif mode == "stale-implementation": + policy["profiles"][0]["effect"]["implementation"]["sha256"] = "0" * 64 elif mode == "unsafe-path": policy["profiles"][0]["effect"]["descriptor"]["runtime_path"] = "/tmp/evil.filt" elif mode == "unreviewed": @@ -81,7 +83,8 @@ PY } @test "unsafe stale guessed coupled and Auto-changing profile mutations fail closed" { - for mode in enable-auto unsupported-device alternate-core stale-filter unsafe-path unreviewed \ + for mode in enable-auto unsupported-device alternate-core stale-filter stale-implementation \ + unsafe-path unreviewed \ duplicate-selector unreduced-aspect; do candidate="$BATS_TEST_TMPDIR/$mode.json" mutate_profiles "$mode" "$candidate" diff --git a/test/shell/display_session_prepare.bats b/test/shell/display_session_prepare.bats index 261c54660..d8a551e34 100644 --- a/test/shell/display_session_prepare.bats +++ b/test/shell/display_session_prepare.bats @@ -79,6 +79,8 @@ EOF tr -d '\r' \ /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt + cp /workspace/static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so \ + /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so cat >"$BLOOM_SETTINGS_BIN" <<'EOF' #!/bin/sh printf '%s\n' '{"schema":1,"service":"bloom-settings","display":{"scaling":"integer-pixels","sampling":"nearest","screen_effect":"lcd","bezel":"auto"}}' @@ -92,6 +94,27 @@ EOF '.profile_id == "plus-gb-gambatte-lcd3x-v1" and .effective.screen_effect == "lcd"' >/dev/null grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg integer-pixels nearest lcd auto 640 480 160 144 10 9 0 /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt" "$MOCK_LOG" rm -f /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt + rm -f /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so +} + +@test "missing filter implementation withholds the effect path" { + mkdir -p /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x + tr -d '\r' \ + /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt + cat >"$BLOOM_SETTINGS_BIN" <<'EOF' +#!/bin/sh +printf '%s\n' '{"schema":1,"service":"bloom-settings","display":{"scaling":"integer-pixels","sampling":"nearest","screen_effect":"lcd","bezel":"auto"}}' +EOF + chmod +x "$BLOOM_SETTINGS_BIN" + + run "$PREPARE" "$REQUEST" + + [ "$status" -eq 0 ] + printf '%s' "$output" | "$BLOOM_JQ_BIN" -e \ + '.profile_id == "plus-gb-gambatte-lcd3x-v1" and .effective.scaling == "integer-pixels"' >/dev/null + grep -F "write-display-config $BLOOM_SESSION_ROOT/ra.cfg integer-pixels nearest lcd auto 640 480 160 144 10 9 0 -" "$MOCK_LOG" + rm -f /mnt/SDCARD/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt } @test "malformed canonical display settings fail without a session config" { diff --git a/tools/display_built_in_profiles.py b/tools/display_built_in_profiles.py index 78e80928e..8edfe6952 100644 --- a/tools/display_built_in_profiles.py +++ b/tools/display_built_in_profiles.py @@ -233,6 +233,7 @@ def validate(policy: Any, root: Path) -> List[str]: else: descriptor = effect.get("descriptor") implementation = effect.get("implementation") + descriptor_text = None if not isinstance(descriptor, dict) or set(descriptor) != {"source_path", "runtime_path", "hash_mode", "bytes", "sha256"}: failures.append(f"{label}: exact filter descriptor identity is required") else: @@ -253,12 +254,27 @@ def validate(policy: Any, root: Path) -> List[str]: failures.append(f"{label}: filter descriptor is not a single bounded built-in identity") except (OSError, UnicodeError, ValueError) as error: failures.append(str(error)) - try: - implementation_path = verified_file(root, implementation, f"{label} implementation", True) - if implementation_path.suffix != ".so": - failures.append(f"{label}: implementation is not a packaged filter binary") - except ValueError as error: - failures.append(str(error)) + if not isinstance(implementation, dict) or set(implementation) != {"source_path", "runtime_path", "hash_mode", "bytes", "sha256"}: + failures.append(f"{label}: exact filter implementation identity is required") + else: + implementation_source = implementation.get("source_path") + implementation_runtime = implementation.get("runtime_path") + expected_implementation_runtime = "/mnt/SDCARD/" + implementation_source[len("static/build/"):] if isinstance(implementation_source, str) and implementation_source.startswith(SOURCE_FILTER_PREFIX) else "" + if implementation_runtime != expected_implementation_runtime or not isinstance(implementation_runtime, str) or not implementation_runtime.startswith(RUNTIME_FILTER_PREFIX) or ".." in implementation_runtime or "\\" in implementation_runtime or any(ord(character) < 0x21 or ord(character) > 0x7e or character == '"' for character in implementation_runtime): + failures.append(f"{label}: runtime implementation path is not fixed and safe") + try: + implementation_path = verified_file(root, { + "source_path": implementation_source, + "hash_mode": implementation.get("hash_mode"), + "bytes": implementation.get("bytes"), + "sha256": implementation.get("sha256"), + }, f"{label} implementation", True) + if implementation_path.suffix != ".so": + failures.append(f"{label}: implementation is not a packaged filter binary") + elif descriptor_text is not None and descriptor_text != f"filter = {implementation_path.stem}\n": + failures.append(f"{label}: descriptor does not select its exact implementation") + except ValueError as error: + failures.append(str(error)) qualification = profile.get("qualification") if not isinstance(qualification, dict) or set(qualification) != {"review_status", "device_evidence_status", "evidence_ids"} or qualification.get("review_status") != "source-reviewed" or qualification.get("device_evidence_status") not in ("pending", "remote-pass", "physical-pass") or qualification.get("evidence_ids") != ["retroarch-miyoomini-filter-contract"]: From 667ef77eba165e3ffb7ad1d0768fcf1ea4d9025d Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 23:00:09 -0400 Subject: [PATCH 4/5] docs(display): record DISP-04 Plus candidate evidence --- .../disp04-20260831/shell-restored.png | Bin 0 -> 17549 bytes ...lus-disp04-built-in-profiles-20260831.json | 127 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docs/validation/display/disp04-20260831/shell-restored.png create mode 100644 docs/validation/display/plus-disp04-built-in-profiles-20260831.json diff --git a/docs/validation/display/disp04-20260831/shell-restored.png b/docs/validation/display/disp04-20260831/shell-restored.png new file mode 100644 index 0000000000000000000000000000000000000000..3f99772113eeb08b77fd65bee0e9f4bbc1e4730f GIT binary patch literal 17549 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%0A)9O3Z{CZM2d2*i|}r!$b0&2lG>1L8nJQ zUM&hzIk+?Pj|z*V6(WJNPMUyXZyPjzr)_v8V11JPLDAI*4ff@tKiiH*SaOAy!tqlq zWujY|lg9HWrI}x`(S$2G8XvHKRSc12S`8ymKD`zxnr&)yXflp$#$?=lQJ4v zUfafF#?@9@%EULMuG^3`3`HO!lq+atRETpedIX=|Jh~6x)36^20YdI{NP39O4Yp-Di znDqJ;*HrIqc`BwMAFgdSSySSM%I2OAh-dy0JG_-6q}Sl+y7 zKr4DyaL7{Px2j6bTsszPy+sauiJ7Rr#d8|rDKdyK9pr8`S~8JJft@>Jk9GA$nIt_Z zP>(lIdK+HNW1+YRf5JJ~QkBd6POp~odm+`msPx-;I4b}It7uvW#2GHuU$K3M2O4r_+JZRjniRiw!4BhT1XJQ@FifUGS zIJq&2PB2nL_eM$?#}g5BCsC~k%BdU56q)=#4|_S&Un-vuq(kU3`zIV5M*<~;{mWik zz8Cv6w^}lG%E&y|FOlcp3~id<7#M3+;-|sL@<%Oir_f)H6poUehY8 z5;O`IPD*iKq_Oa;@7-w0*!+VjYdgOw3AWPh82dHq0gV_Q%%#pNlt-G z9*j)x7@5g*o>lt9`Qt%}=AvNI<^s&b*+!viDtrT!Ov?9uF6A7(PAj15z12u$l-3R7MB+*5HWcFe*sVwGCKeO literal 0 HcmV?d00001 diff --git a/docs/validation/display/plus-disp04-built-in-profiles-20260831.json b/docs/validation/display/plus-disp04-built-in-profiles-20260831.json new file mode 100644 index 000000000..fc16636ae --- /dev/null +++ b/docs/validation/display/plus-disp04-built-in-profiles-20260831.json @@ -0,0 +1,127 @@ +{ + "schema": 1, + "date": "2026-08-31", + "program_slice": "DISP-04", + "operator": "Codex over maintainer-authorized key-only SSH", + "source_commit": "b36e2eb660a306e03e9e869a07c67f8dbc51bebc", + "base_commit": "c31d928db634f560bf1fd0d700fb7c20a2c21488", + "host_validation": { + "focused_native": "8/8 passed", + "full_shell": "799/799 passed across eight shards; slowest shard 94 seconds", + "focused_display_shell": "15/15 passed", + "focused_provenance_manifest_dependency": "passed after final regeneration", + "ci_scope_contract": "passed", + "static_analysis": "focused cppcheck exited 0", + "format": "clang-format dry-run passed", + "diff_check": "passed" + }, + "signed_build": { + "workflow_run": "https://github.com/boburning/BloomOS/actions/runs/33350464921", + "workflow_result": "passed", + "version": "0.1.0-dev-b36e2eb6", + "archive_size": 199861824, + "archive_sha256": "d957635c14d781d2101110b0fcb58fde6232e1b3167c96fb47887ff31b3ee3f8", + "manifest_sha256": "a75012b612ef9244f7ad7b16b27c09339a5dbd5bc23c4ff216075d7de9f71776", + "manifest_signature_sha256": "1d55c70c3c6c59f5a051bf21726dbc26114f8ef4d89adf4afc6601581a91256d", + "payload_manifest_sha256": "2212158d91b2bdd5b12a7f075ce4b974f1e080abec6298a29b9321c487384dbf", + "build_info_sha256": "8d0499c161e14062439e1efd75fa487a9bd4f2a3d6c047bf4e409f6a045f08bd", + "sha256sums_verified": true, + "ed25519_signature_verified": true, + "installed_as_full_update": false + }, + "device": { + "model": "miyoo-mini-plus", + "model_id": "354", + "display": "640x480", + "installed_baseline": "v0.1.0-dev-2933f706", + "update_phase_before_and_after": "known_good", + "developer_ssh": "running_key_only", + "physical_certification": false + }, + "deployment": { + "tier": "hash-differential Warm overlay", + "candidate_id": "disp04-launch-b36e2eb6", + "candidate_component": "launch-session", + "candidate_manifest_files": 12, + "candidate_manifest_exact_hashes_verified": true, + "network_component_deployed": false, + "full_update_performed": false, + "candidate_rolled_back": true, + "current_shell_overlay": "disp04-shell-restore-b36e2eb6", + "current_shell_source": "six exact files extracted from the verified signed artifact", + "temporary_deployer_removed": true, + "temporary_settings_fixture_removed": true + }, + "profile_gate": { + "system": "gb", + "emulator_id": "libretro:gambatte", + "profile_id": "plus-gb-gambatte-lcd3x-v1", + "selection_result": { + "status": "applied", + "scaling": "integer-pixels", + "screen_effect": "lcd", + "bezel": "auto" + }, + "source_geometry": "160x144", + "display_aspect": "10:9", + "viewport": "480x432 at 80,24", + "sampling": "nearest", + "filter_descriptor_sha256": "607c0fb49e6d3524972b9bfcbb88c5df644914e39f9474bcf9c9a86505e10bf9", + "filter_implementation_sha256": "939c552b50b02472b438c35c3036c6173bbbf2f610c7abcc0f3c8e1a9d37b13e", + "private_retroarch_config_sha256": "cf539c91ce78cdd96cb2c7f02252f4e4bb263c6c47879c22ce3f1457bce97190", + "retroarch_state_observed": "RUNNING", + "retroarch_process": "retroarch", + "core": "gambatte_libretro.so", + "recent_status": "recorded", + "global_retroarch_config_sha256_before": "336ee748ca06bba32582087efc867f811f465558df158056b9a25689bf36672e", + "global_retroarch_config_sha256_after": "336ee748ca06bba32582087efc867f811f465558df158056b9a25689bf36672e", + "canonical_settings_sha256_before": "31fe5ad0969bc8a9e831d10b77752da4712b30d0b96082c8243a6fa495316c9d", + "canonical_settings_sha256_after": "31fe5ad0969bc8a9e831d10b77752da4712b30d0b96082c8243a6fa495316c9d", + "wifi_state_before_during_and_after": "enabled and associated", + "rom_or_save_content_committed": false + }, + "teardown_limitation": { + "auto_baseline_result": "forced_kill_required", + "profile_result": "forced_kill_required", + "session_final_record": "FAILED", + "active_retroarch_processes_after": 0, + "aggregate_health_after": "healthy", + "clean_stop_certified": false, + "save_flush_certified": false, + "interpretation": "The identical Auto and profile teardown result indicates an existing game-smoke/session-stop limitation rather than evidence of a display-profile regression; it remains an explicit blocker for clean-stop and save-flush certification." + }, + "restoration": { + "candidate_launch_session_rolled_back": true, + "current_shell_manifest_exact_hashes_verified": true, + "final_shell_pid_count": 1, + "final_shell_rss_kib": 8960, + "final_shell_threads": 3, + "final_shell_file_descriptors": 9, + "final_wifi_state": "enabled and associated", + "final_update_phase": "known_good", + "final_aggregate_health": "healthy", + "final_retroarch_processes": 0, + "task_remote_temporary_files_removed": true + }, + "screenshots": { + "shell_restored": { + "path": "docs/validation/display/disp04-20260831/shell-restored.png", + "size": 17549, + "sha256": "37feba9ef9488b0e93caf4cff6264357dca6a51eb76f888630bca37be545b9d6", + "privacy_safe": true, + "framebuffer_pages_equal": true + }, + "running_game": { + "committed": false, + "usable_visual_evidence": false, + "reason": "The bounded framebuffer capture was black; no rendering claim is made and no private ROM-derived frame is published." + } + }, + "limitations": [ + "This is candidate-only SSH, process, config, integrity, and framebuffer evidence; it is not physical controls, LCD quality, audio, or gameplay certification.", + "The real RetroArch process reached canonical RUNNING, but neither the Auto comparison nor the selected profile completed a clean remote stop; save flush is not certified.", + "The running-game framebuffer capture was black, so this evidence does not certify visible filter output.", + "The GB profile was exercised on a Miyoo Mini Plus only; GBC and GBA remain host-validated, and Mini V1-V4 plus Mini Flip/my285 remain untested.", + "No Wi-Fi, network profile, credential, listener, route, ROM, Save, or global RetroArch configuration mutation was performed." + ] +} From f6b47968ee3e65665a754c4b9c06fc74b1221c8a Mon Sep 17 00:00:00 2001 From: boburning Date: Sun, 30 Aug 2026 23:09:46 -0400 Subject: [PATCH 5/5] fix(display): validate pinned uninitialized license authority --- test/shell/display_built_in_profiles.bats | 33 +++++++++++++++ tools/display_built_in_profiles.py | 50 ++++++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/test/shell/display_built_in_profiles.bats b/test/shell/display_built_in_profiles.bats index 2379671fb..81c0b7d82 100644 --- a/test/shell/display_built_in_profiles.bats +++ b/test/shell/display_built_in_profiles.bats @@ -54,6 +54,39 @@ PY grep -F '@cp $(ROOT_DIR)/build/display-built-in-profiles.json $(BUILD_DIR)/.tmp_update/config/display-built-in-profiles.json' /workspace/Makefile } +@test "pinned uninitialized RetroArch source retains exact reviewed license authority" { + repository="$BATS_TEST_TMPDIR/uninitialized-retroarch" + python3 - /workspace "$repository" <<'PY' +import pathlib +import shutil +import sys + +source = pathlib.Path(sys.argv[1]) +destination = pathlib.Path(sys.argv[2]) +paths = [ + ".gitmodules", + "build/core-selection-policy.json", + "build/dependencies.lock", + "build/display-auto-baseline.json", + "build/display-built-in-profiles.json", + "static/build/RetroArch/RetroArch_Dingux_forMiyooMini.txt", + "static/build/RetroArch/.retroarch/filters/video/Grids/LCD2x/LCD2x.filt", + "static/build/RetroArch/.retroarch/filters/video/Grids/LCD2x/lcd2x.so", + "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/LCD3x_light.filt", + "static/build/RetroArch/.retroarch/filters/video/Grids/LCD3x/lcd3x_light.so", +] +for relative in paths: + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source / relative, target) +PY + + run python3 "$TOOL" validate --repository "$repository" \ + --policy "$repository/build/display-built-in-profiles.json" + [ "$status" -eq 0 ] + [ "$output" = "built-in display profiles valid: 3 Plus profiles, 2 filter identities, Auto preserved" ] +} + @test "built-in profile resolution is exact by device system and core" { run resolve_profile --device miyoo-mini-plus --system gb --core gambatte_libretro.so [ "$status" -eq 0 ] diff --git a/tools/display_built_in_profiles.py b/tools/display_built_in_profiles.py index 8edfe6952..5dc7e07ce 100644 --- a/tools/display_built_in_profiles.py +++ b/tools/display_built_in_profiles.py @@ -45,6 +45,13 @@ SAFE_SHA256 = re.compile(r"^[0-9a-f]{64}$") RUNTIME_FILTER_PREFIX = "/mnt/SDCARD/RetroArch/.retroarch/filters/video/" SOURCE_FILTER_PREFIX = "static/build/RetroArch/.retroarch/filters/video/" +RETROARCH_LICENSE_AUTHORITY = { + "path": "third-party/RetroArch-patch/submodules/RetroArch/COPYING", + "hash_mode": "raw", + "sha256": "0b383d5a63da644f628d99c33976ea6487ed89aaa59f0b3257992deac1171e6b", +} +RETROARCH_PATCH_REVISION = "f9e959f7445d2ba0a4dd6279da41a095163767f2" +RETROARCH_SOURCE_REVISION = "69a4f0ea1e8aaf442ae4858f2e7f2b31a1776576" def load_json(path: Path, label: str) -> Any: @@ -94,6 +101,37 @@ def verified_file(root: Path, record: Any, label: str, include_size: bool = Fals return path +def locked_uninitialized_retroarch_license(root: Path, record: Any) -> bool: + """Accept the reviewed license hash only when its source submodule is absent but pinned.""" + if record != RETROARCH_LICENSE_AUTHORITY: + return False + patch_root = root / "third-party/RetroArch-patch" + source_root = patch_root / "submodules/RetroArch" + if patch_root.is_symlink() or source_root.is_symlink(): + return False + if (patch_root / ".git").exists() and (source_root / ".git").exists(): + return False + try: + modules = safe_repository_path(root, ".gitmodules", "submodule registry").read_text( + encoding="utf-8" + ) + lock = safe_repository_path( + root, "build/dependencies.lock", "dependency lock" + ).read_text(encoding="utf-8") + except (OSError, UnicodeError, ValueError): + return False + if "\tpath = third-party/RetroArch-patch\n" not in modules.replace("\r\n", "\n"): + return False + required_lock_lines = { + '"third-party/RetroArch-patch" = "{}"'.format(RETROARCH_PATCH_REVISION), + '"third-party/RetroArch-patch/submodules/RetroArch" = "{}"'.format( + RETROARCH_SOURCE_REVISION + ), + 'source_revision = "{}"'.format(RETROARCH_SOURCE_REVISION), + } + return required_lock_lines.issubset(set(lock.splitlines())) + + def core_candidate(policy: Any, system_id: str, candidate_id: str) -> Optional[Dict[str, Any]]: if not isinstance(policy, dict): return None @@ -147,6 +185,7 @@ def validate(policy: Any, root: Path) -> List[str]: baseline: Any = {} filter_contract = "" license_text = "" + license_authority_deferred = False for key, label in ( ("core_selection_policy", "core-selection policy"), ("auto_baseline", "Auto baseline"), @@ -164,14 +203,21 @@ def validate(policy: Any, root: Path) -> List[str]: else: license_text = path.read_text(encoding="utf-8") except (OSError, UnicodeError, ValueError) as error: - failures.append(str(error)) + if key == "retroarch_license" and locked_uninitialized_retroarch_license( + root, authorities.get(key) + ): + license_authority_deferred = True + else: + failures.append(str(error)) if core_policy.get("policy_id") != "bloomos-core-selection-v1": failures.append("profiles must join the canonical core-selection authority") if baseline.get("policy_id") != "bloomos-display-auto-baseline-v1": failures.append("profiles must join the DISP-01 Auto authority") if "LCD filters should always be used with Integer:ON Aspect:ON" not in filter_contract or "up to 320pixels width for 2x" not in filter_contract or "up to 213pixels width for 3x" not in filter_contract: failures.append("packaged Miyoo filter constraints are unavailable") - if "GNU GENERAL PUBLIC LICENSE" not in license_text or "Version 3" not in license_text: + if not license_authority_deferred and ( + "GNU GENERAL PUBLIC LICENSE" not in license_text or "Version 3" not in license_text + ): failures.append("RetroArch GPL-3.0 license authority is unavailable") profiles = policy.get("profiles")