diff --git a/build/bloom-typography.json b/build/bloom-typography.json index 179fb4d10..d13c5c12f 100644 --- a/build/bloom-typography.json +++ b/build/bloom-typography.json @@ -96,7 +96,7 @@ "unclassified_data": "unchanged", "fragment_kinds": ["copy", "data"], "maximum_fragments": 8, - "classified_copy_call_sites": 86, + "classified_copy_call_sites": 91, "classified_mixed_call_sites": 15 } } diff --git a/docs/1.0_TEST_MATRIX.md b/docs/1.0_TEST_MATRIX.md index c078e3769..3e4e83ef5 100644 --- a/docs/1.0_TEST_MATRIX.md +++ b/docs/1.0_TEST_MATRIX.md @@ -50,7 +50,7 @@ shoulder button. | 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; 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; Settings, Shell Quick Settings, and GameSwitcher Quick Settings converge on `--settings-wifi`; direct radio toggle removed; bounded fixed-argv profile-list load outside render; connection state and saved-network detail; selectable Scan/Cancel row and read-only discovered networks; active-only 100-ms SDL wake; deferred GameSwitcher until cancellation discard; single-flight nonblocking fixed-argv scan client; 16-KiB output and five-second deadline; child process-group cleanup; controller-owned start/status/results/discard and cancel-during-start sequencing; 250-ms status opportunities; exact profile/scan/ack schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; terminal history discarded; open/WPA2 profile form with shared keyboard, masked secret, fixed profile-upsert argv, escaped stdin JSON, strict ID/SSID/PSK validation, explicit secret clearing, nonblocking submit/poll, 4-KiB acknowledgement cap, exact upsert response, timeout process-group cleanup, exact saved-ID reuse, 104-bit no-follow random new handles, collision/security/full-store rejection, one editing/submitting controller with 100-ms poll opportunities, exact cancellation, terminal clearing, bounded focus return, supported scan-result selection, explicit saved-not-connected result, and no implicit association; credential and unknown-field rejection; passwords absent from arguments, logs, screenshots, durable Shell state, and evidence | +| Wi-Fi UI | Original Mini hidden; Settings, Shell Quick Settings, and GameSwitcher Quick Settings converge on `--settings-wifi`; direct radio toggle removed; bounded fixed-argv profile-list load outside render; connection state and saved-network detail; selectable Scan/Cancel row and read-only discovered networks; active-only 100-ms SDL wake; deferred GameSwitcher until cancellation discard; single-flight nonblocking fixed-argv scan client; 16-KiB output and five-second deadline; child process-group cleanup; controller-owned start/status/results/discard and cancel-during-start sequencing; 250-ms status opportunities; exact profile/scan/ack schemas; 16 profiles and 32 networks; unique bounded IDs/SSIDs; one active profile; fixed scan phases/errors; terminal history discarded; open/WPA2 profile form with shared keyboard, masked secret, fixed profile-upsert argv, escaped stdin JSON, strict ID/SSID/PSK validation, explicit secret clearing, nonblocking submit/poll, 4-KiB acknowledgement cap, exact upsert response, timeout process-group cleanup, exact saved-ID reuse, 104-bit no-follow random new handles, collision/security/full-store rejection, one editing/submitting controller with 100-ms poll opportunities, exact cancellation, terminal clearing, bounded focus return, supported scan-result selection, explicit saved-not-connected result, and no implicit association; explicit inactive-profile X/confirm forget flow; active-row refusal in Shell and store; fixed nonblocking delete; exact acknowledgement; terminal projection reload; graceful TERM cleanup before bounded KILL; 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 5fee12fad..7e01163b3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -614,6 +614,16 @@ returns to the exact bounded row and reports `Network saved - not connected` on success. No result from this surface projects, associates, restarts, enables, or disables the station. +Saved-profile management uses a separate fixed nonblocking delete request and +controller. X is offered only on the saved-profile view, a confirmation dialog +precedes submission, and both the Shell snapshot and private profile service +refuse the projected active profile. Terminal and cancellation paths reload the +credential-free projection because publication may have completed before an +acknowledgement was read. Process teardown sends TERM first so the profile +service can remove its exact lock and incoming file, then uses a bounded KILL +fallback. Reopening Wi-Fi resets terminal scan presentation before loading the +saved list. Forget never projects, associates, restarts, or disables Wi-Fi. + 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 60e7becf4..52f336f3d 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -417,6 +417,12 @@ That integration may open the form only from a supported bounded scan row. It must label a successful upsert as saved but not connected, return focus to the originating row, cancel before GameSwitcher or Shell teardown, and never invoke profile projection, association, radio power, DHCP, or listener reconciliation. +An inactive saved profile may be forgotten only after the explicit X/confirm +flow. The request must remain fixed-argv and nonblocking, reject the active row +before spawn, accept only the exact delete acknowledgement, and reload the +credential-free list on every terminal or cancellation path. Request teardown +must allow the profile service's TERM trap to clean its private lock and +incoming candidate before a bounded KILL fallback. The NET-04 scan slice is asynchronous and keeps result data only under `/tmp`: diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 877aefd2f..bd4cee500 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -187,5 +187,11 @@ its initial slice. The rendered follow-up invokes it only from a supported bounded scan row, renders password state solely through the masked label, and returns a non-secret `saved - not connected` result. It adds no projection, association, network transaction, radio, DHCP, listener, reboot, or update path. +The saved-profile forget surface retains only a bounded non-secret profile ID, +requires explicit confirmation, and never submits an active row. The private +store independently refuses its projected profile and publishes deletion +atomically. A cancelled request reloads the redacted list and first gives the +service a bounded TERM cleanup opportunity, preventing abandoned private lock +or incoming-store artifacts before any forced process-group kill. Security-sensitive migrations and repair operations must be bounded, logged, reversible where possible, and validated before activation. diff --git a/docs/program/BloomOS_Work_Program_v3.json b/docs/program/BloomOS_Work_Program_v3.json index d06ac309f..e44c30134 100644 --- a/docs/program/BloomOS_Work_Program_v3.json +++ b/docs/program/BloomOS_Work_Program_v3.json @@ -890,12 +890,13 @@ 551, 552, 553, - 554 + 554, + 555 ], "risk_tier": 1, "deployment_tier": "hot", "device_gate_required": true, - "last_audited_main": "ad66ede878c07e2c7877361ca08feb22b6890f97", + "last_audited_main": "736b25dcda4502ee6342f267309a8ec7d16a001b", "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", @@ -917,7 +918,9 @@ "GitHub PR #552 passed formatting in 13 seconds, the complete 58-test affected shell selector in 45 seconds, the affected Bloom Shell ARM build in 1 minute 12 seconds, native tests in 1 minute 15 seconds, AddressSanitizer in 1 minute 22 seconds, and scoped static analysis in 1 minute 45 seconds; it merged by squash as 7c67002caf89e767d318d97f751a0b92e93d827e", "GitHub PR #553 gives the credential form one host-only lifecycle owner with editing and submitting states, 100-ms monotonic poll opportunities, input rejection outside editing, exact request cancellation and teardown, terminal form clearing, and a bounded non-secret return row for deterministic focus restoration; it still has no rendered, association, radio, or device consumer", "GitHub PR #553 passed formatting in 16 seconds, the complete 58-test affected shell selector in 55 seconds, native tests in 1 minute 8 seconds, AddressSanitizer in 1 minute 19 seconds, the affected Bloom Shell ARM build in 1 minute 29 seconds, and scoped static analysis in 2 minutes 16 seconds; it merged by squash as ad66ede878c07e2c7877361ca08feb22b6890f97", - "GitHub PR #554 opens the controller only from a supported bounded scan row, uses the shared masked keyboard for WPA2, stores through the fixed nonblocking upsert request, cancels before GameSwitcher or focus return, reports success explicitly as saved but not connected, and adds no projection, association, radio, DHCP, listener, reboot, update, or device action" + "GitHub PR #554 opens the controller only from a supported bounded scan row, uses the shared masked keyboard for WPA2, stores through the fixed nonblocking upsert request, cancels before GameSwitcher or focus return, reports success explicitly as saved but not connected, and adds no projection, association, radio, DHCP, listener, reboot, update, or device action", + "GitHub PR #554 passed formatting in 14 seconds, the focused shell workflow in 40 seconds, native tests in 1 minute 21 seconds, AddressSanitizer in 1 minute 26 seconds, the affected Bloom Shell ARM build in 1 minute 36 seconds, and scoped static analysis in 2 minutes 37 seconds; it merged by squash as 736b25dcda4502ee6342f267309a8ec7d16a001b", + "GitHub PR #555 adds an explicit X/confirm surface for inactive-profile management, refuses the active profile before spawn and again in the store, uses one fixed nonblocking delete request with exact acknowledgement parsing, reloads the redacted projection after terminal or cancellation state, and gives the service a bounded TERM cleanup window before forced process-group teardown; it adds no projection, association, radio, DHCP, listener, reboot, update, or device action" ] }, { diff --git a/src/bloomShell/bloom_shell_wifi_ui.c b/src/bloomShell/bloom_shell_wifi_ui.c index 945c47a46..fc3e72fed 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.c +++ b/src/bloomShell/bloom_shell_wifi_ui.c @@ -821,11 +821,15 @@ int bloom_shell_wifi_request_start(BloomShellWifiRequest *request, BloomShellWifiRequestKind kind, const char *operation_id_value, uint64_t now_ms) { - const char *operation = request_operation(kind); + int profile_delete = kind == BLOOM_SHELL_WIFI_REQUEST_PROFILE_DELETE; + const char *operation = profile_delete ? NULL : request_operation(kind); int needs_identifier = kind != BLOOM_SHELL_WIFI_REQUEST_SCAN_START; if (request == NULL || request->child > 0 || request->output_fd != -1 || - !request_kind_valid(kind) || !executable_regular(bloomctl_path) || - (needs_identifier && !operation_id_string(operation_id_value)) || + (!request_kind_valid(kind) && !profile_delete) || + !executable_regular(bloomctl_path) || + (needs_identifier && + !(profile_delete ? safe_id_string(operation_id_value) + : operation_id_string(operation_id_value))) || (!needs_identifier && operation_id_value != NULL && operation_id_value[0] != '\0')) return -1; int output[2]; @@ -849,7 +853,10 @@ int bloom_shell_wifi_request_start(BloomShellWifiRequest *request, if (null_descriptor > STDERR_FILENO) close(null_descriptor); close_inherited_descriptors(); - if (needs_identifier) + if (profile_delete) + execl(bloomctl_path, bloomctl_path, "network", "forget", + operation_id_value, (char *)NULL); + else if (needs_identifier) execl(bloomctl_path, bloomctl_path, "network", "scan", operation, operation_id_value, (char *)NULL); else @@ -885,7 +892,8 @@ int bloom_shell_wifi_request_start(BloomShellWifiRequest *request, static int request_read(BloomShellWifiRequest *request) { - size_t limit = request->kind == BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT + size_t limit = request->kind == BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT || + request->kind == BLOOM_SHELL_WIFI_REQUEST_PROFILE_DELETE ? BLOOM_SHELL_WIFI_PROFILE_OUTPUT_LIMIT : BLOOM_SHELL_WIFI_REQUEST_OUTPUT_LIMIT; while (request->output_length <= limit) { @@ -972,6 +980,46 @@ static int profile_upsert_ack_parse(const char *json) return valid ? 0 : -1; } +static int profile_delete_ack_parse(const char *json) +{ + cJSON *root = cJSON_ParseWithOpts(json, NULL, 1); + const cJSON *operation = + root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "operation"); + const cJSON *deleted = + root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "deleted"); + const cJSON *count = + root == NULL ? NULL : cJSON_GetObjectItemCaseSensitive(root, "profile_count"); + uint64_t parsed_count = 0; + int valid = exact_object(root, 5) && root_identity(root, "bloom-wifi-profiles") && + bounded_string(operation, 16, 1) && + strcmp(operation->valuestring, "delete") == 0 && + cJSON_IsTrue(deleted) && + unsigned_integer(count, BLOOM_SHELL_WIFI_PROFILE_LIMIT - 1, + &parsed_count); + cJSON_Delete(root); + return valid ? 0 : -1; +} + +static void request_terminate_child(pid_t child) +{ + if (child <= 0) + return; + kill(-child, SIGTERM); + kill(child, SIGTERM); + for (int attempt = 0; attempt < 10; ++attempt) { + pid_t result = waitpid(child, NULL, WNOHANG); + if (result == child || (result < 0 && errno == ECHILD)) + return; + if (result < 0 && errno != EINTR) + break; + usleep(10000); + } + kill(-child, SIGKILL); + kill(child, SIGKILL); + while (waitpid(child, NULL, 0) < 0 && errno == EINTR) { + } +} + static int request_finish(BloomShellWifiRequest *request, uint64_t now_ms) { if (request == NULL || request->child <= 0 || request->output_fd < 0) @@ -981,10 +1029,7 @@ static int request_finish(BloomShellWifiRequest *request, uint64_t now_ms) pid_t result = waitpid(request->child, &status, WNOHANG); if (read_result < 0 || (result == 0 && now_ms - request->started_ms >= BLOOM_SHELL_WIFI_REQUEST_TIMEOUT_MS)) { - kill(-request->child, SIGKILL); - kill(request->child, SIGKILL); - while (waitpid(request->child, NULL, 0) < 0 && errno == EINTR) { - } + request_terminate_child(request->child); request_close(request); return -1; } @@ -1007,12 +1052,15 @@ static int request_finish(BloomShellWifiRequest *request, uint64_t now_ms) int bloom_shell_wifi_request_poll(BloomShellWifiRequest *request, BloomShellWifiUi *wifi, uint64_t now_ms) { - if (wifi == NULL || request == NULL || + if (request == NULL || + (wifi == NULL && request->kind != BLOOM_SHELL_WIFI_REQUEST_PROFILE_DELETE) || request->kind == BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT) return -1; int result = request_finish(request, now_ms); if (result <= 0) return result; + if (request->kind == BLOOM_SHELL_WIFI_REQUEST_PROFILE_DELETE) + return profile_delete_ack_parse(request->output) == 0 ? 1 : -1; return request_parse(request, wifi) == 0 ? 1 : -1; } @@ -1161,6 +1209,112 @@ void bloom_shell_wifi_profile_controller_destroy(BloomShellWifiProfileController bloom_shell_wifi_request_init(&controller->request); } +void bloom_shell_wifi_forget_controller_init(BloomShellWifiForgetController *controller) +{ + if (controller == NULL) + return; + memset(controller, 0, sizeof(*controller)); + bloom_shell_wifi_request_init(&controller->request); +} + +int bloom_shell_wifi_forget_controller_begin(BloomShellWifiForgetController *controller, + const BloomShellWifiUi *wifi, + size_t profile_index) +{ + if (controller == NULL || wifi == NULL || !wifi->ready || + profile_index >= wifi->profile_count || + profile_index >= BLOOM_SHELL_WIFI_PROFILE_LIMIT || + controller->state == BLOOM_SHELL_WIFI_FORGET_CONTROLLER_READY || + controller->state == BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING) + return -1; + bloom_shell_wifi_request_destroy(&controller->request); + controller->next_poll_ms = 0; + controller->return_item = 0; + controller->profile_id[0] = '\0'; + const BloomShellWifiProfile *profile = &wifi->profiles[profile_index]; + if (!safe_id_string(profile->id) || profile->active != 0) { + controller->state = BLOOM_SHELL_WIFI_FORGET_CONTROLLER_FAILED; + return -1; + } + snprintf(controller->profile_id, sizeof(controller->profile_id), "%s", + profile->id); + controller->return_item = profile_index + 1; + controller->state = BLOOM_SHELL_WIFI_FORGET_CONTROLLER_READY; + return 0; +} + +int bloom_shell_wifi_forget_controller_submit(BloomShellWifiForgetController *controller, + const char *bloomctl_path, + uint64_t now_ms) +{ + if (controller == NULL || + controller->state != BLOOM_SHELL_WIFI_FORGET_CONTROLLER_READY || + !safe_id_string(controller->profile_id)) + return -1; + if (bloom_shell_wifi_request_start( + &controller->request, bloomctl_path, + BLOOM_SHELL_WIFI_REQUEST_PROFILE_DELETE, controller->profile_id, + now_ms) != 0) { + controller->profile_id[0] = '\0'; + controller->state = BLOOM_SHELL_WIFI_FORGET_CONTROLLER_FAILED; + return -1; + } + controller->next_poll_ms = now_ms + BLOOM_SHELL_WIFI_PROFILE_POLL_INTERVAL_MS; + controller->state = BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING; + return 0; +} + +int bloom_shell_wifi_forget_controller_poll(BloomShellWifiForgetController *controller, + uint64_t now_ms) +{ + if (controller == NULL || + controller->state != BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING) + return -1; + if (now_ms < controller->next_poll_ms) + return 0; + int result = bloom_shell_wifi_request_poll(&controller->request, NULL, now_ms); + if (result == 0) { + controller->next_poll_ms = + now_ms + BLOOM_SHELL_WIFI_PROFILE_POLL_INTERVAL_MS; + return 0; + } + controller->next_poll_ms = 0; + controller->profile_id[0] = '\0'; + controller->state = result > 0 ? BLOOM_SHELL_WIFI_FORGET_CONTROLLER_COMPLETE + : BLOOM_SHELL_WIFI_FORGET_CONTROLLER_FAILED; + return result; +} + +int bloom_shell_wifi_forget_controller_cancel(BloomShellWifiForgetController *controller) +{ + if (controller == NULL || + (controller->state != BLOOM_SHELL_WIFI_FORGET_CONTROLLER_READY && + controller->state != BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING)) + return -1; + bloom_shell_wifi_request_destroy(&controller->request); + controller->next_poll_ms = 0; + controller->profile_id[0] = '\0'; + controller->state = BLOOM_SHELL_WIFI_FORGET_CONTROLLER_CANCELLED; + return 0; +} + +int bloom_shell_wifi_forget_controller_active( + const BloomShellWifiForgetController *controller) +{ + return controller != NULL && + (controller->state == BLOOM_SHELL_WIFI_FORGET_CONTROLLER_READY || + controller->state == BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING); +} + +void bloom_shell_wifi_forget_controller_destroy(BloomShellWifiForgetController *controller) +{ + if (controller == NULL) + return; + bloom_shell_wifi_request_destroy(&controller->request); + memset(controller, 0, sizeof(*controller)); + bloom_shell_wifi_request_init(&controller->request); +} + int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, BloomShellWifiProfileForm *form) { @@ -1188,12 +1342,8 @@ void bloom_shell_wifi_request_destroy(BloomShellWifiRequest *request) { if (request == NULL) return; - if (request->child > 0) { - kill(-request->child, SIGKILL); - kill(request->child, SIGKILL); - while (waitpid(request->child, NULL, 0) < 0 && errno == EINTR) { - } - } + if (request->child > 0) + request_terminate_child(request->child); if (request->output_fd >= 0) close(request->output_fd); bloom_shell_wifi_request_init(request); diff --git a/src/bloomShell/bloom_shell_wifi_ui.h b/src/bloomShell/bloom_shell_wifi_ui.h index 534bf8724..14e6b105c 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.h +++ b/src/bloomShell/bloom_shell_wifi_ui.h @@ -74,6 +74,7 @@ typedef enum { BLOOM_SHELL_WIFI_REQUEST_SCAN_CANCEL, BLOOM_SHELL_WIFI_REQUEST_SCAN_DISCARD, BLOOM_SHELL_WIFI_REQUEST_PROFILE_UPSERT, + BLOOM_SHELL_WIFI_REQUEST_PROFILE_DELETE, } BloomShellWifiRequestKind; typedef struct { @@ -103,6 +104,23 @@ typedef struct { size_t return_item; } BloomShellWifiProfileController; +typedef enum { + BLOOM_SHELL_WIFI_FORGET_CONTROLLER_IDLE = 0, + BLOOM_SHELL_WIFI_FORGET_CONTROLLER_READY, + BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING, + BLOOM_SHELL_WIFI_FORGET_CONTROLLER_COMPLETE, + BLOOM_SHELL_WIFI_FORGET_CONTROLLER_FAILED, + BLOOM_SHELL_WIFI_FORGET_CONTROLLER_CANCELLED, +} BloomShellWifiForgetControllerState; + +typedef struct { + BloomShellWifiRequest request; + BloomShellWifiForgetControllerState state; + uint64_t next_poll_ms; + size_t return_item; + char profile_id[33]; +} BloomShellWifiForgetController; + typedef enum { BLOOM_SHELL_WIFI_CONTROLLER_IDLE = 0, BLOOM_SHELL_WIFI_CONTROLLER_STARTING, @@ -166,6 +184,19 @@ int bloom_shell_wifi_profile_controller_cancel(BloomShellWifiProfileController * int bloom_shell_wifi_profile_controller_active( const BloomShellWifiProfileController *controller); void bloom_shell_wifi_profile_controller_destroy(BloomShellWifiProfileController *controller); +void bloom_shell_wifi_forget_controller_init(BloomShellWifiForgetController *controller); +int bloom_shell_wifi_forget_controller_begin(BloomShellWifiForgetController *controller, + const BloomShellWifiUi *wifi, + size_t profile_index); +int bloom_shell_wifi_forget_controller_submit(BloomShellWifiForgetController *controller, + const char *bloomctl_path, + uint64_t now_ms); +int bloom_shell_wifi_forget_controller_poll(BloomShellWifiForgetController *controller, + uint64_t now_ms); +int bloom_shell_wifi_forget_controller_cancel(BloomShellWifiForgetController *controller); +int bloom_shell_wifi_forget_controller_active( + const BloomShellWifiForgetController *controller); +void bloom_shell_wifi_forget_controller_destroy(BloomShellWifiForgetController *controller); void bloom_shell_wifi_profile_form_clear(BloomShellWifiProfileForm *form); int bloom_shell_wifi_scan_start_parse(const char *json, BloomShellWifiUi *wifi); int bloom_shell_wifi_scan_status_parse(const char *json, BloomShellWifiUi *wifi); diff --git a/src/bloomShell/main.c b/src/bloomShell/main.c index 6287b2435..e203bfb82 100644 --- a/src/bloomShell/main.c +++ b/src/bloomShell/main.c @@ -2013,6 +2013,18 @@ static size_t wifi_detail_item_count(const BloomShellWifiUi *wifi, return 1 + (items > 0 ? items : 1); } +static int wifi_surface_reload_profiles( + BloomShellWifiUi *wifi, BloomShellWifiController *controller) +{ + if (wifi == NULL || controller == NULL || + bloom_shell_wifi_controller_active(controller) || + wifi->scan_id[0] != '\0') + return -1; + bloom_shell_wifi_controller_destroy(controller); + bloom_shell_wifi_ui_init(wifi); + return bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, wifi); +} + static const char *wifi_scan_action_label(const BloomShellWifiController *controller) { switch (controller->state) { @@ -2043,7 +2055,8 @@ static void draw_wifi_details(SDL_Surface *screen, const BloomUiLayout *layout, const BloomShellQuickValues *quick_values, const BloomShellWifiUi *wifi, const BloomShellWifiController *controller, - const BloomUiFocus *focus, int profile_result) + const BloomUiFocus *focus, int profile_result, + int forget_result) { SDL_Color cream = {243, 226, 189, 0}; SDL_Color sand = {214, 187, 137, 0}; @@ -2063,7 +2076,13 @@ static void draw_wifi_details(SDL_Surface *screen, const BloomUiLayout *layout, layout->content.y + layout->row_height, layout->content.width - 40, sand); render_copy_label(screen, compact_font, - profile_result > 0 + forget_result > 0 + ? "Network forgotten" + : forget_result == -2 + ? "Active network cannot be forgotten" + : forget_result < 0 + ? "Network could not be forgotten" + : profile_result > 0 ? "Network saved - not connected" : profile_result < 0 ? "Network could not be saved" @@ -2073,7 +2092,9 @@ static void draw_wifi_details(SDL_Surface *screen, const BloomUiLayout *layout, layout->content.x + 20, layout->content.y + layout->row_height * 2, layout->content.width - 40, - profile_result < 0 ? (SDL_Color){226, 169, 59, 0} : sand); + profile_result < 0 || forget_result < 0 + ? (SDL_Color){226, 169, 59, 0} + : sand); int top = layout->content.y + layout->row_height * 3; size_t visible_rows = layout->visible_rows > 3 ? layout->visible_rows - 3 : 1; for (size_t row = 0; @@ -2198,6 +2219,55 @@ static void draw_wifi_profile_form( draw_keyboard_glyphs(screen, layout, font, &form->keyboard); } +static void draw_wifi_forget_confirm(SDL_Surface *screen, + const BloomUiLayout *layout, + TTF_Font *font, + const BloomUiDialogFocus *dialog) +{ + if (bloom_ui_render_dialog(screen, layout, dialog) != 0) + return; + SDL_Color cream = {243, 226, 189, 0}; + SDL_Color canvas = {33, 23, 17, 0}; + int width = layout->content.width * 4 / 5; + int height = layout->viewport_height / 3; + int x = (layout->viewport_width - width) / 2; + int y = (layout->viewport_height - height) / 2; + int gap = 12; + int button_width = (width - 48 - gap) / 2; + int button_height = layout->row_height * 2 / 3; + int button_y = y + height - button_height - 20; + render_copy_label(screen, font, "Forget this saved network?", x + 24, + y + 18, width - 48, cream); + render_copy_label(screen, font, "Cancel", x + 24 + button_width / 3, + button_y + button_height / 4, button_width * 2 / 3, + dialog->selected == 0 ? canvas : cream); + render_copy_label(screen, font, "Forget", + x + 24 + button_width + gap + button_width / 4, + button_y + button_height / 4, button_width * 3 / 4, + dialog->selected == 1 ? canvas : cream); +} + +static void draw_wifi_forget_progress( + SDL_Surface *screen, const BloomUiLayout *layout, TTF_Font *font, + const BloomUiTheme *theme, + const BloomShellWifiForgetController *controller) +{ + if (controller->state != BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING) + return; + BloomUiRect panel = layout->content; + if (bloom_ui_render_panel(screen, theme, &panel, 0) != 0) + return; + SDL_Color cream = {243, 226, 189, 0}; + SDL_Color sand = {214, 187, 137, 0}; + render_copy_label(screen, font, "Forgetting network...", + layout->content.x + 20, + layout->content.y + layout->row_height, + layout->content.width - 40, cream); + render_copy_label(screen, font, "B Cancel", layout->content.x + 20, + layout->content.y + layout->row_height * 2, + layout->content.width - 40, sand); +} + static void draw_remote_key_remove_confirm(SDL_Surface *screen, const BloomUiLayout *layout, TTF_Font *font, const BloomUiDialogFocus *dialog) @@ -2797,7 +2867,10 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l const BloomShellWifiUi *wifi, const BloomShellWifiController *wifi_controller, const BloomShellWifiProfileController *wifi_profile_controller, - int wifi_profile_result, int wifi_open, + const BloomShellWifiForgetController *wifi_forget_controller, + int wifi_profile_result, int wifi_forget_result, + int wifi_forget_confirm_open, + const BloomUiDialogFocus *wifi_forget_dialog, int wifi_open, const BloomUiFocus *wifi_focus, const BloomShellRemoteKeys *remote_keys, int remote_keys_open, const BloomUiFocus *remote_keys_focus, int remote_keys_result, @@ -2825,7 +2898,7 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l { int immutable_surface = safe_mode || first_run_open || ra_form_open || update_confirm_open || rollback_confirm_open || reset_confirm_open || - remote_key_remove_confirm_open || + remote_key_remove_confirm_open || wifi_forget_confirm_open || (network_credential != NULL && network_credential->available); if (immutable_surface) { layout = immutable_layout; @@ -3177,6 +3250,11 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l theme); const char *footer = quick_settings ? "Left/Right Change A Toggle/Open B/START Close" + : wifi_forget_confirm_open + ? "Left/Right Choose A Confirm B Cancel" + : bloom_shell_wifi_forget_controller_active( + wifi_forget_controller) + ? "B Cancel" : bloom_shell_wifi_profile_controller_active( wifi_profile_controller) ? wifi_profile_controller->state == @@ -3191,7 +3269,10 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l ? "A Cancel B Settings START Quick" : wifi_focus->selected == 0 ? "A Scan B Settings START Quick" - : "Up/Down Browse L/R Page B Settings" + : wifi_controller->state == + BLOOM_SHELL_WIFI_CONTROLLER_COMPLETE + ? "A Save L/R Page B Settings" + : "X Forget inactive L/R Page B Settings" : remote_keys_open ? remote_keys_focus->selected < remote_keys->key_count ? "X Remove L/R Page B Settings" @@ -3226,11 +3307,18 @@ static void draw(SDL_Surface *screen, SDL_Surface *video, const BloomUiLayout *l remote_keys_result); if (wifi_open && !quick_settings) draw_wifi_details(screen, layout, font, compact_font, theme, quick_values, - wifi, wifi_controller, wifi_focus, wifi_profile_result); + wifi, wifi_controller, wifi_focus, wifi_profile_result, + wifi_forget_result); if (bloom_shell_wifi_profile_controller_active(wifi_profile_controller) && !quick_settings) draw_wifi_profile_form(screen, layout, font, theme, wifi_profile_controller); + if (bloom_shell_wifi_forget_controller_active(wifi_forget_controller) && + !quick_settings) + draw_wifi_forget_progress(screen, layout, font, theme, + wifi_forget_controller); + if (wifi_forget_confirm_open) + draw_wifi_forget_confirm(screen, layout, font, wifi_forget_dialog); if (ra_form_open) draw_ra_form(screen, layout, font, ra_form); if (search->open) @@ -3572,11 +3660,16 @@ int main(int argc, char **argv) bloom_shell_wifi_controller_init(&wifi_controller); BloomShellWifiProfileController wifi_profile_controller; bloom_shell_wifi_profile_controller_init(&wifi_profile_controller); + BloomShellWifiForgetController wifi_forget_controller; + bloom_shell_wifi_forget_controller_init(&wifi_forget_controller); int wifi_profile_result = 0; + int wifi_forget_result = 0; + int wifi_forget_confirm_open = 0; + BloomUiDialogFocus wifi_forget_dialog = {0}; int wifi_game_switcher_pending = 0; int wifi_open = wifi_requested && !safe_mode && capabilities.wifi; if (wifi_open) - bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, &wifi_ui); + wifi_surface_reload_profiles(&wifi_ui, &wifi_controller); BloomUiFocus wifi_focus; bloom_ui_focus_init(&wifi_focus, wifi_detail_item_count(&wifi_ui, &wifi_controller)); @@ -3656,7 +3749,9 @@ int main(int argc, char **argv) immutable_compact_font, destination, &root, &games_browser, &favorites_focus, &recent_focus, &all_games_focus, &collections, games, &achievement_index, favorites, recents, has_recent, &search, browse, game_list_view, &library_menu, &filter_picker, &details, jump_open, jump_letter, &settings_focus, &apps_focus, apps, &status, &capabilities, &quick_values, &network_credential, &wifi_ui, &wifi_controller, - &wifi_profile_controller, wifi_profile_result, wifi_open, &wifi_focus, + &wifi_profile_controller, &wifi_forget_controller, wifi_profile_result, + wifi_forget_result, wifi_forget_confirm_open, &wifi_forget_dialog, + wifi_open, &wifi_focus, &remote_keys, remote_keys_open, &remote_keys_focus, remote_keys_result, remote_key_remove_confirm_open, &remote_key_remove_dialog, &ra_values, support_export_result, @@ -3685,6 +3780,22 @@ int main(int argc, char **argv) &wifi_profile_controller); wifi_profile_result = 0; } + wifi_forget_confirm_open = 0; + if (bloom_shell_wifi_forget_controller_active( + &wifi_forget_controller)) { + size_t return_item = wifi_forget_controller.return_item; + bloom_shell_wifi_forget_controller_cancel( + &wifi_forget_controller); + bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, &wifi_ui); + bloom_ui_focus_set_count( + &wifi_focus, + wifi_detail_item_count(&wifi_ui, &wifi_controller), + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + focus_select( + &wifi_focus, return_item, + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + wifi_forget_result = 0; + } if (bloom_shell_wifi_controller_active(&wifi_controller) || wifi_ui.scan_id[0] != '\0') { wifi_game_switcher_pending = 1; @@ -3785,13 +3896,31 @@ int main(int argc, char **argv) layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); } } + if (wifi_forget_controller.state == + BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING) { + int forget_poll = bloom_shell_wifi_forget_controller_poll( + &wifi_forget_controller, SDL_GetTicks()); + if (forget_poll != 0) { + wifi_forget_result = forget_poll > 0 ? 1 : -1; + wifi_surface_reload_profiles(&wifi_ui, &wifi_controller); + bloom_ui_focus_set_count( + &wifi_focus, + wifi_detail_item_count(&wifi_ui, &wifi_controller), + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + focus_select( + &wifi_focus, wifi_forget_controller.return_item, + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + } + } bloom_ui_focus_set_count( &wifi_focus, wifi_detail_item_count(&wifi_ui, &wifi_controller), layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); if (!bloom_shell_wifi_controller_active(&wifi_controller) && wifi_profile_controller.state != - BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING) { + BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING && + wifi_forget_controller.state != + BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING) { if (wifi_timer != NULL) { SDL_RemoveTimer(wifi_timer); wifi_timer = NULL; @@ -3880,6 +4009,91 @@ int main(int argc, char **argv) } else if (library_menu.operations.open && action != BLOOM_UI_ACTION_NONE) continue; + else if (wifi_forget_confirm_open && + action == BLOOM_UI_ACTION_GAME_SWITCHER) { + wifi_forget_confirm_open = 0; + if (run_game_switcher_child( + destination == BLOOM_UI_DESTINATION_ROOT) > 0) { + exit_code = LAUNCH_READY_EXIT; + running = 0; + } + } + else if (wifi_forget_confirm_open && + action == BLOOM_UI_ACTION_QUICK_SETTINGS) { + wifi_forget_confirm_open = 0; + quick_settings = 1; + } + else if (wifi_forget_confirm_open && + action == BLOOM_UI_ACTION_FOCUS_LEFT) + bloom_ui_dialog_step(&wifi_forget_dialog, -1); + else if (wifi_forget_confirm_open && + action == BLOOM_UI_ACTION_FOCUS_RIGHT) + bloom_ui_dialog_step(&wifi_forget_dialog, 1); + else if (wifi_forget_confirm_open && action == BLOOM_UI_ACTION_BACK) + wifi_forget_confirm_open = 0; + else if (wifi_forget_confirm_open && + action == BLOOM_UI_ACTION_CONFIRM) { + if (wifi_forget_dialog.selected == 1 && + wifi_focus.selected > 0 && + wifi_focus.selected - 1 < wifi_ui.profile_count && + bloom_shell_wifi_forget_controller_begin( + &wifi_forget_controller, &wifi_ui, + wifi_focus.selected - 1) == 0 && + bloom_shell_wifi_forget_controller_submit( + &wifi_forget_controller, BLOOMCTL_BINARY, + SDL_GetTicks()) == 0) { + wifi_forget_result = 0; + if (wifi_timer_start(&wifi_timer) != 0) { + bloom_shell_wifi_forget_controller_cancel( + &wifi_forget_controller); + wifi_surface_reload_profiles(&wifi_ui, &wifi_controller); + wifi_forget_result = -1; + } + } + else if (wifi_forget_dialog.selected == 1) + wifi_forget_result = -1; + wifi_forget_confirm_open = 0; + } + else if (wifi_forget_confirm_open) + continue; + else if (bloom_shell_wifi_forget_controller_active( + &wifi_forget_controller) && + action == BLOOM_UI_ACTION_GAME_SWITCHER) { + size_t return_item = wifi_forget_controller.return_item; + bloom_shell_wifi_forget_controller_cancel( + &wifi_forget_controller); + wifi_surface_reload_profiles(&wifi_ui, &wifi_controller); + bloom_ui_focus_set_count( + &wifi_focus, + wifi_detail_item_count(&wifi_ui, &wifi_controller), + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + focus_select(&wifi_focus, return_item, + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + wifi_forget_result = 0; + if (run_game_switcher_child( + destination == BLOOM_UI_DESTINATION_ROOT) > 0) { + exit_code = LAUNCH_READY_EXIT; + running = 0; + } + } + else if (bloom_shell_wifi_forget_controller_active( + &wifi_forget_controller) && + action == BLOOM_UI_ACTION_BACK) { + size_t return_item = wifi_forget_controller.return_item; + bloom_shell_wifi_forget_controller_cancel( + &wifi_forget_controller); + wifi_surface_reload_profiles(&wifi_ui, &wifi_controller); + bloom_ui_focus_set_count( + &wifi_focus, + wifi_detail_item_count(&wifi_ui, &wifi_controller), + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + focus_select(&wifi_focus, return_item, + layout.visible_rows > 3 ? layout.visible_rows - 3 : 1); + wifi_forget_result = 0; + } + else if (bloom_shell_wifi_forget_controller_active( + &wifi_forget_controller)) + continue; else if (bloom_shell_wifi_profile_controller_active( &wifi_profile_controller) && action == BLOOM_UI_ACTION_GAME_SWITCHER) { @@ -4011,11 +4225,12 @@ int main(int argc, char **argv) break; } } - bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, &wifi_ui); + wifi_surface_reload_profiles(&wifi_ui, &wifi_controller); bloom_ui_focus_init( &wifi_focus, wifi_detail_item_count(&wifi_ui, &wifi_controller)); wifi_profile_result = 0; + wifi_forget_result = 0; wifi_open = 1; } } @@ -4060,6 +4275,7 @@ int main(int argc, char **argv) else if (wifi_open && action == BLOOM_UI_ACTION_CONFIRM && wifi_focus.selected == 0) { wifi_profile_result = 0; + wifi_forget_result = 0; int result = bloom_shell_wifi_controller_active(&wifi_controller) || wifi_ui.scan_id[0] != '\0' ? bloom_shell_wifi_controller_cancel( @@ -4080,11 +4296,27 @@ int main(int argc, char **argv) wifi_focus.selected > 0 && wifi_focus.selected - 1 < wifi_ui.network_count) { wifi_profile_result = 0; + wifi_forget_result = 0; if (bloom_shell_wifi_profile_controller_begin( &wifi_profile_controller, &wifi_ui, wifi_focus.selected - 1) != 0) wifi_profile_result = -1; } + else if (wifi_open && action == BLOOM_UI_ACTION_CONTEXT && + wifi_controller.state != BLOOM_SHELL_WIFI_CONTROLLER_COMPLETE && + wifi_focus.selected > 0 && wifi_ui.ready && + wifi_focus.selected - 1 < wifi_ui.profile_count) { + wifi_profile_result = 0; + wifi_forget_result = 0; + size_t profile_index = wifi_focus.selected - 1; + if (!wifi_ui.profiles[profile_index].active && + bloom_ui_dialog_init(&wifi_forget_dialog, 2, 0, 1) == 0) + wifi_forget_confirm_open = 1; + else + wifi_forget_result = wifi_ui.profiles[profile_index].active + ? -2 + : -1; + } else if (wifi_open && action == BLOOM_UI_ACTION_BACK) { if (bloom_shell_wifi_controller_active(&wifi_controller) || wifi_ui.scan_id[0] != '\0') { @@ -5242,11 +5474,12 @@ int main(int argc, char **argv) else if (action == BLOOM_UI_ACTION_CONFIRM && destination == BLOOM_UI_DESTINATION_SETTINGS && selected_settings_valid && selected_settings.id == BLOOM_SHELL_SETTINGS_WIFI) { - bloom_shell_wifi_profiles_load(BLOOMCTL_BINARY, &wifi_ui); + wifi_surface_reload_profiles(&wifi_ui, &wifi_controller); bloom_ui_focus_init( &wifi_focus, wifi_detail_item_count(&wifi_ui, &wifi_controller)); wifi_profile_result = 0; + wifi_forget_result = 0; wifi_open = 1; } else if (action == BLOOM_UI_ACTION_CONFIRM && @@ -5393,7 +5626,9 @@ int main(int argc, char **argv) &recent_focus, &all_games_focus, &collections, games, &achievement_index, favorites, recents, has_recent, &search, browse, game_list_view, &library_menu, &filter_picker, &details, jump_open, jump_letter, &settings_focus, &apps_focus, apps, &status, &capabilities, &quick_values, &network_credential, &wifi_ui, &wifi_controller, &wifi_profile_controller, - wifi_profile_result, wifi_open, &wifi_focus, &remote_keys, + &wifi_forget_controller, wifi_profile_result, wifi_forget_result, + wifi_forget_confirm_open, &wifi_forget_dialog, wifi_open, + &wifi_focus, &remote_keys, remote_keys_open, &remote_keys_focus, remote_keys_result, remote_key_remove_confirm_open, &remote_key_remove_dialog, &ra_values, support_export_result, @@ -5421,7 +5656,9 @@ int main(int argc, char **argv) has_recent, &search, browse, game_list_view, &library_menu, &filter_picker, &details, jump_open, jump_letter, &settings_focus, &apps_focus, apps, &status, &capabilities, &quick_values, &network_credential, &wifi_ui, &wifi_controller, &wifi_profile_controller, - wifi_profile_result, wifi_open, &wifi_focus, &remote_keys, + &wifi_forget_controller, wifi_profile_result, + wifi_forget_result, wifi_forget_confirm_open, + &wifi_forget_dialog, wifi_open, &wifi_focus, &remote_keys, remote_keys_open, &remote_keys_focus, remote_keys_result, remote_key_remove_confirm_open, &remote_key_remove_dialog, &ra_values, support_export_result, @@ -5447,7 +5684,9 @@ int main(int argc, char **argv) has_recent, &search, browse, game_list_view, &library_menu, &filter_picker, &details, jump_open, jump_letter, &settings_focus, &apps_focus, apps, &status, &capabilities, &quick_values, &network_credential, &wifi_ui, &wifi_controller, &wifi_profile_controller, - wifi_profile_result, wifi_open, &wifi_focus, &remote_keys, + &wifi_forget_controller, wifi_profile_result, + wifi_forget_result, wifi_forget_confirm_open, + &wifi_forget_dialog, wifi_open, &wifi_focus, &remote_keys, remote_keys_open, &remote_keys_focus, remote_keys_result, remote_key_remove_confirm_open, &remote_key_remove_dialog, &ra_values, support_export_result, @@ -5483,6 +5722,7 @@ int main(int argc, char **argv) bloom_shell_library_refresh_destroy(&library_menu.refresh); bloom_shell_wifi_controller_destroy(&wifi_controller); bloom_shell_wifi_profile_controller_destroy(&wifi_profile_controller); + bloom_shell_wifi_forget_controller_destroy(&wifi_forget_controller); if (media_ready) bloom_shell_media_runtime_destroy(&media_runtime); if (theme_media_ready) diff --git a/test/shell/bloom_shell_settings.bats b/test/shell/bloom_shell_settings.bats index 81f915833..160e2ce43 100644 --- a/test/shell/bloom_shell_settings.bats +++ b/test/shell/bloom_shell_settings.bats @@ -108,6 +108,15 @@ grep -F 'bloom_shell_wifi_profile_controller_destroy(' "$shell" grep -F 'BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING' "$shell" grep -F 'wifi_profile_controller.return_item' "$shell" + grep -F 'draw_wifi_forget_confirm(' "$shell" + grep -F '"Network forgotten"' "$shell" + grep -F 'bloom_shell_wifi_forget_controller_begin(' "$shell" + grep -F 'bloom_shell_wifi_forget_controller_submit(' "$shell" + grep -F 'bloom_shell_wifi_forget_controller_poll(' "$shell" + grep -F 'bloom_shell_wifi_forget_controller_cancel(' "$shell" + grep -F 'bloom_shell_wifi_forget_controller_destroy(' "$shell" + grep -F '!wifi_ui.profiles[profile_index].active' "$shell" + grep -F 'wifi_surface_reload_profiles(' "$shell" form_render=$(sed -n '/static void draw_wifi_profile_form(/,/^}/p' "$shell") [[ "$form_render" != *'psk'* ]] ! grep -F 'network connect' "$shell" diff --git a/test/test_bloom_shell_wifi_ui.cpp b/test/test_bloom_shell_wifi_ui.cpp index 4eba51774..07f7f8f8d 100644 --- a/test/test_bloom_shell_wifi_ui.cpp +++ b/test/test_bloom_shell_wifi_ui.cpp @@ -273,6 +273,99 @@ TEST(BloomShellWifiUi, ControlsOneBoundedProfileFormLifecycle) std::filesystem::remove_all(directory); } +TEST(BloomShellWifiUi, ForgetsOnlyValidatedInactiveProfilesNonblocking) +{ + auto directory = std::filesystem::temp_directory_path() / + ("bloom-shell-wifi-forget-controller-" + + std::to_string(getpid())); + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + auto script = directory / "bloomctl"; + auto arguments = directory / "arguments"; + auto terminated = directory / "terminated"; + { + std::ofstream output(script); + output << "#!/bin/sh\nprintf '%s' \"$*\" > '" << arguments.string() + << "'\nsleep 0.05\nprintf '%s\\n' '{\"schema\":1,\"service\":\"bloom-wifi-profiles\",\"operation\":\"delete\",\"deleted\":true,\"profile_count\":1}'\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + + BloomShellWifiUi wifi{}; + wifi.ready = 1; + wifi.profile_count = 2; + snprintf(wifi.profiles[0].id, sizeof(wifi.profiles[0].id), "%s", + "fixture-active"); + wifi.profiles[0].active = 1; + snprintf(wifi.profiles[1].id, sizeof(wifi.profiles[1].id), "%s", + "fixture-inactive"); + + BloomShellWifiForgetController controller; + bloom_shell_wifi_forget_controller_init(&controller); + EXPECT_NE(0, bloom_shell_wifi_forget_controller_begin(&controller, &wifi, 0)); + EXPECT_EQ(BLOOM_SHELL_WIFI_FORGET_CONTROLLER_FAILED, controller.state); + ASSERT_EQ(0, bloom_shell_wifi_forget_controller_begin(&controller, &wifi, 1)); + EXPECT_TRUE(bloom_shell_wifi_forget_controller_active(&controller)); + EXPECT_EQ(2U, controller.return_item); + EXPECT_STREQ("fixture-inactive", controller.profile_id); + ASSERT_EQ(0, bloom_shell_wifi_forget_controller_submit( + &controller, script.c_str(), 100)); + EXPECT_EQ(BLOOM_SHELL_WIFI_FORGET_CONTROLLER_SUBMITTING, controller.state); + int result = 0; + for (uint64_t attempt = 0; result == 0 && attempt < 100; ++attempt) { + result = bloom_shell_wifi_forget_controller_poll( + &controller, 200 + attempt * BLOOM_SHELL_WIFI_PROFILE_POLL_INTERVAL_MS); + if (result == 0) + usleep(10000); + } + EXPECT_EQ(1, result); + EXPECT_EQ(BLOOM_SHELL_WIFI_FORGET_CONTROLLER_COMPLETE, controller.state); + EXPECT_FALSE(bloom_shell_wifi_forget_controller_active(&controller)); + EXPECT_STREQ("", controller.profile_id); + std::ifstream argument_input(arguments); + std::string recorded_arguments; + std::getline(argument_input, recorded_arguments); + EXPECT_EQ("network forget fixture-inactive", recorded_arguments); + + { + std::ofstream output(script); + output << "#!/bin/sh\nprintf '%s\\n' '{\"schema\":1,\"service\":\"bloom-wifi-profiles\",\"operation\":\"delete\",\"deleted\":true,\"profile_count\":1,\"psk\":\"blocked\"}'\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + ASSERT_EQ(0, bloom_shell_wifi_forget_controller_begin(&controller, &wifi, 1)); + ASSERT_EQ(0, bloom_shell_wifi_forget_controller_submit( + &controller, script.c_str(), 300)); + result = 0; + for (uint64_t attempt = 0; result == 0 && attempt < 100; ++attempt) { + result = bloom_shell_wifi_forget_controller_poll( + &controller, 400 + attempt * BLOOM_SHELL_WIFI_PROFILE_POLL_INTERVAL_MS); + if (result == 0) + usleep(10000); + } + EXPECT_EQ(-1, result); + EXPECT_EQ(BLOOM_SHELL_WIFI_FORGET_CONTROLLER_FAILED, controller.state); + + { + std::ofstream output(script); + output << "#!/bin/sh\ntrap 'printf done > \"" << terminated.string() + << "\"; exit 0' TERM\nwhile :; do sleep 1; done\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + ASSERT_EQ(0, bloom_shell_wifi_forget_controller_begin(&controller, &wifi, 1)); + ASSERT_EQ(0, bloom_shell_wifi_forget_controller_submit( + &controller, script.c_str(), 500)); + usleep(50000); + ASSERT_EQ(0, bloom_shell_wifi_forget_controller_cancel(&controller)); + EXPECT_EQ(BLOOM_SHELL_WIFI_FORGET_CONTROLLER_CANCELLED, controller.state); + EXPECT_TRUE(std::filesystem::exists(terminated)); + + snprintf(wifi.profiles[1].id, sizeof(wifi.profiles[1].id), "%s", "../escape"); + EXPECT_NE(0, bloom_shell_wifi_forget_controller_begin(&controller, &wifi, 1)); + bloom_shell_wifi_forget_controller_destroy(&controller); + EXPECT_EQ(BLOOM_SHELL_WIFI_FORGET_CONTROLLER_IDLE, controller.state); + EXPECT_EQ(-1, controller.request.output_fd); + std::filesystem::remove_all(directory); +} + TEST(BloomShellWifiUi, SubmitsFixedProfileArgumentsViaStdinAndClearsSecret) { auto directory = std::filesystem::temp_directory_path() /