From 6027b5a09a7a13d05d406fd1cafb569e8c0ac3e5 Mon Sep 17 00:00:00 2001 From: boburning Date: Sat, 29 Aug 2026 23:42:18 -0400 Subject: [PATCH] feat(network): add credential-free Wi-Fi UI model --- build/dependencies.lock | 2 +- build/legacy-manifest.json | 8 +- docs/1.0_TEST_MATRIX.md | 3 +- docs/ARCHITECTURE.md | 12 +- docs/DEVELOPMENT.md | 7 + docs/SECURITY.md | 12 +- docs/program/BloomOS_Work_Program_v3.json | 35 ++- src/bloomShell/bloom_shell_wifi_ui.c | 269 ++++++++++++++++++ src/bloomShell/bloom_shell_wifi_ui.h | 59 ++++ .../build/.tmp_update/bin/bloom-wifi-profiles | 24 ++ static/build/.tmp_update/bin/bloomctl | 4 + test/Makefile | 1 + test/shell/bloom_platform.bats | 2 +- test/shell/bloomctl_info.bats | 5 + test/shell/wifi_profiles.bats | 19 ++ test/test_bloom_shell_wifi_ui.cpp | 77 +++++ test/test_ci_scope.sh | 6 + tools/ci-scope.sh | 9 + 18 files changed, 529 insertions(+), 25 deletions(-) create mode 100644 src/bloomShell/bloom_shell_wifi_ui.c create mode 100644 src/bloomShell/bloom_shell_wifi_ui.h create mode 100644 test/test_bloom_shell_wifi_ui.cpp diff --git a/build/dependencies.lock b/build/dependencies.lock index c2a708b66..aaa126dc7 100644 --- a/build/dependencies.lock +++ b/build/dependencies.lock @@ -209,7 +209,7 @@ manifest = "build/legacy-manifest.json" schema = 1 component_count = 332 file_count = 2131 -byte_count = 314682516 +byte_count = 314683902 source_build_components = 228 unresolved_components = 104 inherited_baseline = "07505ea58c7bba698d6b9220ff43946a43cac76b" diff --git a/build/legacy-manifest.json b/build/legacy-manifest.json index b670d5d1b..2535f6dcd 100644 --- a/build/legacy-manifest.json +++ b/build/legacy-manifest.json @@ -995,8 +995,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloom-wifi-profiles", "file_count": 1, - "byte_count": 14104, - "tree_sha256": "07502370e9ac8bc9a6df02a732bdf60ecf362f620641b0f659c734a40ff6552a", + "byte_count": 15282, + "tree_sha256": "c66a44035c6e6678b1b051ed49145d6df6aedb19dfb6e147f6408ad13bbfa605", "source": "https://github.com/boburning/BloomOS", "source_revision": "release-commit", "license": "GPL-3.0-only", @@ -1021,8 +1021,8 @@ "kind": "runtime", "path": "static/build/.tmp_update/bin/bloomctl", "file_count": 1, - "byte_count": 39469, - "tree_sha256": "8342402e14fa666f3d5472d4b3546d89bc6f47e518a01402785c004fc1178b60", + "byte_count": 39677, + "tree_sha256": "ec2d7df4e4286896130d576d562e20078600dc623309f0b84731b0473665b725", "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 d8d40b26d..8862d4ead 100644 --- a/docs/1.0_TEST_MATRIX.md +++ b/docs/1.0_TEST_MATRIX.md @@ -48,8 +48,9 @@ shoulder button. | Settings | One sectioned surface; inline values; capability filtering; one-layer details; hidden Developer section; optional overlapping L1/R1 list paging | | Quick Settings | START from core surfaces; immediate brightness/volume/mute; capability-gated Wi-Fi; qualified Plus remains toggleable while a powered-off radio has no `wlan0`; Off-to-On reaches the fixed backend; readable battery; B/START close | | Network services | Desired/runtime separation; carrier without IPv4 remains acquiring/not-ready; canonical DHCP restart and bounded post-address listener reconciliation; SSH shell and SFTP independent key-only states; canonical transfer roots and modes; Saves read-only; private roots denied; traversal/symlink/path bounds; same-filesystem atomic publication; authorized-key import; quiesce/health; stale FTP/Telnet/FileBrowser/inherited-Samba/WPS/hotspot flags cannot activate listeners; process-aware listener audit; Samba hidden until a materially different source-build candidate passes feasibility | -| Wi-Fi profile store | JFFS2 private root; stdin-only bounded replacement and `bloomctl network profile upsert ID`; requested/document ID binding; explicit `network forget ID`; inactive-ID deletion with active-profile refusal; active forget only after a separately confirmed fallback connection; schema/unknown-field/count/size enforcement; WPA2-PSK, explicit open, and manual hidden WPA2; WEP and hidden open denial; unique IDs/SSIDs; redacted status; mode-0600 atomic single-profile supplicant projection; symlink, occupied-slot, live-lock, invalid-profile, and prior-byte preservation | +| Wi-Fi profile store | JFFS2 private root; stdin-only bounded replacement and `bloomctl network profile upsert ID`; requested/document ID binding; explicit `network forget ID`; inactive-ID deletion with active-profile refusal; active forget only after a separately confirmed fallback connection; schema/unknown-field/count/size enforcement; WPA2-PSK, explicit open, and manual hidden WPA2; WEP and hidden open denial; unique IDs/SSIDs; redacted status; explicit local credential-free UI list with one active marker; mode-0600 atomic single-profile supplicant projection; symlink, occupied-slot, live-lock, invalid-profile, and prior-byte preservation | | Wi-Fi scan API | Asynchronous start; redacted status; explicit ID-scoped local results; cancel and terminal discard; one active worker; five-second rate limit; 15-second runtime; 64-KiB raw ceiling; 32 unique printable SSIDs; eight-operation history; open/WPA2-PSK/unsupported classification; duplicate signal reconciliation; backend failure, timeout, oversize, unsafe-path, stale/live-lock, and unsupported-hardware handling | +| Wi-Fi UI | Original Mini hidden; exact profile/scan schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; credential and unknown-field rejection; passwords absent from arguments, logs, screenshots, durable Shell state, and evidence | | GameSwitcher | MENU open/toggle; Left/Right select; L1/R1 page; A resume; X actions; START Quick Settings; B Home; no inherited brightness/view/delete behavior | | Apps/Tweaks | Every app disposition recorded; normal configuration requires no Tweaks; supervised launch remains bounded | | Persistent state | Versioned atomic publication; exact restoration across Details/game/GameSwitcher/Quick Settings; malformed/oversized/symlink input defaults without Safe Mode | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7dae18050..e1786cac9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -501,7 +501,10 @@ reader remains only as that fail-closed migration path. schema-1 store lives below the internal JFFS2 `/appconfigs` boundary, is limited to 16 profiles and 16 KiB, and accepts a complete replacement only on stdin. Profile IDs are non-secret stable handles; status reports only counts -and managed/unmanaged state. SSIDs and credentials are never returned. The +and managed/unmanaged state. An explicit local `list --json` projection serves +the on-device Wi-Fi UI with at most 16 IDs, printable SSIDs, security classes, +hidden flags, and one active marker. It validates the private store again and +never returns a PSK or an unknown field. The initial security vocabulary is WPA2-PSK plus an explicitly created open profile. A hidden profile must be WPA2-PSK. WEP, unknown security modes, duplicate IDs or SSIDs, control characters, links, extra fields, and unsafe @@ -535,6 +538,13 @@ and classify only explicit open, WPA2-PSK, or unsupported security. A caller can cancel active work and explicitly discard any terminal result. The scanner does not change a profile, preference, association, DHCP client, or listener. +NET-05 consumes those two credential-free projections through a bounded Shell +model. It accepts only exact schema-1 service identities, 16 unique stored +profiles, 32 unique scan results, printable 32-byte SSIDs, fixed security and +operation states, one active profile, and one 18–32 byte scan ID. Unknown +fields—including any credential-like field—make the snapshot unusable. This +first model slice adds no direct radio toggle or arbitrary command boundary. + Profile association reuses the one guarded network-transaction authority rather than adding a second recovery mechanism. `begin` copies the exact current supplicant file into a mode-0700 JFFS2-private transaction root, records only diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 9dc61d0f5..d6f9f6ec0 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -337,6 +337,7 @@ bloom-wifi-profiles replace < synthetic-profiles.json bloom-wifi-profiles upsert fixture-profile-id < synthetic-profile.json bloom-wifi-profiles delete inactive-fixture-profile-id bloom-wifi-profiles status --json +bloom-wifi-profiles list --json bloom-wifi-profiles project fixture-profile-id ``` @@ -359,6 +360,12 @@ may inspect only filesystem type, mode, byte count, structural counts, and redacted health. NET-04 orchestration retains stdin or an equivalent fixed private input boundary and the existing rollback rules. +`list --json` is a local on-device UI projection, not a diagnostic or export +surface. It contains non-secret IDs and SSIDs but must never contain `psk` or +unknown fields. Keep its output out of shell history, screenshots, logs, +support bundles, and validation evidence. Exercise it only with synthetic +profiles in host or `/tmp` fixtures until the NET-05 UI gate is reached. + The NET-04 scan slice is asynchronous and keeps result data only under `/tmp`: ```sh diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 5de32b6fc..6a95ac022 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -95,8 +95,8 @@ process arguments. Wi-Fi profiles follow that same boundary. `bloom-wifi-profiles replace` reads one bounded schema-1 document from stdin and publishes it mode 0600 below a -mode-0700 `/appconfigs/bloom-network` root. It exposes no export or raw-list -command. Public status contains only the profile count and whether the current +mode-0700 `/appconfigs/bloom-network` root. It exposes no credential export or +raw-store command. Public status contains only the profile count and whether the current supplicant file carries Bloom's managed marker. Projection accepts only a non-secret profile ID, revalidates the store, and atomically publishes a single-network mode-0600 configuration with `update_config=0`, preventing @@ -111,6 +111,14 @@ handle, revalidates the resulting complete store, and publishes atomically. the managed supplicant projection. Neither operation scans, associates, restarts Wi-Fi, or exposes an SSID or credential. +The explicit local `list --json` operation exists only for the on-device +NET-05 consumer. It revalidates the complete private store and emits only the +bounded profile ID, printable SSID, security class, hidden flag, and active +marker. Its JSON shape cannot contain a PSK, and the Shell rejects unknown +fields so a credential-bearing response cannot cross into UI state. Do not +capture this local SSID list in diagnostics, screenshots, logs, or durable +evidence. + Wi-Fi scan status and logs never contain SSIDs. An explicit local results call may return at most 32 printable SSIDs for the future on-device UI, but that ephemeral mode-0600 result is excluded from diagnostics, support exports, diff --git a/docs/program/BloomOS_Work_Program_v3.json b/docs/program/BloomOS_Work_Program_v3.json index e7392fa9d..df9da60d9 100644 --- a/docs/program/BloomOS_Work_Program_v3.json +++ b/docs/program/BloomOS_Work_Program_v3.json @@ -215,7 +215,7 @@ "id": "REC-02", "track": "Recovery", "title": "Repair failing existing branches #443 and #429, then propagate dependents", - "status": "merge_ready", + "status": "merged", "dependencies": [ "REC-01" ], @@ -454,7 +454,7 @@ "id": "CORE-04", "track": "Core", "title": "System Emulator UI and exact RA capability badges", - "status": "published", + "status": "merged", "dependencies": [ "CORE-03" ], @@ -479,7 +479,7 @@ "id": "CORE-05", "track": "Core", "title": "Generic Auto and effective RA resolution; remove GBA hardcode", - "status": "published", + "status": "merged", "dependencies": [ "CORE-03", "CORE-04" @@ -505,7 +505,7 @@ "id": "CORE-06", "track": "Core", "title": "Save/state compatibility groups and trusted migrations", - "status": "published", + "status": "merged", "dependencies": [ "CORE-03" ], @@ -695,7 +695,7 @@ "id": "CORE-08", "track": "Core", "title": "System-family emulator/core qualification trains", - "status": "published", + "status": "merged", "dependencies": [ "CORE-03", "CORE-06" @@ -734,7 +734,7 @@ "id": "PLAT-01", "track": "Platform", "title": "Granular SSD202D device capability registry", - "status": "published", + "status": "merged", "dependencies": [ "REC-07" ], @@ -757,7 +757,7 @@ "id": "NET-01", "track": "Network", "title": "Network service v2 and non-secret settings authority", - "status": "published", + "status": "merged", "dependencies": [ "PLAT-01", "REC-04" @@ -782,7 +782,7 @@ "id": "NET-02", "track": "Network", "title": "Canonical transfer/share policy", - "status": "merge_ready", + "status": "merged", "dependencies": [ "NET-01" ], @@ -822,7 +822,7 @@ "id": "NET-03", "track": "Network", "title": "Bloom Wi-Fi secret profile store and runtime projection", - "status": "merge_ready", + "status": "merged", "dependencies": [ "NET-01" ], @@ -875,7 +875,7 @@ "id": "NET-05", "track": "Network", "title": "First-class Wi-Fi UI", - "status": "planned", + "status": "in_progress", "dependencies": [ "NET-04", "UI-05" @@ -884,8 +884,12 @@ "risk_tier": 1, "deployment_tier": "hot", "device_gate_required": true, - "last_audited_main": "3a7e78290285806e1ce3eb0c126887644ff665f2", - "evidence": [] + "last_audited_main": "dbe30da0a2fe9ee2a5211ce471988f132026e759", + "evidence": [ + "issue #544 tracks NET-05 on current main after merged NET-04A-D and UI-05; the audit found that Bloom Shell still treats Wi-Fi as an inline toggle whose A and left/right handlers synchronously call bloom-network enable or disable instead of opening the bounded profile and scan UI", + "the first host-safe slice adds an explicit local credential-free projection of at most 16 validated profile IDs, printable SSIDs, security classes, hidden flags, and one active marker; the private service never returns a PSK and the Shell model rejects unknown or credential-bearing fields", + "the bounded Shell model accepts at most 32 unique scan results, one 18-32 byte operation ID, fixed scan phases and errors, integral timestamps and signal values, and exact schema-1 service identities; this slice adds no rendered surface, radio toggle, scan, association, profile transition, listener mutation, reboot, update, or device mutation" + ] }, { "id": "NET-06", @@ -1676,7 +1680,7 @@ "id": "META-09", "track": "Metadata", "title": "Provider UX, storage controls, resource and fault hardening", - "status": "published", + "status": "merged", "dependencies": [ "META-05" ], @@ -1687,7 +1691,7 @@ "risk_tier": 2, "deployment_tier": "warm", "device_gate_required": true, - "last_audited_main": "f9fbedf5f2bdb258c644609f5098def5c55d58ad", + "last_audited_main": "dbe30da0a2fe9ee2a5211ce471988f132026e759", "evidence": [ "issue #541 tracks META-09 after merged META-05 and audits current main as already owning durable job state, field resolution, downloaded-media storage and GC, and local-only metadata actions but lacking one provider-neutral operations projection, bounded review/retry controls, downloaded-media policy controls, Shell progress/background/review UX, and a consolidated fault/resource gate", "META-09 is split at the risk boundary: Slice A publishes the host-testable job/progress/review/storage authority and narrowly bounded controls; dependent Slice B adds the Bloom Shell consumer and owns the signed ARM and Mini Plus Warm checkpoint, so the package remains incomplete until both slices finish", @@ -1700,7 +1704,8 @@ "Slice B host validation passed strict gnu18 -Wall -Wextra -Werror syntax compilation, focused cppcheck 2.19, CI-scope regression, all 8 affected native and AddressSanitizer tests, 100 repeated snapshots in 70 milliseconds native and 291 milliseconds under AddressSanitizer, all 540 native tests in 11.42 seconds, and all 540 AddressSanitizer tests in 21.36 seconds; the Windows archive-mode correction passed all 29 focused bloom-device shell cases", "hardware workflow 33287065389 passed for exact Slice B implementation source 19d2fff334edc6ceb88b4870d522767a48124d53; the 199827820-byte signed release SHA-256 is f31afcdba141da13ab203f7cfd8b6afcfe4e53b87ad17d3c70bf35bc53f7d09b, its Ed25519 signature and release metadata verified, and its extracted 440932-byte ARM bloom-shell SHA-256 is 68372100e31d94c1ea9e4548a7da5fdd927210cd6d81c2cb5b9edebe8aba8ae5", "the Mini Plus Warm checkpoint staged the exact complete six-file Shell bundle, opened Library > Background Tasks, proved fail-closed stale-schema handling and explicit retry, rendered zero-job and storage aggregates without provider/network activity, held one PID with 3 threads, 9-12 descriptors, and a 4 KiB RSS/VmSize range across 31 one-second samples, and restored the exact pre-test root framebuffer; runtime rollback restored all signed hashes while Wi-Fi stayed associated, the network transaction stayed idle, and listener audit remained clean", - "the checkpoint also exposed that ordinary development file-overlay rollback does not own a catalog migration: after exact local and device backups, the zero-row schema 9-13 additions were transactionally reversed and the signed schema-8 Library again reported ready with 53 systems, 22 games, 23 apps, zero favorites, and 12 recents; sanitized evidence and the explicit limitation are recorded in docs/validation/library/plus-meta09-operations-20260829.json" + "the checkpoint also exposed that ordinary development file-overlay rollback does not own a catalog migration: after exact local and device backups, the zero-row schema 9-13 additions were transactionally reversed and the signed schema-8 Library again reported ready with 53 systems, 22 games, 23 apps, zero favorites, and 12 recents; sanitized evidence and the explicit limitation are recorded in docs/validation/library/plus-meta09-operations-20260829.json", + "GitHub PR #543 passed format in 17 seconds, native in 1 minute 5 seconds, AddressSanitizer in 1 minute 13 seconds, static analysis in 1 minute 41 seconds, the corrected 735-test shell gate in 2 minutes 4 seconds, and the exhaustive ARM build in 4 minutes 34 seconds; it merged by squash as dbe30da0a2fe9ee2a5211ce471988f132026e759" ] }, { diff --git a/src/bloomShell/bloom_shell_wifi_ui.c b/src/bloomShell/bloom_shell_wifi_ui.c new file mode 100644 index 000000000..98510983b --- /dev/null +++ b/src/bloomShell/bloom_shell_wifi_ui.c @@ -0,0 +1,269 @@ +#include "bloom_shell_wifi_ui.h" + +#include + +#include +#include +#include + +static int exact_object(const cJSON *value, int fields) +{ + return cJSON_IsObject(value) && cJSON_GetArraySize(value) == fields; +} + +static int bounded_string(const cJSON *value, size_t maximum, int printable) +{ + if (!cJSON_IsString(value) || value->valuestring == NULL || value->valuestring[0] == '\0') + return 0; + size_t length = strlen(value->valuestring); + if (length > maximum) + return 0; + for (size_t index = 0; printable && index < length; ++index) + if ((unsigned char)value->valuestring[index] < 0x20 || + (unsigned char)value->valuestring[index] > 0x7e) + return 0; + return 1; +} + +static int safe_id(const cJSON *value) +{ + if (!bounded_string(value, 32, 1) || value->valuestring[0] == '.' || + value->valuestring[0] == '-') + return 0; + for (const char *cursor = value->valuestring; *cursor != '\0'; ++cursor) + if (!((*cursor >= 'a' && *cursor <= 'z') || (*cursor >= '0' && *cursor <= '9') || + *cursor == '.' || *cursor == '_' || *cursor == '-')) + return 0; + return 1; +} + +static int operation_id(const cJSON *value) +{ + if (!bounded_string(value, 32, 1)) + return 0; + size_t length = strlen(value->valuestring); + if (length < 18 || value->valuestring[0] == '-' || value->valuestring[length - 1] == '-') + return 0; + for (const char *cursor = value->valuestring; *cursor != '\0'; ++cursor) + if (!((*cursor >= '0' && *cursor <= '9') || (*cursor >= 'a' && *cursor <= 'f') || + *cursor == '-')) + return 0; + return 1; +} + +static int unsigned_integer(const cJSON *value, uint64_t maximum, uint64_t *result) +{ + if (!cJSON_IsNumber(value) || value->valuedouble < 0.0 || + value->valuedouble > (double)maximum || value->valuedouble != (double)value->valueint) + return 0; + *result = (uint64_t)value->valuedouble; + return 1; +} + +static int root_identity(const cJSON *root, const char *service) +{ + const cJSON *schema = cJSON_GetObjectItemCaseSensitive(root, "schema"); + const cJSON *name = cJSON_GetObjectItemCaseSensitive(root, "service"); + return cJSON_IsNumber(schema) && schema->valuedouble == 1.0 && + bounded_string(name, 32, 1) && strcmp(name->valuestring, service) == 0; +} + +static int security_value(const cJSON *value, int allow_unsupported, + BloomShellWifiSecurity *security) +{ + if (!bounded_string(value, 16, 1)) + return 0; + if (strcmp(value->valuestring, "open") == 0) + *security = BLOOM_SHELL_WIFI_SECURITY_OPEN; + else if (strcmp(value->valuestring, "wpa2-psk") == 0) + *security = BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK; + else if (allow_unsupported && strcmp(value->valuestring, "unsupported") == 0) + *security = BLOOM_SHELL_WIFI_SECURITY_UNSUPPORTED; + else + return 0; + return 1; +} + +void bloom_shell_wifi_ui_init(BloomShellWifiUi *wifi) +{ + if (wifi != NULL) + memset(wifi, 0, sizeof(*wifi)); +} + +int bloom_shell_wifi_profiles_parse(const char *json, BloomShellWifiUi *wifi) +{ + if (json == NULL || wifi == NULL) + return -1; + cJSON *root = cJSON_ParseWithOpts(json, NULL, 1); + const cJSON *profiles = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "profiles"); + BloomShellWifiUi parsed = *wifi; + parsed.profile_count = 0; + memset(parsed.profiles, 0, sizeof(parsed.profiles)); + int valid = exact_object(root, 3) && root_identity(root, "bloom-wifi-profiles") && + cJSON_IsArray(profiles) && cJSON_GetArraySize(profiles) >= 0 && + cJSON_GetArraySize(profiles) <= BLOOM_SHELL_WIFI_PROFILE_LIMIT; + int active_count = 0; + for (int index = 0; valid && index < cJSON_GetArraySize(profiles); ++index) { + const cJSON *entry = cJSON_GetArrayItem(profiles, index); + const cJSON *id = cJSON_GetObjectItemCaseSensitive(entry, "id"); + const cJSON *ssid = cJSON_GetObjectItemCaseSensitive(entry, "ssid"); + const cJSON *security = cJSON_GetObjectItemCaseSensitive(entry, "security"); + const cJSON *hidden = cJSON_GetObjectItemCaseSensitive(entry, "hidden"); + const cJSON *active = cJSON_GetObjectItemCaseSensitive(entry, "active"); + BloomShellWifiProfile *destination = &parsed.profiles[index]; + valid = exact_object(entry, 5) && safe_id(id) && bounded_string(ssid, 32, 1) && + security_value(security, 0, &destination->security) && cJSON_IsBool(hidden) && + cJSON_IsBool(active); + for (int prior = 0; valid && prior < index; ++prior) + if (strcmp(parsed.profiles[prior].id, id->valuestring) == 0 || + strcmp(parsed.profiles[prior].ssid, ssid->valuestring) == 0) + valid = 0; + if (!valid) + break; + snprintf(destination->id, sizeof(destination->id), "%s", id->valuestring); + snprintf(destination->ssid, sizeof(destination->ssid), "%s", ssid->valuestring); + destination->hidden = cJSON_IsTrue(hidden); + destination->active = cJSON_IsTrue(active); + active_count += destination->active; + parsed.profile_count++; + } + valid = valid && active_count <= 1; + cJSON_Delete(root); + if (!valid) + return -1; + parsed.ready = 1; + *wifi = parsed; + return 0; +} + +int bloom_shell_wifi_scan_start_parse(const char *json, BloomShellWifiUi *wifi) +{ + if (json == NULL || wifi == NULL) + return -1; + cJSON *root = cJSON_ParseWithOpts(json, NULL, 1); + const cJSON *operation = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "operation"); + const cJSON *accepted = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "accepted"); + const cJSON *identifier = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "operation_id"); + int valid = exact_object(root, 5) && root_identity(root, "bloom-wifi-scan") && + bounded_string(operation, 16, 1) && strcmp(operation->valuestring, "start") == 0 && + cJSON_IsTrue(accepted) && operation_id(identifier); + if (valid) { + snprintf(wifi->scan_id, sizeof(wifi->scan_id), "%s", identifier->valuestring); + wifi->scan_phase = BLOOM_SHELL_WIFI_SCAN_QUEUED; + snprintf(wifi->scan_error, sizeof(wifi->scan_error), "%s", "none"); + wifi->scan_result_count = 0; + wifi->network_count = 0; + } + cJSON_Delete(root); + return valid ? 0 : -1; +} + +static int phase_value(const cJSON *value, BloomShellWifiScanPhase *phase) +{ + if (!bounded_string(value, 16, 1)) + return 0; + if (strcmp(value->valuestring, "queued") == 0) + *phase = BLOOM_SHELL_WIFI_SCAN_QUEUED; + else if (strcmp(value->valuestring, "running") == 0) + *phase = BLOOM_SHELL_WIFI_SCAN_RUNNING; + else if (strcmp(value->valuestring, "succeeded") == 0) + *phase = BLOOM_SHELL_WIFI_SCAN_SUCCEEDED; + else if (strcmp(value->valuestring, "failed") == 0) + *phase = BLOOM_SHELL_WIFI_SCAN_FAILED; + else if (strcmp(value->valuestring, "cancelled") == 0) + *phase = BLOOM_SHELL_WIFI_SCAN_CANCELLED; + else + return 0; + return 1; +} + +static int scan_error_value(const cJSON *value) +{ + return bounded_string(value, 24, 1) && + (strcmp(value->valuestring, "none") == 0 || + strcmp(value->valuestring, "cancelled") == 0 || + strcmp(value->valuestring, "backend_failed") == 0 || + strcmp(value->valuestring, "output_too_large") == 0 || + strcmp(value->valuestring, "timeout") == 0); +} + +int bloom_shell_wifi_scan_status_parse(const char *json, BloomShellWifiUi *wifi) +{ + if (json == NULL || wifi == NULL) + return -1; + cJSON *root = cJSON_ParseWithOpts(json, NULL, 1); + const cJSON *identifier = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "operation_id"); + const cJSON *phase = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "phase"); + const cJSON *error = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "error"); + const cJSON *count = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "result_count"); + const cJSON *started = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "started_at"); + const cJSON *completed = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "completed_at"); + BloomShellWifiScanPhase parsed_phase = BLOOM_SHELL_WIFI_SCAN_IDLE; + uint64_t parsed_count = 0, parsed_started = 0, parsed_completed = 0; + int valid = exact_object(root, 8) && root_identity(root, "bloom-wifi-scan") && + operation_id(identifier) && phase_value(phase, &parsed_phase) && + scan_error_value(error) && unsigned_integer(count, BLOOM_SHELL_WIFI_NETWORK_LIMIT, &parsed_count) && + unsigned_integer(started, INT_MAX, &parsed_started) && + unsigned_integer(completed, INT_MAX, &parsed_completed) && parsed_started > 0 && + (wifi->scan_id[0] == '\0' || strcmp(wifi->scan_id, identifier->valuestring) == 0); + if (valid && (parsed_phase == BLOOM_SHELL_WIFI_SCAN_QUEUED || + parsed_phase == BLOOM_SHELL_WIFI_SCAN_RUNNING)) + valid = parsed_count == 0 && parsed_completed == 0 && + strcmp(error->valuestring, "none") == 0; + else if (valid && parsed_phase == BLOOM_SHELL_WIFI_SCAN_SUCCEEDED) + valid = parsed_completed >= parsed_started && strcmp(error->valuestring, "none") == 0; + else if (valid && parsed_phase == BLOOM_SHELL_WIFI_SCAN_FAILED) + valid = parsed_completed >= parsed_started && strcmp(error->valuestring, "none") != 0 && + strcmp(error->valuestring, "cancelled") != 0; + else if (valid && parsed_phase == BLOOM_SHELL_WIFI_SCAN_CANCELLED) + valid = parsed_count == 0 && parsed_completed >= parsed_started && + strcmp(error->valuestring, "cancelled") == 0; + if (valid) { + snprintf(wifi->scan_id, sizeof(wifi->scan_id), "%s", identifier->valuestring); + wifi->scan_phase = parsed_phase; + snprintf(wifi->scan_error, sizeof(wifi->scan_error), "%s", error->valuestring); + wifi->scan_result_count = (size_t)parsed_count; + wifi->scan_started_at = parsed_started; + wifi->scan_completed_at = parsed_completed; + } + cJSON_Delete(root); + return valid ? 0 : -1; +} + +int bloom_shell_wifi_scan_results_parse(const char *json, BloomShellWifiUi *wifi) +{ + if (json == NULL || wifi == NULL) + return -1; + cJSON *root = cJSON_ParseWithOpts(json, NULL, 1); + const cJSON *networks = root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "networks"); + BloomShellWifiNetwork parsed[BLOOM_SHELL_WIFI_NETWORK_LIMIT] = {0}; + int valid = exact_object(root, 3) && root_identity(root, "bloom-wifi-scan") && + cJSON_IsArray(networks) && cJSON_GetArraySize(networks) >= 0 && + cJSON_GetArraySize(networks) <= BLOOM_SHELL_WIFI_NETWORK_LIMIT && + wifi->scan_phase == BLOOM_SHELL_WIFI_SCAN_SUCCEEDED && + wifi->scan_result_count == (size_t)cJSON_GetArraySize(networks); + for (int index = 0; valid && index < cJSON_GetArraySize(networks); ++index) { + const cJSON *entry = cJSON_GetArrayItem(networks, index); + const cJSON *ssid = cJSON_GetObjectItemCaseSensitive(entry, "ssid"); + const cJSON *security = cJSON_GetObjectItemCaseSensitive(entry, "security"); + const cJSON *signal = cJSON_GetObjectItemCaseSensitive(entry, "signal_dbm"); + valid = exact_object(entry, 3) && bounded_string(ssid, 32, 1) && + security_value(security, 1, &parsed[index].security) && cJSON_IsNumber(signal) && + signal->valuedouble == (double)signal->valueint && signal->valueint >= -200 && + signal->valueint <= 0; + for (int prior = 0; valid && prior < index; ++prior) + if (strcmp(parsed[prior].ssid, ssid->valuestring) == 0) + valid = 0; + if (!valid) + break; + snprintf(parsed[index].ssid, sizeof(parsed[index].ssid), "%s", ssid->valuestring); + parsed[index].signal_dbm = signal->valueint; + } + if (valid) { + wifi->network_count = (size_t)cJSON_GetArraySize(networks); + memset(wifi->networks, 0, sizeof(wifi->networks)); + memcpy(wifi->networks, parsed, wifi->network_count * sizeof(parsed[0])); + } + cJSON_Delete(root); + return valid ? 0 : -1; +} diff --git a/src/bloomShell/bloom_shell_wifi_ui.h b/src/bloomShell/bloom_shell_wifi_ui.h new file mode 100644 index 000000000..6382c516d --- /dev/null +++ b/src/bloomShell/bloom_shell_wifi_ui.h @@ -0,0 +1,59 @@ +#ifndef BLOOM_SHELL_WIFI_UI_H +#define BLOOM_SHELL_WIFI_UI_H + +#include +#include + +#define BLOOM_SHELL_WIFI_PROFILE_LIMIT 16 +#define BLOOM_SHELL_WIFI_NETWORK_LIMIT 32 + +typedef enum { + BLOOM_SHELL_WIFI_SECURITY_OPEN = 0, + BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK, + BLOOM_SHELL_WIFI_SECURITY_UNSUPPORTED, +} BloomShellWifiSecurity; + +typedef enum { + BLOOM_SHELL_WIFI_SCAN_IDLE = 0, + BLOOM_SHELL_WIFI_SCAN_QUEUED, + BLOOM_SHELL_WIFI_SCAN_RUNNING, + BLOOM_SHELL_WIFI_SCAN_SUCCEEDED, + BLOOM_SHELL_WIFI_SCAN_FAILED, + BLOOM_SHELL_WIFI_SCAN_CANCELLED, +} BloomShellWifiScanPhase; + +typedef struct { + char id[33]; + char ssid[33]; + BloomShellWifiSecurity security; + int hidden; + int active; +} BloomShellWifiProfile; + +typedef struct { + char ssid[33]; + BloomShellWifiSecurity security; + int signal_dbm; +} BloomShellWifiNetwork; + +typedef struct { + int ready; + size_t profile_count; + BloomShellWifiProfile profiles[BLOOM_SHELL_WIFI_PROFILE_LIMIT]; + char scan_id[33]; + BloomShellWifiScanPhase scan_phase; + char scan_error[25]; + size_t scan_result_count; + uint64_t scan_started_at; + uint64_t scan_completed_at; + size_t network_count; + BloomShellWifiNetwork networks[BLOOM_SHELL_WIFI_NETWORK_LIMIT]; +} BloomShellWifiUi; + +void bloom_shell_wifi_ui_init(BloomShellWifiUi *wifi); +int bloom_shell_wifi_profiles_parse(const char *json, BloomShellWifiUi *wifi); +int bloom_shell_wifi_scan_start_parse(const char *json, BloomShellWifiUi *wifi); +int bloom_shell_wifi_scan_status_parse(const char *json, BloomShellWifiUi *wifi); +int bloom_shell_wifi_scan_results_parse(const char *json, BloomShellWifiUi *wifi); + +#endif diff --git a/static/build/.tmp_update/bin/bloom-wifi-profiles b/static/build/.tmp_update/bin/bloom-wifi-profiles index 7a34fdae5..f450773c5 100755 --- a/static/build/.tmp_update/bin/bloom-wifi-profiles +++ b/static/build/.tmp_update/bin/bloom-wifi-profiles @@ -252,6 +252,29 @@ store_status() { printf '{"schema":1,"service":"bloom-wifi-profiles","initialized":true,"profile_count":%s,"projection_managed":%s}\n' "$count" "$managed" } +list_profiles() { + if [ ! -e "$ROOT" ] && [ ! -L "$ROOT" ]; then + printf '{"schema":1,"service":"bloom-wifi-profiles","profiles":[]}\n' + return 0 + fi + [ -d "$ROOT" ] && [ ! -L "$ROOT" ] || fail "profile-store root is unsafe" + if [ ! -e "$STORE" ] && [ ! -L "$STORE" ]; then + printf '{"schema":1,"service":"bloom-wifi-profiles","profiles":[]}\n' + return 0 + fi + validate_store "$STORE" || fail "profile store is invalid" + active= + if [ -e "$CONFIG" ] || [ -L "$CONFIG" ]; then + [ -f "$CONFIG" ] && [ ! -L "$CONFIG" ] || fail "supplicant configuration is unsafe" + if grep -Fqx '# managed-by=BloomOS wifi-profile-store-v1' "$CONFIG"; then + marker=$(grep '^# bloom-profile-id=' "$CONFIG" 2>/dev/null || true) + active=${marker#*=} + safe_profile_id "$active" || fail "managed profile id is invalid" + fi + fi + "$JQ" -c --arg active "$active" '{schema:1,service:"bloom-wifi-profiles",profiles:[.profiles[]|{id,ssid,security,hidden,active:(.id==$active)}]}' "$STORE" || fail "cannot list profiles" +} + project_profile() { profile_id=$1 safe_profile_id "$profile_id" || fail "profile id is invalid" @@ -317,6 +340,7 @@ project_profile() { case "$#:${1:-}" in 1:replace) replace_store ;; 2:status) [ "$2" = --json ] || fail "invalid arguments"; store_status ;; + 2:list) [ "$2" = --json ] || fail "invalid arguments"; list_profiles ;; 2:upsert) upsert_profile "$2" ;; 2:delete) delete_profile "$2" ;; 2:project) project_profile "$2" ;; diff --git a/static/build/.tmp_update/bin/bloomctl b/static/build/.tmp_update/bin/bloomctl index 2036c0107..d28dcf22a 100755 --- a/static/build/.tmp_update/bin/bloomctl +++ b/static/build/.tmp_update/bin/bloomctl @@ -684,6 +684,10 @@ network_command() { [ -x "$BLOOM_WIFI_PROFILES" ] || { printf 'bloomctl: Wi-Fi profile service is unavailable\n' >&2; return 1; } "$BLOOM_WIFI_PROFILES" status --json ;; + 2:profile:list) + [ -x "$BLOOM_WIFI_PROFILES" ] || { printf 'bloomctl: Wi-Fi profile service is unavailable\n' >&2; return 1; } + "$BLOOM_WIFI_PROFILES" list --json + ;; 3:profile:upsert) case "$3" in ''|.*|-*|*[!a-z0-9._-]*) usage; return 2 ;; esac [ "${#3}" -le 32 ] || { usage; return 2; } diff --git a/test/Makefile b/test/Makefile index 091a4c3e3..8bae57295 100644 --- a/test/Makefile +++ b/test/Makefile @@ -60,6 +60,7 @@ CFILES := $(CFILES) ../src/bloomShell/bloom_shell_collection_picker.c CFILES := $(CFILES) ../src/bloomShell/bloom_shell_search.c CFILES := $(CFILES) ../src/bloomShell/bloom_shell_library_menu.c CFILES := $(CFILES) ../src/bloomShell/bloom_shell_library_operations.c +CFILES := $(CFILES) ../src/bloomShell/bloom_shell_wifi_ui.c CFILES := $(CFILES) ../src/bloomShell/bloom_shell_achievements.c CFILES := $(CFILES) ../src/bloomShell/bloom_shell_browse.c CFILES := $(CFILES) ../src/bloomShell/bloom_shell_filter_picker.c diff --git a/test/shell/bloom_platform.bats b/test/shell/bloom_platform.bats index e505ea60b..db002690d 100644 --- a/test/shell/bloom_platform.bats +++ b/test/shell/bloom_platform.bats @@ -218,7 +218,7 @@ assert evidence["state_preservation"]["session_state_after"] == "STOPPED" assert evidence["registry"]["capabilities"]["network.hotspot"]["status"] == "unavailable" assert evidence["registry"]["capabilities"]["lid.hall_sensor"]["status"] == "not_applicable" assert any("not physical" in item.lower() for item in evidence["limitations"]) -assert package["status"] == "published" +assert package["status"] == "merged" assert package["github_prs"] == [480] assert package["last_audited_main"] == "e965215bb21756dcd6b7e786374720ff6168f830" PY diff --git a/test/shell/bloomctl_info.bats b/test/shell/bloomctl_info.bats index ff4db3185..5eb8f39f1 100644 --- a/test/shell/bloomctl_info.bats +++ b/test/shell/bloomctl_info.bats @@ -814,6 +814,11 @@ SH [ "$status" -eq 0 ] [ "$output" = '{"schema":1,"profile_arguments":"status --json","input":""}' ] + run env BLOOM_NETWORK_BIN="$service" BLOOM_WIFI_PROFILES_BIN="$profiles" \ + sh /workspace/static/build/.tmp_update/bin/bloomctl network profile list + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"profile_arguments":"list --json","input":""}' ] + run bash -c 'printf %s "{synthetic}" | BLOOM_NETWORK_BIN="$1" BLOOM_WIFI_PROFILES_BIN="$2" sh /workspace/static/build/.tmp_update/bin/bloomctl network profile upsert fixture-home' _ "$service" "$profiles" [ "$status" -eq 0 ] [ "$output" = '{"schema":1,"profile_arguments":"upsert fixture-home","input":"{synthetic}"}' ] diff --git a/test/shell/wifi_profiles.bats b/test/shell/wifi_profiles.bats index 733ce39d6..d369b3665 100644 --- a/test/shell/wifi_profiles.bats +++ b/test/shell/wifi_profiles.bats @@ -89,6 +89,25 @@ replace_from_input() { [[ "$output" == *'"projection_managed":true'* ]] } +@test "explicit local list exposes bounded profile metadata but never credentials" { + write_valid_store + replace_from_input + + run "$SERVICE" list --json + [ "$status" -eq 0 ] + [ "$output" = '{"schema":1,"service":"bloom-wifi-profiles","profiles":[{"id":"fixture-wpa2","ssid":"Fixture WPA2","security":"wpa2-psk","hidden":false,"active":false},{"id":"fixture-open","ssid":"Fixture Open","security":"open","hidden":false,"active":false}]}' ] + [[ "$output" != *'test-only-passphrase'* ]] + [[ "$output" != *'"psk"'* ]] + + run "$SERVICE" project fixture-wpa2 + [ "$status" -eq 0 ] + run "$SERVICE" list --json + [ "$status" -eq 0 ] + [ "$(printf '%s' "$output" | jq '[.profiles[] | select(.active)] | length')" -eq 1 ] + [ "$(printf '%s' "$output" | jq -r '.profiles[] | select(.active) | .id')" = fixture-wpa2 ] + [[ "$output" != *'test-only-passphrase'* ]] +} + @test "explicit open projection contains no credential and hidden open is rejected" { write_valid_store replace_from_input diff --git a/test/test_bloom_shell_wifi_ui.cpp b/test/test_bloom_shell_wifi_ui.cpp new file mode 100644 index 000000000..acf5003ce --- /dev/null +++ b/test/test_bloom_shell_wifi_ui.cpp @@ -0,0 +1,77 @@ +#include "gtest/gtest.h" + +extern "C" { +#include "../src/bloomShell/bloom_shell_wifi_ui.h" +} + +TEST(BloomShellWifiUi, ParsesOnlyCredentialFreeBoundedProfiles) +{ + BloomShellWifiUi wifi{}; + ASSERT_EQ(0, bloom_shell_wifi_profiles_parse( + R"({"schema":1,"service":"bloom-wifi-profiles","profiles":[{"id":"home","ssid":"Home WiFi","security":"wpa2-psk","hidden":false,"active":true},{"id":"guest","ssid":"Guest","security":"open","hidden":false,"active":false}]})", + &wifi)); + ASSERT_TRUE(wifi.ready); + ASSERT_EQ(2U, wifi.profile_count); + EXPECT_STREQ("home", wifi.profiles[0].id); + EXPECT_STREQ("Home WiFi", wifi.profiles[0].ssid); + EXPECT_TRUE(wifi.profiles[0].active); + + EXPECT_NE(0, bloom_shell_wifi_profiles_parse( + R"({"schema":1,"service":"bloom-wifi-profiles","profiles":[{"id":"home","ssid":"Home WiFi","security":"wpa2-psk","hidden":false,"active":true,"psk":"must-not-enter-ui"}]})", + &wifi)); + EXPECT_NE(0, bloom_shell_wifi_profiles_parse( + R"({"schema":1,"service":"bloom-wifi-profiles","profiles":[{"id":"home","ssid":"One","security":"open","hidden":false,"active":true},{"id":"home","ssid":"Two","security":"open","hidden":false,"active":true}]})", + &wifi)); +} + +TEST(BloomShellWifiUi, TracksOneBoundedAsynchronousScan) +{ + BloomShellWifiUi wifi{}; + ASSERT_EQ(0, bloom_shell_wifi_scan_start_parse( + R"({"schema":1,"service":"bloom-wifi-scan","operation":"start","accepted":true,"operation_id":"1234567890-abcdef0123456789"})", + &wifi)); + EXPECT_EQ(BLOOM_SHELL_WIFI_SCAN_QUEUED, wifi.scan_phase); + ASSERT_EQ(0, bloom_shell_wifi_scan_status_parse( + R"({"schema":1,"service":"bloom-wifi-scan","operation_id":"1234567890-abcdef0123456789","phase":"running","error":"none","result_count":0,"started_at":100,"completed_at":0})", + &wifi)); + EXPECT_EQ(BLOOM_SHELL_WIFI_SCAN_RUNNING, wifi.scan_phase); + ASSERT_EQ(0, bloom_shell_wifi_scan_status_parse( + R"({"schema":1,"service":"bloom-wifi-scan","operation_id":"1234567890-abcdef0123456789","phase":"succeeded","error":"none","result_count":2,"started_at":100,"completed_at":103})", + &wifi)); + EXPECT_EQ(BLOOM_SHELL_WIFI_SCAN_SUCCEEDED, wifi.scan_phase); + EXPECT_EQ(2U, wifi.scan_result_count); + + EXPECT_NE(0, bloom_shell_wifi_scan_status_parse( + R"({"schema":1,"service":"bloom-wifi-scan","operation_id":"1234567890-deadbeefdeadbeef","phase":"succeeded","error":"none","result_count":2,"started_at":100,"completed_at":103})", + &wifi)); + + BloomShellWifiUi failed{}; + ASSERT_EQ(0, bloom_shell_wifi_scan_status_parse( + R"({"schema":1,"service":"bloom-wifi-scan","operation_id":"1234567890-abcdef0123456789","phase":"failed","error":"output_too_large","result_count":0,"started_at":100,"completed_at":103})", + &failed)); + EXPECT_EQ(BLOOM_SHELL_WIFI_SCAN_FAILED, failed.scan_phase); +} + +TEST(BloomShellWifiUi, ParsesPrivacySafeNetworksAndRejectsMalformedState) +{ + BloomShellWifiUi wifi{}; + ASSERT_EQ(0, bloom_shell_wifi_scan_status_parse( + R"({"schema":1,"service":"bloom-wifi-scan","operation_id":"1234567890-abcdef0123456789","phase":"succeeded","error":"none","result_count":2,"started_at":100,"completed_at":103})", + &wifi)); + ASSERT_EQ(0, bloom_shell_wifi_scan_results_parse( + R"({"schema":1,"service":"bloom-wifi-scan","networks":[{"ssid":"Home WiFi","security":"wpa2-psk","signal_dbm":-42},{"ssid":"Cafe","security":"unsupported","signal_dbm":-71}]})", + &wifi)); + ASSERT_EQ(2U, wifi.network_count); + EXPECT_STREQ("Home WiFi", wifi.networks[0].ssid); + EXPECT_EQ(BLOOM_SHELL_WIFI_SECURITY_UNSUPPORTED, wifi.networks[1].security); + + EXPECT_NE(0, bloom_shell_wifi_scan_results_parse( + R"({"schema":1,"service":"bloom-wifi-scan","networks":[{"ssid":"Home WiFi","security":"wpa2-psk","signal_dbm":-42,"password":"secret"}]})", + &wifi)); + EXPECT_NE(0, bloom_shell_wifi_scan_results_parse( + R"({"schema":1,"service":"bloom-wifi-scan","networks":[{"ssid":"Home WiFi","security":"wpa2-psk","signal_dbm":-42}]})", + &wifi)); + EXPECT_NE(0, bloom_shell_wifi_scan_status_parse( + R"({"schema":1,"service":"bloom-wifi-scan","operation_id":"1234567890-abcdef0123456789","phase":"succeeded","error":"backend_failed","result_count":0,"started_at":100,"completed_at":101})", + &wifi)); +} diff --git a/test/test_ci_scope.sh b/test/test_ci_scope.sh index ade469a18..c761b7240 100755 --- a/test/test_ci_scope.sh +++ b/test/test_ci_scope.sh @@ -177,6 +177,12 @@ expect "bloomShell" build "src/bloomUi/bloom_ui_typography.c" expect "BloomUiTypography*" native-tests "build/bloom-typography.json" expect "/workspace/test/shell/bloom_typography.bats" shell-tests "tools/validate_bloom_typography.py" expect "/workspace/test/shell/bloom_shell_runtime.bats /workspace/test/shell/bloom_shell_safe_mode.bats /workspace/test/shell/bloom_shell_settings.bats /workspace/test/shell/bloom_typography.bats /workspace/test/shell/gameswitcher_controls.bats" shell-tests "src/bloomShell/main.c" +expect "BloomShellWifiUi*" native-tests "src/bloomShell/bloom_shell_wifi_ui.c +static/build/.tmp_update/bin/bloom-wifi-profiles +test/shell/wifi_scan.bats" +expect "/workspace/test/shell/wifi_profiles.bats /workspace/test/shell/wifi_scan.bats" shell-tests "src/bloomShell/bloom_shell_wifi_ui.h +static/build/.tmp_update/bin/bloom-wifi-profiles +static/build/.tmp_update/bin/bloom-wifi-scan" expect "full" shell-tests "build/content-identity-policy.json tools/validate_content_identity_policy.py src/bloomLibrary/bloom_library_database.c" diff --git a/tools/ci-scope.sh b/tools/ci-scope.sh index f1420d2bb..95fd32914 100755 --- a/tools/ci-scope.sh +++ b/tools/ci-scope.sh @@ -304,6 +304,15 @@ while IFS= read -r path; do add_shell_target /workspace/test/shell/bloom_typography.bats add_shell_target /workspace/test/shell/gameswitcher_controls.bats ;; + src/bloomShell/bloom_shell_wifi_ui.c|src/bloomShell/bloom_shell_wifi_ui.h|\ + test/test_bloom_shell_wifi_ui.cpp|static/build/.tmp_update/bin/bloom-wifi-profiles|\ + static/build/.tmp_update/bin/bloom-wifi-scan|test/shell/wifi_profiles.bats|\ + test/shell/wifi_scan.bats) + raise_risk 2 + add_native_filter 'BloomShellWifiUi*' + add_shell_target /workspace/test/shell/wifi_profiles.bats + add_shell_target /workspace/test/shell/wifi_scan.bats + ;; build/content-identity-policy.json|tools/validate_content_identity_policy.py|\ test/shell/content_identity_policy.bats|src/bloomLibrary/bloom_library_database.c|\ src/bloomLibrary/bloom_library_database.h|test/test_bloom_library_database.cpp)