diff --git a/docs/1.0_TEST_MATRIX.md b/docs/1.0_TEST_MATRIX.md index 74422a878..3bf88adf1 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; host-only 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, and collision/security/full-store rejection; 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; host-only 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, and one editing/submitting controller with 100-ms poll opportunities, exact cancellation, terminal clearing, and bounded focus return; 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 d1a286feb..b9853c00e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -597,6 +597,14 @@ retried within a fixed bound. Exhausted collision retries, a full profile store, unsupported security class, missing snapshot, or saved/scan security mismatch fails closed before form state is created. +One event-loop-independent profile controller owns the mapped form and its +single-flight upsert request. It admits input only while editing, polls only at +100-ms monotonic opportunities while submitting, clears the complete form at +every terminal result, and kills and reaps the exact request process group on +cancel or destroy. The controller retains only the bounded non-secret list row +needed to return focus after completion, failure, or cancellation. It remains +unreachable from the rendered Shell in this host-only slice. + 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 ef07cc6f1..970136c8a 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -405,6 +405,15 @@ bounded SDL wake; only an exact schema-1 upsert acknowledgement is success. The request client exists for host validation but remains unreachable until the rendered form/controller slice explicitly owns cancellation and focus return. +Use `BloomShellWifiProfileController` for that lifecycle rather than retaining a +form and request separately. It owns exactly one editing or submitting state, +accepts input only while editing, exposes 100-ms monotonic poll opportunities, +clears form and secret bytes on every terminal path, and kills and reaps the +request process group on cancellation or teardown. Its `return_item` is a +bounded non-secret row index for deterministic focus restoration; it is not a +profile, SSID, or association authority. The controller remains host-only until +the rendered integration slice adds explicit input and cancellation ownership. + 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 a3f183c7d..df3c95ac3 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -177,4 +177,12 @@ retry a collision within a fixed bound. Exhaustion, an unavailable profile snapshot, and a full store fail closed, so the future scan UI cannot overwrite an unrelated credential document. +The host-only profile controller is the sole lifecycle owner for an edited +credential and its upsert child. Input is refused outside the editing state; +submission clears the password before returning; every terminal, cancellation, +and teardown path clears the complete form; and cancellation kills and reaps the +exact child process group. Only a bounded non-secret focus row survives a +terminal transition. This controller has no rendered or association consumer in +its initial slice. + 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 c0d01a364..384d2dcde 100644 --- a/docs/program/BloomOS_Work_Program_v3.json +++ b/docs/program/BloomOS_Work_Program_v3.json @@ -887,12 +887,14 @@ 548, 549, 550, - 551 + 551, + 552, + 553 ], "risk_tier": 1, "deployment_tier": "hot", "device_gate_required": true, - "last_audited_main": "49f0a8096bff9a9157e4ce176201d2f532bb74dd", + "last_audited_main": "7c67002caf89e767d318d97f751a0b92e93d827e", "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", @@ -910,7 +912,9 @@ "GitHub PR #550 passed format in 23 seconds, the complete 58-test affected shell selector in 46 seconds, native tests in 1 minute 11 seconds, AddressSanitizer in 1 minute 17 seconds, the affected Bloom Shell ARM build in 1 minute 23 seconds, and scoped static analysis in 2 minutes 22 seconds; it merged by squash as 0ef8e110c0afd197b3c63a400c6e02afd0fec8f9", "GitHub PR #551 evolves profile submission into a bounded nonblocking request: start writes stdin and clears the password before returning, poll accepts one exact schema-1 upsert acknowledgement, and malformed, oversized, failed, or timed-out children are reaped through the exact process group; no rendered consumer, association, restart, forget, or device mutation is added", "GitHub PR #551 passed format in 14 seconds, the complete 58-test affected shell selector in 50 seconds, AddressSanitizer in 1 minute 15 seconds, native tests in 1 minute 20 seconds, the affected Bloom Shell ARM build in 1 minute 23 seconds, and scoped static analysis in 2 minutes 18 seconds; it merged by squash as 49f0a8096bff9a9157e4ce176201d2f532bb74dd", - "GitHub PR #552 adds a host-only selection boundary that requires a current validated profile snapshot, reuses an exact saved SSID only when its security matches, creates a 104-bit high-entropy no-follow random handle for a new network, and rejects malformed state, unsupported security, full stores, security mismatch, unavailable state, and bounded collision exhaustion before any form or request can be created" + "GitHub PR #552 adds a host-only selection boundary that requires a current validated profile snapshot, reuses an exact saved SSID only when its security matches, creates a 104-bit high-entropy no-follow random handle for a new network, and rejects malformed state, unsupported security, full stores, security mismatch, unavailable state, and bounded collision exhaustion before any form or request can be created", + "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" ] }, { diff --git a/src/bloomShell/bloom_shell_wifi_ui.c b/src/bloomShell/bloom_shell_wifi_ui.c index c5a54056b..945c47a46 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.c +++ b/src/bloomShell/bloom_shell_wifi_ui.c @@ -1027,6 +1027,140 @@ int bloom_shell_wifi_profile_request_poll(BloomShellWifiRequest *request, return profile_upsert_ack_parse(request->output) == 0 ? 1 : -1; } +void bloom_shell_wifi_profile_controller_init(BloomShellWifiProfileController *controller) +{ + if (controller == NULL) + return; + memset(controller, 0, sizeof(*controller)); + bloom_shell_wifi_request_init(&controller->request); +} + +int bloom_shell_wifi_profile_controller_begin(BloomShellWifiProfileController *controller, + const BloomShellWifiUi *wifi, + size_t network_index) +{ + if (controller == NULL || + controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING || + controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING) + return -1; + bloom_shell_wifi_request_destroy(&controller->request); + bloom_shell_wifi_profile_form_clear(&controller->form); + controller->next_poll_ms = 0; + controller->return_item = 0; + if (bloom_shell_wifi_profile_form_from_network( + wifi, network_index, &controller->form) != 0) { + controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED; + return -1; + } + controller->return_item = network_index + 1; + controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING; + return 0; +} + +int bloom_shell_wifi_profile_controller_move(BloomShellWifiProfileController *controller, + int horizontal, int vertical) +{ + if (controller == NULL || + controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING || + controller->form.security != BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK) + return -1; + return bloom_shell_wifi_profile_form_move(&controller->form, horizontal, vertical); +} + +void bloom_shell_wifi_profile_controller_cycle_mode( + BloomShellWifiProfileController *controller) +{ + if (controller != NULL && + controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING && + controller->form.security == BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK) + bloom_shell_wifi_profile_form_cycle_mode(&controller->form); +} + +int bloom_shell_wifi_profile_controller_append(BloomShellWifiProfileController *controller) +{ + if (controller == NULL || + controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING) + return -1; + return bloom_shell_wifi_profile_form_append(&controller->form); +} + +int bloom_shell_wifi_profile_controller_backspace(BloomShellWifiProfileController *controller) +{ + if (controller == NULL || + controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING) + return -1; + return bloom_shell_wifi_profile_form_backspace(&controller->form); +} + +int bloom_shell_wifi_profile_controller_submit(BloomShellWifiProfileController *controller, + const char *bloomctl_path, + uint64_t now_ms) +{ + if (controller == NULL || + controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING) + return -1; + if (bloom_shell_wifi_profile_request_start( + &controller->request, bloomctl_path, &controller->form, now_ms) != 0) { + bloom_shell_wifi_profile_form_clear(&controller->form); + controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED; + return -1; + } + controller->next_poll_ms = now_ms + BLOOM_SHELL_WIFI_PROFILE_POLL_INTERVAL_MS; + controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING; + return 0; +} + +int bloom_shell_wifi_profile_controller_poll(BloomShellWifiProfileController *controller, + uint64_t now_ms) +{ + if (controller == NULL || + controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING) + return -1; + if (now_ms < controller->next_poll_ms) + return 0; + int result = bloom_shell_wifi_profile_request_poll(&controller->request, 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; + bloom_shell_wifi_profile_form_clear(&controller->form); + controller->state = result > 0 ? BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_COMPLETE + : BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED; + return result; +} + +int bloom_shell_wifi_profile_controller_cancel(BloomShellWifiProfileController *controller) +{ + if (controller == NULL || + (controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING && + controller->state != BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING)) + return -1; + bloom_shell_wifi_request_destroy(&controller->request); + bloom_shell_wifi_profile_form_clear(&controller->form); + controller->next_poll_ms = 0; + controller->state = BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_CANCELLED; + return 0; +} + +int bloom_shell_wifi_profile_controller_active( + const BloomShellWifiProfileController *controller) +{ + return controller != NULL && + (controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING || + controller->state == BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING); +} + +void bloom_shell_wifi_profile_controller_destroy(BloomShellWifiProfileController *controller) +{ + if (controller == NULL) + return; + bloom_shell_wifi_request_destroy(&controller->request); + bloom_shell_wifi_profile_form_clear(&controller->form); + secure_clear(controller, sizeof(*controller)); + bloom_shell_wifi_request_init(&controller->request); +} + int bloom_shell_wifi_profile_form_submit(const char *bloomctl_path, BloomShellWifiProfileForm *form) { diff --git a/src/bloomShell/bloom_shell_wifi_ui.h b/src/bloomShell/bloom_shell_wifi_ui.h index 339ec6742..534bf8724 100644 --- a/src/bloomShell/bloom_shell_wifi_ui.h +++ b/src/bloomShell/bloom_shell_wifi_ui.h @@ -12,6 +12,7 @@ #define BLOOM_SHELL_WIFI_REQUEST_OUTPUT_LIMIT 16384 #define BLOOM_SHELL_WIFI_REQUEST_TIMEOUT_MS 5000 #define BLOOM_SHELL_WIFI_SCAN_POLL_INTERVAL_MS 250 +#define BLOOM_SHELL_WIFI_PROFILE_POLL_INTERVAL_MS 100 typedef enum { BLOOM_SHELL_WIFI_SECURITY_OPEN = 0, @@ -85,6 +86,23 @@ typedef struct { char output[BLOOM_SHELL_WIFI_REQUEST_OUTPUT_LIMIT + 2]; } BloomShellWifiRequest; +typedef enum { + BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_IDLE = 0, + BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_EDITING, + BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING, + BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_COMPLETE, + BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED, + BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_CANCELLED, +} BloomShellWifiProfileControllerState; + +typedef struct { + BloomShellWifiProfileForm form; + BloomShellWifiRequest request; + BloomShellWifiProfileControllerState state; + uint64_t next_poll_ms; + size_t return_item; +} BloomShellWifiProfileController; + typedef enum { BLOOM_SHELL_WIFI_CONTROLLER_IDLE = 0, BLOOM_SHELL_WIFI_CONTROLLER_STARTING, @@ -129,6 +147,25 @@ int bloom_shell_wifi_profile_request_start(BloomShellWifiRequest *request, uint64_t now_ms); int bloom_shell_wifi_profile_request_poll(BloomShellWifiRequest *request, uint64_t now_ms); +void bloom_shell_wifi_profile_controller_init(BloomShellWifiProfileController *controller); +int bloom_shell_wifi_profile_controller_begin(BloomShellWifiProfileController *controller, + const BloomShellWifiUi *wifi, + size_t network_index); +int bloom_shell_wifi_profile_controller_move(BloomShellWifiProfileController *controller, + int horizontal, int vertical); +void bloom_shell_wifi_profile_controller_cycle_mode( + BloomShellWifiProfileController *controller); +int bloom_shell_wifi_profile_controller_append(BloomShellWifiProfileController *controller); +int bloom_shell_wifi_profile_controller_backspace(BloomShellWifiProfileController *controller); +int bloom_shell_wifi_profile_controller_submit(BloomShellWifiProfileController *controller, + const char *bloomctl_path, + uint64_t now_ms); +int bloom_shell_wifi_profile_controller_poll(BloomShellWifiProfileController *controller, + uint64_t now_ms); +int bloom_shell_wifi_profile_controller_cancel(BloomShellWifiProfileController *controller); +int bloom_shell_wifi_profile_controller_active( + const BloomShellWifiProfileController *controller); +void bloom_shell_wifi_profile_controller_destroy(BloomShellWifiProfileController *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/test/test_bloom_shell_wifi_ui.cpp b/test/test_bloom_shell_wifi_ui.cpp index e661b64da..4eba51774 100644 --- a/test/test_bloom_shell_wifi_ui.cpp +++ b/test/test_bloom_shell_wifi_ui.cpp @@ -164,6 +164,115 @@ TEST(BloomShellWifiUi, MapsScanResultsToCollisionSafeProfileHandles) EXPECT_NE(0, bloom_shell_wifi_profile_form_from_network(&wifi, 1, &form)); } +TEST(BloomShellWifiUi, ControlsOneBoundedProfileFormLifecycle) +{ + auto directory = std::filesystem::temp_directory_path() / + ("bloom-shell-wifi-profile-controller-" + + std::to_string(getpid())); + std::filesystem::remove_all(directory); + std::filesystem::create_directories(directory); + auto script = directory / "bloomctl"; + auto input_path = directory / "input"; + { + std::ofstream output(script); + output << "#!/bin/sh\ncat > '" << input_path.string() + << "'\nsleep 0.05\nprintf '%s\\n' '{\"schema\":1,\"service\":\"bloom-wifi-profiles\",\"operation\":\"upsert\",\"published\":true,\"replaced\":false,\"profile_count\":1}'\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + + BloomShellWifiUi wifi{}; + wifi.ready = 1; + wifi.network_count = 2; + snprintf(wifi.networks[0].ssid, sizeof(wifi.networks[0].ssid), "%s", + "Fixture Secure"); + wifi.networks[0].security = BLOOM_SHELL_WIFI_SECURITY_WPA2_PSK; + snprintf(wifi.networks[1].ssid, sizeof(wifi.networks[1].ssid), "%s", + "Fixture Open"); + wifi.networks[1].security = BLOOM_SHELL_WIFI_SECURITY_OPEN; + + BloomShellWifiProfileController controller; + bloom_shell_wifi_profile_controller_init(&controller); + EXPECT_EQ(BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_IDLE, controller.state); + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_begin(&controller, &wifi, 0)); + EXPECT_TRUE(bloom_shell_wifi_profile_controller_active(&controller)); + EXPECT_EQ(1U, controller.return_item); + EXPECT_NE(0, bloom_shell_wifi_profile_controller_begin(&controller, &wifi, 1)); + for (int index = 0; index < 8; ++index) + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_append(&controller)); + char label[96]; + ASSERT_EQ(0, bloom_shell_wifi_profile_form_label( + &controller.form, label, sizeof(label))); + EXPECT_STREQ("Password: ********", label); + EXPECT_EQ(std::string::npos, std::string(label).find("11111111")); + + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_submit( + &controller, script.c_str(), 100)); + EXPECT_EQ(BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_SUBMITTING, controller.state); + EXPECT_STREQ("", controller.form.psk); + EXPECT_NE(0, bloom_shell_wifi_profile_controller_append(&controller)); + EXPECT_NE(0, bloom_shell_wifi_profile_controller_backspace(&controller)); + EXPECT_NE(0, bloom_shell_wifi_profile_controller_move(&controller, 1, 0)); + EXPECT_EQ(0, bloom_shell_wifi_profile_controller_poll(&controller, 199)); + int result = 0; + for (uint64_t attempt = 0; result == 0 && attempt < 100; ++attempt) { + result = bloom_shell_wifi_profile_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_PROFILE_CONTROLLER_COMPLETE, controller.state); + EXPECT_FALSE(bloom_shell_wifi_profile_controller_active(&controller)); + EXPECT_STREQ("", controller.form.profile_id); + EXPECT_EQ(1U, controller.return_item); + EXPECT_NE(0, bloom_shell_wifi_profile_controller_poll(&controller, 1000)); + + std::ifstream profile_input(input_path); + std::string recorded_profile; + std::getline(profile_input, recorded_profile); + EXPECT_NE(std::string::npos, recorded_profile.find("Fixture Secure")); + EXPECT_NE(std::string::npos, recorded_profile.find("11111111")); + + { + std::ofstream output(script); + output << "#!/bin/sh\ncat >/dev/null\nsleep 10\n"; + } + ASSERT_EQ(0, chmod(script.c_str(), 0700)); + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_begin(&controller, &wifi, 1)); + EXPECT_EQ(2U, controller.return_item); + EXPECT_NE(0, bloom_shell_wifi_profile_controller_move(&controller, 1, 0)); + EXPECT_NE(0, bloom_shell_wifi_profile_controller_append(&controller)); + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_cancel(&controller)); + EXPECT_EQ(BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_CANCELLED, controller.state); + EXPECT_STREQ("", controller.form.ssid); + EXPECT_EQ(2U, controller.return_item); + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_begin(&controller, &wifi, 1)); + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_submit( + &controller, script.c_str(), 500)); + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_cancel(&controller)); + EXPECT_EQ(BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_CANCELLED, controller.state); + EXPECT_FALSE(bloom_shell_wifi_profile_controller_active(&controller)); + EXPECT_STREQ("", controller.form.ssid); + EXPECT_EQ(2U, controller.return_item); + + wifi.networks[1].security = BLOOM_SHELL_WIFI_SECURITY_UNSUPPORTED; + EXPECT_NE(0, bloom_shell_wifi_profile_controller_begin(&controller, &wifi, 1)); + EXPECT_EQ(BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED, controller.state); + EXPECT_EQ(0U, controller.return_item); + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_begin(&controller, &wifi, 0)); + ASSERT_EQ(0, bloom_shell_wifi_profile_controller_append(&controller)); + EXPECT_NE(0, bloom_shell_wifi_profile_controller_submit( + &controller, script.c_str(), 600)); + EXPECT_EQ(BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_FAILED, controller.state); + EXPECT_STREQ("", controller.form.psk); + + bloom_shell_wifi_profile_controller_destroy(&controller); + EXPECT_EQ(BLOOM_SHELL_WIFI_PROFILE_CONTROLLER_IDLE, controller.state); + EXPECT_EQ(-1, controller.request.output_fd); + EXPECT_EQ(0U, controller.return_item); + std::filesystem::remove_all(directory); +} + TEST(BloomShellWifiUi, SubmitsFixedProfileArgumentsViaStdinAndClearsSecret) { auto directory = std::filesystem::temp_directory_path() /