diff --git a/.github/workflows/build_parallel.yml b/.github/workflows/build_parallel.yml index 6e1b4d135..401d2fa9c 100644 --- a/.github/workflows/build_parallel.yml +++ b/.github/workflows/build_parallel.yml @@ -2,16 +2,23 @@ name: Build and Push Parallel on: workflow_dispatch: - push: - branches: - - master - tags: - - "*" + inputs: + create_release: + description: "Create the draft release after the full matrix passes" + required: true + type: boolean + default: true pull_request: + types: [labeled] + +concurrency: + group: full-hardware-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: compile_sketch: name: build ${{ matrix.board.name }} + if: ${{ github.event_name == 'workflow_dispatch' || github.event.label.name == 'full-hardware-ci' }} runs-on: ubuntu-latest strategy: fail-fast: false @@ -36,7 +43,7 @@ jobs: - { name: "Marauder CYD 3.5inch", flag: "MARAUDER_CYD_3_5_INCH", fbqn: "esp32:esp32:d32:PartitionScheme=min_spiffs", file_name: "cyd_3_5_inch", tft: true, tft_file: "User_Setup_cyd_3_5_inch.h", build_dir: "d32", addr: "0x1000", idf_ver: "2.0.11", nimble_ver: "1.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "M5Cardputer", flag: "MARAUDER_CARDPUTER", fbqn: "esp32:esp32:esp32s3:PartitionScheme=min_spiffs,FlashSize=8M,PSRAM=disabled", file_name: "m5cardputer", tft: true, tft_file: "User_Setup_marauder_m5cardputer.h", build_dir: "esp32s3", addr: "0x1000", idf_ver: "2.0.11", nimble_ver: "1.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "M5Cardputer ADV", flag: "MARAUDER_CARDPUTER_ADV", fbqn: "esp32:esp32:esp32s3:PartitionScheme=min_spiffs,FlashSize=8M,PSRAM=disabled", file_name: "m5cardputer_adv", tft: true, tft_file: "User_Setup_marauder_m5cardputer_adv.h", build_dir: "esp32s3", addr: "0x1000", idf_ver: "2.0.11", nimble_ver: "1.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - - { name: "ESP32-C5-DevKitC-1", flag: "MARAUDER_C5", fbqn: "esp32:esp32:esp32c5:FlashSize=8M,PartitionScheme=min_spiffs,PSRAM=enabled", file_name: "esp32c5devkitc1", tft: false, tft_file: "", build_dir: "esp32c5", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } + - { name: "ESP32-C5-DevKitC-1", flag: "MARAUDER_C5", fbqn: "esp32:esp32:esp32c5:FlashSize=8M,PartitionScheme=default_8MB,PSRAM=enabled", file_name: "esp32c5devkitc1", tft: false, tft_file: "", build_dir: "esp32c5", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "LilyGo T-Dongle C5", flag: "MARAUDER_T_DONGLE_C5", fbqn: "esp32:esp32:esp32c5:CDCOnBoot=cdc,FlashMode=qio,FlashSize=16M,PartitionScheme=custom,PSRAM=enabled", file_name: "t_dongle_c5", tft: true, tft_file: "User_Setup_marauder_t_dongle_c5.h", tft_repo: "H4W9/TFT_eSPI", tft_ref: "ESP32-C5", build_dir: "esp32c5", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "M5NanoC6", flag: "MARAUDER_M5_NANO_C6", fbqn: "esp32:esp32:esp32c6:CDCOnBoot=cdc,PartitionScheme=min_spiffs", file_name: "m5nanoc6", tft: false, tft_file: "", build_dir: "esp32c6", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "Marauder Pancake", flag: "MARAUDER_PANCAKE", fbqn: "esp32:esp32:esp32c5:FlashSize=8M,PartitionScheme=default_8MB,PSRAM=enabled", file_name: "pancake", tft: true, tft_file: "User_Setup_marauder_pancake.h", build_dir: "esp32c5", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master", tft_repo: "H4W9/TFT_eSPI", tft_ref: "ESP32-C5" } @@ -44,6 +51,8 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v3 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Install Arduino CLI run: | @@ -286,7 +295,7 @@ jobs: name: Create Release runs-on: ubuntu-latest needs: [compile_sketch] - if: ${{ github.event_name == 'workflow_dispatch' }} + if: ${{ github.event_name == 'workflow_dispatch' && inputs.create_release }} permissions: contents: write steps: diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index 50318e33f..4eb462e46 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -99,7 +99,7 @@ jobs: - { name: "Marauder CYD 3.5inch", flag: "MARAUDER_CYD_3_5_INCH", fbqn: "esp32:esp32:d32:PartitionScheme=min_spiffs", file_name: "cyd_3_5_inch", tft: true, tft_file: "User_Setup_cyd_3_5_inch.h", build_dir: "d32", addr: "0x1000", idf_ver: "2.0.11", nimble_ver: "1.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "M5Cardputer", flag: "MARAUDER_CARDPUTER", fbqn: "esp32:esp32:esp32s3:PartitionScheme=min_spiffs,FlashSize=8M,PSRAM=disabled", file_name: "m5cardputer", tft: true, tft_file: "User_Setup_marauder_m5cardputer.h", build_dir: "esp32s3", addr: "0x1000", idf_ver: "2.0.11", nimble_ver: "1.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "M5Cardputer ADV", flag: "MARAUDER_CARDPUTER_ADV", fbqn: "esp32:esp32:esp32s3:PartitionScheme=min_spiffs,FlashSize=8M,PSRAM=disabled", file_name: "m5cardputer_adv", tft: true, tft_file: "User_Setup_marauder_m5cardputer_adv.h", build_dir: "esp32s3", addr: "0x1000", idf_ver: "2.0.11", nimble_ver: "1.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - - { name: "ESP32-C5-DevKitC-1", flag: "MARAUDER_C5", fbqn: "esp32:esp32:esp32c5:FlashSize=8M,PartitionScheme=min_spiffs,PSRAM=enabled", file_name: "esp32c5devkitc1", tft: false, tft_file: "", build_dir: "esp32c5", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } + - { name: "ESP32-C5-DevKitC-1", flag: "MARAUDER_C5", fbqn: "esp32:esp32:esp32c5:FlashSize=8M,PartitionScheme=default_8MB,PSRAM=enabled", file_name: "esp32c5devkitc1", tft: false, tft_file: "", build_dir: "esp32c5", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "LilyGo T-Dongle C5", flag: "MARAUDER_T_DONGLE_C5", fbqn: "esp32:esp32:esp32c5:CDCOnBoot=cdc,FlashMode=qio,FlashSize=16M,PartitionScheme=custom,PSRAM=enabled", file_name: "t_dongle_c5", tft: true, tft_file: "User_Setup_marauder_t_dongle_c5.h", tft_repo: "H4W9/TFT_eSPI", tft_ref: "ESP32-C5", build_dir: "esp32c5", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } - { name: "M5NanoC6", flag: "MARAUDER_M5_NANO_C6", fbqn: "esp32:esp32:esp32c6:CDCOnBoot=cdc,PartitionScheme=min_spiffs", file_name: "m5nanoc6", tft: false, tft_file: "", build_dir: "esp32c6", addr: "0x2000", idf_ver: "3.3.4", nimble_ver: "2.3.8", esp_async: "bigbrodude6119/ESPAsyncWebServer", esp_async_ver: "master" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..1f0b0baec --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Development workflow + +ESP32 Marauder supports many hardware targets, so the full firmware matrix is +reserved for final validation and releases. It is not an iteration loop. + +## Iteration + +1. Work on a local branch without opening a pull request. +2. Run native unit tests and static checks locally. +3. Compile only the hardware targets requested for physical testing. +4. Clearly identify locally compiled test binaries by target and exact commit. +5. Revise locally until the requested hardware behavior is accepted. + +## Pull request gate + +Push the branch and open a pull request only when the change is ready for +review. Pull requests run the comparatively inexpensive native unit-test +workflow. Additional commits should be batched instead of pushed after every +small edit. + +## Full-matrix gate + +When a pull request is merge-ready, a maintainer applies the +`full-hardware-ci` label. That label runs **Build and Push Parallel** against the +pull request's exact current head. Any later source push invalidates that +evidence, so the label must be removed and applied again to validate the new +head before merge. + +The workflow can also be dispatched manually against a final candidate branch +with `create_release` disabled. Do not use the full matrix for ordinary +iteration or to obtain one or two test binaries. + +Untrusted fork code must not receive protected-build credentials. Private V8, +Mini V3, and Dual Mini C5 validation is performed only after review, from an +exact-head branch controlled by the maintainers. + +For a firmware release, dispatch the same workflow against the exact release +source commit with `create_release` enabled. The workflow remains the +authoritative producer of the draft release and its public binaries. diff --git a/esp32_marauder/CommandLine.cpp b/esp32_marauder/CommandLine.cpp index 48f0d1e55..4d6629e2b 100644 --- a/esp32_marauder/CommandLine.cpp +++ b/esp32_marauder/CommandLine.cpp @@ -50,7 +50,9 @@ namespace { #endif void CommandLine::RunSetup() { - Serial.println(this->ascii_art); + #ifndef MARAUDER_V8 + Serial.println(this->ascii_art); + #endif Serial.println(F("\n\n--------------------------------\n")); Serial.println(F(" ESP32 Marauder \n")); @@ -275,11 +277,11 @@ void CommandLine::runCommand(String input) { Serial.println(HELP_UPDATE_CMD_A); Serial.println(HELP_LS_CMD); // GCOVR_EXCL_START -- hardware-only command help entry. - Serial.println(PROTOCOL_INFO_CMD); + Serial.println(HELP_PROTOCOL_INFO_CMD); #ifdef HAS_SD - Serial.println(BACKUP_SPIFFS_CMD); - Serial.println(BACKUP_STATUS_CMD); - Serial.println(RESTORE_SPIFFS_CMD); + Serial.println(HELP_BACKUP_SPIFFS_CMD); + Serial.println(HELP_BACKUP_STATUS_CMD); + Serial.println(HELP_RESTORE_SPIFFS_CMD); #endif // GCOVR_EXCL_STOP Serial.println(HELP_LED_CMD); @@ -1410,11 +1412,11 @@ void CommandLine::runCommand(String input) { this->startScanFromCLI(WIFI_PING_SCAN, TFT_GREEN, "Ping Scan"); } - #ifndef HAS_DUAL_BAND - if (cmd_args.get(0) == ARP_SCAN_CMD) { - this->startScanFromCLI(WIFI_ARP_SCAN, TFT_CYAN, "ARP Scan"); - } - #endif + // ARP discovery uses the active station netif on both legacy and C5 + // dual-band hardware. + if (cmd_args.get(0) == ARP_SCAN_CMD) { + this->startScanFromCLI(WIFI_ARP_SCAN, TFT_CYAN, "ARP Scan"); + } // GPS POI if (cmd_args.get(0) == GPS_POI_CMD) { diff --git a/esp32_marauder/CommandLine.h b/esp32_marauder/CommandLine.h index 8b05aaa03..09129674b 100644 --- a/esp32_marauder/CommandLine.h +++ b/esp32_marauder/CommandLine.h @@ -137,6 +137,10 @@ const char PROGMEM HELP_REBOOT_CMD[] = "reboot"; const char PROGMEM HELP_UPDATE_CMD_A[] = "update -s/-w"; const char PROGMEM HELP_SETTINGS_CMD[] = "settings [-s enable/disable>]/[-r]"; const char PROGMEM HELP_LS_CMD[] = "ls "; +const char PROGMEM HELP_PROTOCOL_INFO_CMD[] = "protocolinfo [--machine ]"; +const char PROGMEM HELP_BACKUP_SPIFFS_CMD[] = "backupspiffs [--machine ] - copy SPIFFS to /spiffs on SD"; +const char PROGMEM HELP_BACKUP_STATUS_CMD[] = "backupstatus [--machine ] - inspect /spiffs on SD"; +const char PROGMEM HELP_RESTORE_SPIFFS_CMD[] = "restorespiffs [--machine ] - restore SPIFFS from /spiffs on SD"; const char PROGMEM HELP_LED_CMD[] = "led -s /-p "; const char PROGMEM HELP_GPS_DATA_CMD[] = "gpsdata"; const char PROGMEM HELP_GPS_CMD[] = "gps [-t] [-g] \r\n [-n] \r\n [-b = use BD vs GB for beidou]"; @@ -221,7 +225,6 @@ class CommandLine { LinkedList parseCommand(String input, char* delim); String toLowerCase(String str); void filterAccessPoints(String filter); - void runCommand(String input); bool checkValueExists(LinkedList* cmd_args_list, int index); bool inRange(int max, int index); //bool apSelected(); @@ -230,6 +233,7 @@ class CommandLine { int argSearch(LinkedList* cmd_args, const char* key); void startScanFromCLI(int scan_mode, uint16_t color, const char* scan_name); + #ifndef MARAUDER_V8 const char* ascii_art = "\r\n" " @@@@@@ \r\n" @@ -256,11 +260,13 @@ class CommandLine { " @@@@@@ \r\n" " @@@@ \r\n" "\r\n"; + #endif public: void RunSetup(); void main(uint32_t currentTime); + void runCommand(String input); }; #endif diff --git a/esp32_marauder/IPv4Range.cpp b/esp32_marauder/IPv4Range.cpp new file mode 100644 index 000000000..5e9c7cfef --- /dev/null +++ b/esp32_marauder/IPv4Range.cpp @@ -0,0 +1,43 @@ +#include "IPv4Range.h" + +namespace marauder { + +namespace { + +bool isContiguousMask(uint32_t mask) { + const uint32_t inverted = ~mask; + return (inverted & (inverted + 1U)) == 0U; +} + +} // namespace + +IPv4HostRange ipv4HostRange(uint32_t address, uint32_t subnetMask) { + const uint32_t network = address & subnetMask; + const uint32_t broadcast = network | ~subnetMask; + const bool valid = isContiguousMask(subnetMask) && + (broadcast - network) >= 2U; + + return {network, broadcast, valid ? network + 1U : 0U, + valid ? broadcast - 1U : 0U, valid}; +} + +uint32_t nextIPv4Host(uint32_t current, const IPv4HostRange& range) { + if (!range.valid || current >= range.last) { + return 0U; + } + if (current < range.first) { + return range.first; + } + return current + 1U; +} + +uint32_t previousIPv4Host(uint32_t current, uint32_t steps, + const IPv4HostRange& range) { + if (!range.valid || current < range.first || current > range.last || + steps > current - range.first) { + return 0U; + } + return current - steps; +} + +} // namespace marauder diff --git a/esp32_marauder/IPv4Range.h b/esp32_marauder/IPv4Range.h new file mode 100644 index 000000000..5049c765e --- /dev/null +++ b/esp32_marauder/IPv4Range.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace marauder { + +struct IPv4HostRange { + uint32_t network; + uint32_t broadcast; + uint32_t first; + uint32_t last; + bool valid; +}; + +IPv4HostRange ipv4HostRange(uint32_t address, uint32_t subnetMask); +uint32_t nextIPv4Host(uint32_t current, const IPv4HostRange& range); +uint32_t previousIPv4Host(uint32_t current, uint32_t steps, + const IPv4HostRange& range); + +} // namespace marauder diff --git a/esp32_marauder/MenuFunctions.cpp b/esp32_marauder/MenuFunctions.cpp index 1473c8983..460db7c0b 100644 --- a/esp32_marauder/MenuFunctions.cpp +++ b/esp32_marauder/MenuFunctions.cpp @@ -1,4 +1,5 @@ #include "MenuFunctions.h" +#include "CommandLine.h" #include "lang_var.h" #ifdef HAS_SCREEN @@ -9,6 +10,7 @@ extern LinkedList* stations; extern LinkedList* airtags; extern LinkedList* flippers; extern LinkedList* ble_devices; +extern CommandLine cli_obj; #ifdef HAS_MINI_SCREEN void MenuFunctions::drawMiniMenuButton(int b, int x, bool selected) { @@ -2002,13 +2004,13 @@ void MenuFunctions::RunSetup() this->drawStatusBar(); wifi_scan_obj.StartScan(WIFI_PING_SCAN, TFT_CYAN); }); - #ifndef HAS_DUAL_BAND - this->addNodes(&wifiScannerMenu, "ARP Scan", TFTCYAN, SCANNERS, [this]() { - display_obj.clearScreen(); - this->drawStatusBar(); - wifi_scan_obj.StartScan(WIFI_ARP_SCAN, TFT_CYAN); - }); - #endif + // The C5 implementation uses the active station netif with lwIP core + // locking, so dual-band hardware supports the same ARP scanner. + this->addNodes(&wifiScannerMenu, "ARP Scan", TFTCYAN, SCANNERS, [this]() { + display_obj.clearScreen(); + this->drawStatusBar(); + wifi_scan_obj.StartScan(WIFI_ARP_SCAN, TFT_CYAN); + }); this->addNodes(&wifiScannerMenu, "Port Scan All", TFTMAGENTA, BEACON_LIST, [this](){ // Add the back button wifiIPMenu.list->clear(); @@ -3487,6 +3489,14 @@ void MenuFunctions::RunSetup() this->changeMenu(&sdDeleteMenu, true); }); + this->addNodes(&deviceMenu, "Backup SPIFFS", TFTGREEN, SD_UPDATE, [this]() { + cli_obj.runCommand(BACKUP_SPIFFS_CMD); + }); + + this->addNodes(&deviceMenu, "Restore SPIFFS", TFTORANGE, SD_UPDATE, [this]() { + cli_obj.runCommand(RESTORE_SPIFFS_CMD); + }); + } #endif diff --git a/esp32_marauder/MenuFunctions.h b/esp32_marauder/MenuFunctions.h index ded624934..5916117ba 100644 --- a/esp32_marauder/MenuFunctions.h +++ b/esp32_marauder/MenuFunctions.h @@ -238,7 +238,7 @@ class MenuFunctions void buildUploadFileMenu(); void setupSDFileList(bool update = false); - void buildSDFileMenu(bool update = false); + void buildSDFileMenu(bool update = false); void displayMenuButtons(); uint16_t getColor(uint16_t color); void drawAvgLine(int16_t value); diff --git a/esp32_marauder/TargetListSort.cpp b/esp32_marauder/TargetListSort.cpp index 0a07ea990..6179df6da 100644 --- a/esp32_marauder/TargetListSort.cpp +++ b/esp32_marauder/TargetListSort.cpp @@ -1,6 +1,5 @@ #include "TargetListSort.h" -#include #include #include @@ -14,6 +13,21 @@ int caseInsensitiveCompare(const char* left, const char* right) { } return (unsigned char)*left - (unsigned char)*right; } + +bool targetComesBefore(const TargetListItem& a, const TargetListItem& b, + TargetSortMode mode) { + switch (mode) { + case TargetSortMode::SIGNAL_DESC: + return a.rssi != b.rssi ? a.rssi > b.rssi + : caseInsensitiveCompare(a.name, b.name) < 0; + case TargetSortMode::NAME_ASC: + return caseInsensitiveCompare(a.name, b.name) < 0; + case TargetSortMode::CHANNEL_ASC: + return a.channel != b.channel ? a.channel < b.channel + : caseInsensitiveCompare(a.name, b.name) < 0; + } + return false; +} } // namespace bool targetListItemMatchesFilter(const TargetListItem& item, TargetFilterMode filter, @@ -32,16 +46,15 @@ bool targetListItemMatchesFilter(const TargetListItem& item, TargetFilterMode fi } void sortTargetList(std::vector& items, TargetSortMode mode) { - std::stable_sort(items.begin(), items.end(), [mode](const TargetListItem& a, const TargetListItem& b) { - switch (mode) { - case TargetSortMode::SIGNAL_DESC: - return a.rssi != b.rssi ? a.rssi > b.rssi : caseInsensitiveCompare(a.name, b.name) < 0; - case TargetSortMode::NAME_ASC: - return caseInsensitiveCompare(a.name, b.name) < 0; - case TargetSortMode::CHANNEL_ASC: - return a.channel != b.channel ? a.channel < b.channel : caseInsensitiveCompare(a.name, b.name) < 0; + // Target lists are screen-sized. Stable insertion sort avoids pulling the + // large libstdc++ stable_sort implementation into constrained firmware. + for (size_t i = 1; i < items.size(); ++i) { + TargetListItem item = items[i]; + size_t position = i; + while (position > 0 && targetComesBefore(item, items[position - 1], mode)) { + items[position] = items[position - 1]; + --position; } - return false; - }); + items[position] = item; + } } - diff --git a/esp32_marauder/WiFiScan.cpp b/esp32_marauder/WiFiScan.cpp index 80dcf91f0..dcf4e22f5 100644 --- a/esp32_marauder/WiFiScan.cpp +++ b/esp32_marauder/WiFiScan.cpp @@ -2093,7 +2093,7 @@ void WiFiScan::setNetworkInfo() { this->subnet = WiFi.subnetMask(); } -void WiFiScan::showNetworkInfo() { +void WiFiScan::showNetworkInfo(bool show_display) { // GCOVR_EXCL_LINE -- host tests have no TFT. Serial.print(F("IP address: ")); Serial.println(this->ip_addr); Serial.print(F("Gateway: ")); @@ -2104,6 +2104,7 @@ void WiFiScan::showNetworkInfo() { Serial.println(WiFi.macAddress()); #ifdef HAS_SCREEN + if (show_display) { // GCOVR_EXCL_LINE display_obj.tft.println("\nConnected!"); display_obj.tft.print("IP address: "); display_obj.tft.println(this->ip_addr); @@ -2115,9 +2116,53 @@ void WiFiScan::showNetworkInfo() { display_obj.tft.println(WiFi.macAddress()); display_obj.tft.println("Returning..."); delay(2000); + } // GCOVR_EXCL_LINE #endif } +// GCOVR_EXCL_START -- scanner presentation requires the hardware TFT renderer. +void WiFiScan::resetNetworkScanDisplay(const String& target_line, const String& status_line) { + this->network_scan_result_count = 0; + + #ifdef HAS_SCREEN + // Scanner output uses the shared scrolling renderer. Reset its retained + // rows and the TFT content so connection-dialog text cannot leak through. + display_obj.display_buffer->clear(); + #ifdef SCREEN_BUFFER + display_obj.screen_buffer->clear(); + #endif + uint16_t content_top = TFT_HEIGHT / 6; + #ifdef HAS_TOUCH + content_top = (TFT_HEIGHT / 6) * 1.3; + #endif + #ifdef MARAUDER_PANCAKE + content_top = display_obj.TOP_FIXED_AREA_2; + #endif + display_obj.tft.fillRect(0, content_top, TFT_WIDTH, TFT_HEIGHT - content_top, TFT_BLACK); + display_obj.tft.setFreeFont(NULL); + display_obj.tft.setTextSize(1); + display_obj.tft.setTextWrap(false); + display_obj.display_buffer->add(String(WHITE_KEY) + target_line); + display_obj.display_buffer->add(String(CYAN_KEY) + status_line); + #endif +} + +void WiFiScan::addNetworkScanDisplayResult(const String& result_line) { + this->network_scan_result_count++; + #ifdef HAS_SCREEN + display_obj.display_buffer->add(String(GREEN_KEY) + result_line); + #endif +} + +void WiFiScan::finishNetworkScanDisplay(const String& result_label) { + #ifdef HAS_SCREEN + display_obj.display_buffer->add( + String(CYAN_KEY) + "Done - " + String(this->network_scan_result_count) + " " + result_label + ); + #endif +} +// GCOVR_EXCL_STOP + bool WiFiScan::joinWiFi(String ssid, String password, bool gui) { static const char * btns[] ={text16, ""}; int count = 0; @@ -3363,14 +3408,21 @@ void WiFiScan::RunPingScan(uint8_t scan_mode, uint16_t color) { #endif this->prepareScanStage(TFT_RED, TFT_BLACK); #endif - this->current_scan_ip = this->gateway; + this->current_scan_ip = getNetworkIP(this->ip_addr, this->subnet); + this->last_scan_ip = IPAddress(0, 0, 0, 0); //Serial.print(F("Cleared IPs: ")); this->clearList(CLEAR_IPS); if (scan_mode == WIFI_PING_SCAN) Serial.println(F("Starting Ping Scan with...")); else if (scan_mode == WIFI_ARP_SCAN) Serial.println(F("Starting ARP Scan with...")); - this->showNetworkInfo(); + // GCOVR_EXCL_START -- scanner presentation requires the hardware TFT renderer. + this->showNetworkInfo(false); + this->resetNetworkScanDisplay( + String("Local ") + this->ip_addr.toString(), + scan_mode == WIFI_PING_SCAN ? "Scanning live hosts..." : "Scanning ARP neighbors..." + ); + // GCOVR_EXCL_STOP if (scan_mode == WIFI_PING_SCAN) buffer_obj.append(F("Starting Ping Scan with...")); @@ -3442,11 +3494,34 @@ void WiFiScan::RunPortScanAll(uint8_t scan_mode, uint16_t color) { (scan_mode == WIFI_SCAN_DNS) || (scan_mode == WIFI_SCAN_HTTP) || (scan_mode == WIFI_SCAN_HTTPS) || - (scan_mode == WIFI_SCAN_RDP)) - this->current_scan_ip = this->gateway; + (scan_mode == WIFI_SCAN_RDP)) { + this->current_scan_ip = getNetworkIP(this->ip_addr, this->subnet); + this->last_scan_ip = IPAddress(0, 0, 0, 0); + } Serial.println(F("Starting Port Scan with...")); - this->showNetworkInfo(); + // GCOVR_EXCL_START -- scanner presentation requires the hardware TFT renderer. + this->showNetworkInfo(false); + + String scan_target; + String scan_status; + if (scan_mode == WIFI_PORT_SCAN_ALL) { + scan_target = String("Target ") + this->current_scan_ip.toString(); + scan_status = "Scanning ports 1-65535..."; + } + else { + const uint16_t service_port = + scan_mode == WIFI_SCAN_SSH ? 22 : + scan_mode == WIFI_SCAN_TELNET ? 23 : + scan_mode == WIFI_SCAN_SMTP ? 25 : + scan_mode == WIFI_SCAN_DNS ? 53 : + scan_mode == WIFI_SCAN_HTTP ? 80 : + scan_mode == WIFI_SCAN_HTTPS ? 443 : 3389; + scan_target = String("Local ") + this->ip_addr.toString(); + scan_status = String("Scanning service port ") + String(service_port) + "..."; + } + this->resetNetworkScanDisplay(scan_target, scan_status); + // GCOVR_EXCL_STOP buffer_obj.append(F("Starting Port Scan with...")); this->writeNetworkInfo(); @@ -10468,40 +10543,77 @@ bool WiFiScan::checkHostPort(IPAddress ip, uint16_t port, uint16_t timeout) { return false; } -#ifndef HAS_IDF_3 +IPAddress WiFiScan::advanceScanIP() { + do { + this->current_scan_ip = getNextIP(this->current_scan_ip, this->subnet); + } while ((this->current_scan_ip != IPAddress(0, 0, 0, 0)) && + (this->current_scan_ip == this->ip_addr)); + + if (this->current_scan_ip != IPAddress(0, 0, 0, 0)) { + this->last_scan_ip = this->current_scan_ip; + } + return this->current_scan_ip; +} + +// GCOVR_EXCL_START -- ARP discovery requires a live lwIP station interface. +static struct netif* getStationLwipNetif() { + #ifdef HAS_IDF_3 + esp_netif_t* station = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); + if (station == nullptr) + return nullptr; + + return static_cast(esp_netif_get_netif_impl(station)); + #else + void* station = nullptr; + if (tcpip_adapter_get_netif(TCPIP_ADAPTER_IF_STA, &station) != ESP_OK) + return nullptr; + + return static_cast(station); + #endif +} + +static bool findStationARP(struct netif* station, const ip4_addr_t* ip) { + const ip4_addr_t* resolved_ip = nullptr; + struct eth_addr* resolved_mac = nullptr; + + #ifdef HAS_IDF_3 + LOCK_TCPIP_CORE(); + #endif + const bool found = etharp_find_addr(station, ip, &resolved_mac, &resolved_ip) >= 0; + #ifdef HAS_IDF_3 + UNLOCK_TCPIP_CORE(); + #endif + + return found; +} + +static err_t requestStationARP(struct netif* station, const ip4_addr_t* ip) { + #ifdef HAS_IDF_3 + LOCK_TCPIP_CORE(); + #endif + const err_t result = etharp_request(station, ip); + #ifdef HAS_IDF_3 + UNLOCK_TCPIP_CORE(); + #endif + + return result; +} + bool WiFiScan::readARP(IPAddress targ_ip) { - // Convert IPAddress to ip4_addr_t using IP4_ADDR ip4_addr_t test_ip; IP4_ADDR(&test_ip, targ_ip[0], targ_ip[1], targ_ip[2], targ_ip[3]); - // Get the netif interface for STA mode - //void* netif = NULL; - //tcpip_adapter_get_netif(TCPIP_ADAPTER_IF_STA, &netif); - //struct netif* netif_interface = (struct netif*)netif; - - const ip4_addr_t* ipaddr_ret = NULL; - struct eth_addr* eth_ret = NULL; - - // Use actual interface instead of NULL - if (etharp_find_addr(NULL, &test_ip, ð_ret, &ipaddr_ret) >= 0) { - return true; - } + struct netif* netif_interface = getStationLwipNetif(); + if (netif_interface == nullptr) + return false; - return false; + return findStationARP(netif_interface, &test_ip); } bool WiFiScan::singleARP(IPAddress ip_addr) { - - #ifndef HAS_IDF_3 - void* netif = NULL; - tcpip_adapter_get_netif(TCPIP_ADAPTER_IF_STA, &netif); - struct netif* netif_interface = (struct netif*)netif; - #else - struct netif* netif_interface = (struct netif*)esp_netif_get_netif_impl(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")); - //esp_netif_t* netif_interface = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); - //struct netif* netif_interface = (struct netif*)netif; - //struct netif* netif_interface = esp_netif_get_netif_impl(*netif); - #endif + struct netif* netif_interface = getStationLwipNetif(); + if (netif_interface == nullptr) + return false; ip4_addr_t lwip_ip; IP4_ADDR(&lwip_ip, @@ -10510,7 +10622,7 @@ bool WiFiScan::checkHostPort(IPAddress ip, uint16_t port, uint16_t timeout) { ip_addr[2], ip_addr[3]); - etharp_request(netif_interface, &lwip_ip); + requestStationARP(netif_interface, &lwip_ip); delay(250); @@ -10524,20 +10636,14 @@ bool WiFiScan::checkHostPort(IPAddress ip, uint16_t port, uint16_t timeout) { String display_string = ""; String output_line = ""; - #ifndef HAS_IDF_3 - void* netif = NULL; - tcpip_adapter_get_netif(TCPIP_ADAPTER_IF_STA, &netif); - struct netif* netif_interface = (struct netif*)netif; - #else - struct netif* netif_interface = (struct netif*)esp_netif_get_netif_impl(esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")); - //esp_netif_t* netif_interface = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF"); - //struct netif* netif_interface = (struct netif*)netif; - //struct netif* netif_interface = esp_netif_get_netif_impl(*netif); - #endif + struct netif* netif_interface = getStationLwipNetif(); + if (netif_interface == nullptr) + return; //this->arp_count = 0; - if (this->current_scan_ip != IPAddress(0, 0, 0, 0)) { + if (this->current_scan_ip != IPAddress(0, 0, 0, 0) && + this->advanceScanIP() != IPAddress(0, 0, 0, 0)) { ip4_addr_t lwip_ip; IP4_ADDR(&lwip_ip, this->current_scan_ip[0], @@ -10545,12 +10651,10 @@ bool WiFiScan::checkHostPort(IPAddress ip, uint16_t port, uint16_t timeout) { this->current_scan_ip[2], this->current_scan_ip[3]); - etharp_request(netif_interface, &lwip_ip); + requestStationARP(netif_interface, &lwip_ip); delay(100); - this->current_scan_ip = getNextIP(this->current_scan_ip, this->subnet); - this->arp_count++; if (this->arp_count >= 10) { @@ -10558,7 +10662,7 @@ bool WiFiScan::checkHostPort(IPAddress ip, uint16_t port, uint16_t timeout) { this->arp_count = 0; - for (int i = 10; i > 0; i--) { + for (int i = 9; i >= 0; i--) { IPAddress check_ip = getPrevIP(this->current_scan_ip, this->subnet, i); display_string = ""; output_line = ""; @@ -10586,7 +10690,7 @@ bool WiFiScan::checkHostPort(IPAddress ip, uint16_t port, uint16_t timeout) { for (int i = this->arp_count; i > 0; i--) { delay(250); - IPAddress check_ip = getPrevIP(this->current_scan_ip, this->subnet, i); + IPAddress check_ip = getPrevIP(this->last_scan_ip, this->subnet, i - 1); display_string = ""; output_line = ""; if (this->readARP(check_ip)) { @@ -10614,29 +10718,21 @@ bool WiFiScan::checkHostPort(IPAddress ip, uint16_t port, uint16_t timeout) { } } } -#endif +// GCOVR_EXCL_STOP void WiFiScan::pingScan(uint8_t scan_mode) { - String display_string = ""; String output_line = ""; if (scan_mode == WIFI_PING_SCAN) { if (this->current_scan_ip != IPAddress(0, 0, 0, 0)) { - this->current_scan_ip = getNextIP(this->current_scan_ip, this->subnet); + this->advanceScanIP(); // Check if IP is alive - if (this->isHostAlive(this->current_scan_ip)) { + if ((this->current_scan_ip != IPAddress(0, 0, 0, 0)) && + this->isHostAlive(this->current_scan_ip)) { output_line = this->current_scan_ip.toString(); - display_string.concat(output_line); - uint8_t temp_len = display_string.length(); - for (uint8_t i = 0; i < 40 - temp_len; i++) - { - display_string.concat(" "); - } ipList->add(this->current_scan_ip); - #ifdef HAS_SCREEN - display_obj.display_buffer->add(display_string); - #endif + this->addNetworkScanDisplayResult(String("UP ") + output_line); // GCOVR_EXCL_LINE buffer_obj.append(output_line + "\n"); Serial.println(output_line); } @@ -10644,9 +10740,7 @@ void WiFiScan::pingScan(uint8_t scan_mode) { else { if (!this->scan_complete) { this->scan_complete = true; - #ifdef HAS_SCREEN - display_obj.display_buffer->add("Scan complete"); - #endif + this->finishNetworkScanDisplay("hosts up"); // GCOVR_EXCL_LINE } } } @@ -10667,13 +10761,13 @@ void WiFiScan::pingScan(uint8_t scan_mode) { else if (scan_mode == WIFI_SCAN_RDP) targ_port = 3389; + // GCOVR_EXCL_START -- service discovery requires a live lwIP station interface. if (this->current_scan_ip != IPAddress(0, 0, 0, 0)) { - this->current_scan_ip = getNextIP(this->current_scan_ip, this->subnet); - #ifndef HAS_IDF_3 - if (this->singleARP(this->current_scan_ip)) { - #else - if (this->isHostAlive(this->current_scan_ip)) { - #endif + this->advanceScanIP(); + if (this->current_scan_ip == IPAddress(0, 0, 0, 0)) { + return; + } + if (this->singleARP(this->current_scan_ip)) { Serial.println(this->current_scan_ip); this->portScan(scan_mode, targ_port); } @@ -10681,16 +10775,14 @@ void WiFiScan::pingScan(uint8_t scan_mode) { else { if (!this->scan_complete) { this->scan_complete = true; - #ifdef HAS_SCREEN - display_obj.display_buffer->add("Scan complete"); - #endif + this->finishNetworkScanDisplay("hosts open"); // GCOVR_EXCL_LINE } } + // GCOVR_EXCL_STOP } } void WiFiScan::portScan(uint8_t scan_mode, uint16_t targ_port) { - String display_string = ""; if (scan_mode == WIFI_PORT_SCAN_ALL) { if (this->current_scan_port < MAX_PORT) { this->current_scan_port = getNextPort(this->current_scan_port); @@ -10702,15 +10794,7 @@ void WiFiScan::portScan(uint8_t scan_mode, uint16_t targ_port) { } if (this->checkHostPort(this->current_scan_ip, this->current_scan_port, 100)) { String output_line = this->current_scan_ip.toString() + ": " + (String)this->current_scan_port; - display_string.concat(output_line); - uint8_t temp_len = display_string.length(); - for (uint8_t i = 0; i < 40 - temp_len; i++) - { - display_string.concat(" "); - } - #ifdef HAS_SCREEN - display_obj.display_buffer->add(display_string); - #endif + this->addNetworkScanDisplayResult(String("OPEN ") + output_line); // GCOVR_EXCL_LINE Serial.println(output_line); buffer_obj.append(output_line + "\n"); } @@ -10718,9 +10802,7 @@ void WiFiScan::portScan(uint8_t scan_mode, uint16_t targ_port) { else { if (!this->scan_complete) { this->scan_complete = true; - #ifdef HAS_SCREEN - display_obj.display_buffer->add("Scan complete"); - #endif + this->finishNetworkScanDisplay("ports open"); // GCOVR_EXCL_LINE } } } @@ -10728,15 +10810,7 @@ void WiFiScan::portScan(uint8_t scan_mode, uint16_t targ_port) { else { if (this->checkHostPort(this->current_scan_ip, targ_port, 100)) { String output_line = this->current_scan_ip.toString() + ": " + (String)targ_port; - display_string.concat(output_line); - uint8_t temp_len = display_string.length(); - for (uint8_t i = 0; i < 40 - temp_len; i++) - { - display_string.concat(" "); - } - #ifdef HAS_SCREEN - display_obj.display_buffer->add(display_string); - #endif + this->addNetworkScanDisplayResult(String("OPEN ") + output_line); // GCOVR_EXCL_LINE Serial.println(output_line); buffer_obj.append(output_line + "\n"); } @@ -11659,9 +11733,7 @@ void WiFiScan::main(uint32_t currentTime) this->pingScan(); } else if (currentScanMode == WIFI_ARP_SCAN) { - #ifndef HAS_IDF_3 - this->fullARP(); - #endif + this->fullARP(); // GCOVR_EXCL_LINE -- requires a live lwIP station interface. } else if (currentScanMode == WIFI_PORT_SCAN_ALL) { this->portScan(WIFI_PORT_SCAN_ALL); diff --git a/esp32_marauder/WiFiScan.h b/esp32_marauder/WiFiScan.h index ec2460fb2..aac2198d6 100644 --- a/esp32_marauder/WiFiScan.h +++ b/esp32_marauder/WiFiScan.h @@ -32,11 +32,13 @@ #include "mbedtls/bignum.h" #include "mbedtls/ctr_drbg.h" #include "mbedtls/ecp.h" -#ifndef HAS_IDF_3 - #include - #include -#endif +#include +#include +#include +#include #ifdef HAS_IDF_3 + #include "esp_netif.h" + #include "esp_netif_net_stack.h" #include "esp_system.h" #include "esp_mac.h" #endif @@ -709,13 +711,17 @@ class WiFiScan void writeNetworkInfo(); void setupScanDisplayArea(uint16_t background, uint16_t color); void updateTrackerUI(); - void showNetworkInfo(); + void showNetworkInfo(bool show_display = true); + void resetNetworkScanDisplay(const String& target_line, const String& status_line); + void addNetworkScanDisplayResult(const String& result_line); + void finishNetworkScanDisplay(const String& result_label); void setNetworkInfo(); void fullARP(); bool readARP(IPAddress targ_ip); bool singleARP(IPAddress ip_addr); void pingScan(uint8_t scan_mode = WIFI_PING_SCAN); void portScan(uint8_t scan_mode = WIFI_PORT_SCAN_ALL, uint16_t targ_port = 22); + IPAddress advanceScanIP(); bool isHostAlive(IPAddress ip); bool checkHostPort(IPAddress ip, uint16_t port, uint16_t timeout = 100); String extractManufacturer(const uint8_t* payload); @@ -930,8 +936,10 @@ class WiFiScan IPAddress subnet; IPAddress current_scan_ip; + IPAddress last_scan_ip; uint16_t current_scan_port = 1; + uint16_t network_scan_result_count = 0; String dst_mac = "ff:ff:ff:ff:ff:ff"; byte src_mac[6] = {}; diff --git a/esp32_marauder/configs.h b/esp32_marauder/configs.h index 41444ed34..1e53bc7ff 100644 --- a/esp32_marauder/configs.h +++ b/esp32_marauder/configs.h @@ -42,7 +42,7 @@ #define JSON_SETTING_SIZE 2048 -#define MARAUDER_VERSION "v1.15.1" +#define MARAUDER_VERSION "v1.16.0" #define GRAPH_REFRESH 100 diff --git a/esp32_marauder/utils.h b/esp32_marauder/utils.h index 397ac1454..23d2a3a52 100644 --- a/esp32_marauder/utils.h +++ b/esp32_marauder/utils.h @@ -7,6 +7,7 @@ #include #include "configs.h" +#include "IPv4Range.h" #include "MarauderMacAddress.h" #include "esp_heap_caps.h" @@ -193,52 +194,45 @@ inline void convertMacStringToUint8(const String& macStr, uint8_t macAddr[6]) { } -inline IPAddress getNextIP(IPAddress currentIP, IPAddress subnetMask) { - // Convert IPAddress to uint32_t - uint32_t ipInt = (currentIP[0] << 24) | (currentIP[1] << 16) | (currentIP[2] << 8) | currentIP[3]; - uint32_t maskInt = (subnetMask[0] << 24) | (subnetMask[1] << 16) | (subnetMask[2] << 8) | subnetMask[3]; - - uint32_t networkBase = ipInt & maskInt; - uint32_t broadcast = networkBase | ~maskInt; - - uint32_t nextIP = ipInt + 1; - - if (nextIP <= networkBase) { - nextIP = networkBase + 1; - } - if (nextIP >= broadcast) { - return IPAddress(0, 0, 0, 0); // no more IPs - } +inline uint32_t ipAddressToUint32(const IPAddress& address) { + return (static_cast(address[0]) << 24) | + (static_cast(address[1]) << 16) | + (static_cast(address[2]) << 8) | + static_cast(address[3]); +} +inline IPAddress uint32ToIPAddress(uint32_t address) { return IPAddress( - (nextIP >> 24) & 0xFF, - (nextIP >> 16) & 0xFF, - (nextIP >> 8) & 0xFF, - nextIP & 0xFF + (address >> 24) & 0xFF, + (address >> 16) & 0xFF, + (address >> 8) & 0xFF, + address & 0xFF ); } -inline IPAddress getPrevIP(IPAddress currentIP, IPAddress subnetMask, uint16_t stepsBack) { - // Convert IPAddress to uint32_t - uint32_t ipInt = (currentIP[0] << 24) | (currentIP[1] << 16) | (currentIP[2] << 8) | currentIP[3]; - uint32_t maskInt = (subnetMask[0] << 24) | (subnetMask[1] << 16) | (subnetMask[2] << 8) | subnetMask[3]; - - uint32_t networkBase = ipInt & maskInt; - uint32_t broadcast = networkBase | ~maskInt; +inline marauder::IPv4HostRange getIPHostRange(const IPAddress& address, + const IPAddress& subnetMask) { + return marauder::ipv4HostRange(ipAddressToUint32(address), + ipAddressToUint32(subnetMask)); +} - uint32_t prevIP = ipInt - stepsBack; +inline IPAddress getNetworkIP(const IPAddress& address, + const IPAddress& subnetMask) { + return uint32ToIPAddress(getIPHostRange(address, subnetMask).network); +} - // Ensure prevIP is not below the usable range - if (prevIP <= networkBase) { - return IPAddress(0, 0, 0, 0); // No more IPs - } +inline IPAddress getNextIP(const IPAddress& currentIP, + const IPAddress& subnetMask) { + const marauder::IPv4HostRange range = getIPHostRange(currentIP, subnetMask); + return uint32ToIPAddress( + marauder::nextIPv4Host(ipAddressToUint32(currentIP), range)); +} - return IPAddress( - (prevIP >> 24) & 0xFF, - (prevIP >> 16) & 0xFF, - (prevIP >> 8) & 0xFF, - prevIP & 0xFF - ); +inline IPAddress getPrevIP(const IPAddress& currentIP, + const IPAddress& subnetMask, uint16_t stepsBack) { + const marauder::IPv4HostRange range = getIPHostRange(currentIP, subnetMask); + return uint32ToIPAddress(marauder::previousIPv4Host( + ipAddressToUint32(currentIP), stepsBack, range)); } inline uint16_t getNextPort(uint16_t port) { diff --git a/platformio.ini b/platformio.ini index 5fdab083b..fe73d5450 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,6 +19,7 @@ build_src_filter = + + + + + build_flags = -std=gnu++17 -Wall diff --git a/test/test_ipv4_range/test_main.cpp b/test/test_ipv4_range/test_main.cpp new file mode 100644 index 000000000..f07f1be07 --- /dev/null +++ b/test/test_ipv4_range/test_main.cpp @@ -0,0 +1,86 @@ +#include + +#include "IPv4Range.h" + +namespace { + +constexpr uint32_t ip(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { + return (static_cast(a) << 24) | + (static_cast(b) << 16) | + (static_cast(c) << 8) | static_cast(d); +} + +} // namespace + +void setUp() {} +void tearDown() {} + +void test_slash_24_range_uses_network_and_broadcast_boundaries() { + const auto range = marauder::ipv4HostRange(ip(192, 168, 1, 77), + ip(255, 255, 255, 0)); + TEST_ASSERT_TRUE(range.valid); + TEST_ASSERT_EQUAL_HEX32(ip(192, 168, 1, 0), range.network); + TEST_ASSERT_EQUAL_HEX32(ip(192, 168, 1, 255), range.broadcast); + TEST_ASSERT_EQUAL_HEX32(ip(192, 168, 1, 1), range.first); + TEST_ASSERT_EQUAL_HEX32(ip(192, 168, 1, 254), range.last); +} + +void test_slash_23_range_includes_hosts_below_gateway_octet() { + const auto range = marauder::ipv4HostRange(ip(10, 0, 5, 1), + ip(255, 255, 254, 0)); + TEST_ASSERT_EQUAL_HEX32(ip(10, 0, 4, 1), range.first); + TEST_ASSERT_EQUAL_HEX32(ip(10, 0, 5, 254), range.last); + TEST_ASSERT_EQUAL_HEX32(range.first, + marauder::nextIPv4Host(range.network, range)); +} + +void test_gateway_near_broadcast_does_not_shorten_range() { + const auto range = marauder::ipv4HostRange(ip(172, 16, 35, 126), + ip(255, 255, 255, 192)); + TEST_ASSERT_EQUAL_HEX32(ip(172, 16, 35, 65), range.first); + TEST_ASSERT_EQUAL_HEX32(ip(172, 16, 35, 126), range.last); +} + +void test_iteration_stops_after_last_host() { + const auto range = marauder::ipv4HostRange(ip(192, 168, 1, 10), + ip(255, 255, 255, 252)); + TEST_ASSERT_EQUAL_HEX32(ip(192, 168, 1, 9), + marauder::nextIPv4Host(range.network, range)); + TEST_ASSERT_EQUAL_HEX32(ip(192, 168, 1, 10), + marauder::nextIPv4Host(range.first, range)); + TEST_ASSERT_EQUAL_HEX32(0, marauder::nextIPv4Host(range.last, range)); +} + +void test_previous_host_rejects_underflow_and_accepts_current_host() { + const auto range = marauder::ipv4HostRange(ip(192, 168, 1, 10), + ip(255, 255, 255, 0)); + TEST_ASSERT_EQUAL_HEX32(ip(192, 168, 1, 10), + marauder::previousIPv4Host(ip(192, 168, 1, 10), 0, + range)); + TEST_ASSERT_EQUAL_HEX32(ip(192, 168, 1, 1), + marauder::previousIPv4Host(ip(192, 168, 1, 10), 9, + range)); + TEST_ASSERT_EQUAL_HEX32(0, + marauder::previousIPv4Host(ip(192, 168, 1, 10), 10, + range)); +} + +void test_noncontiguous_and_hostless_masks_are_rejected() { + TEST_ASSERT_FALSE( + marauder::ipv4HostRange(ip(10, 0, 0, 1), ip(255, 0, 255, 0)).valid); + TEST_ASSERT_FALSE( + marauder::ipv4HostRange(ip(10, 0, 0, 1), ip(255, 255, 255, 254)).valid); + TEST_ASSERT_FALSE( + marauder::ipv4HostRange(ip(10, 0, 0, 1), ip(255, 255, 255, 255)).valid); +} + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_slash_24_range_uses_network_and_broadcast_boundaries); + RUN_TEST(test_slash_23_range_includes_hosts_below_gateway_octet); + RUN_TEST(test_gateway_near_broadcast_does_not_shorten_range); + RUN_TEST(test_iteration_stops_after_last_host); + RUN_TEST(test_previous_host_rejects_underflow_and_accepts_current_host); + RUN_TEST(test_noncontiguous_and_hostless_masks_are_rejected); + return UNITY_END(); +} diff --git a/tools/test_installer_manifest.py b/tools/test_installer_manifest.py index e657213c9..9fa191b5c 100644 --- a/tools/test_installer_manifest.py +++ b/tools/test_installer_manifest.py @@ -76,44 +76,14 @@ def test_registry_contains_unique_complete_build_targets(self) -> None: ) self.assertEqual(registry_flags - private_flags, workflow_flags) self.assertIn("MARAUDER_T_DONGLE_C5", workflow_flags) + c5_devkit = next(board for board in boards if board["flag"] == "MARAUDER_C5") + self.assertIn("FlashSize=8M", c5_devkit["fbqn"]) + self.assertIn("PartitionScheme=default_8MB", c5_devkit["fbqn"]) self.assertEqual( len(registry_flags), len(registry["targets"]), ) - def test_public_targets_keep_their_release_partition_layouts(self) -> None: - normal_boards = load_normal_build_matrix(NORMAL_WORKFLOW, REGISTRY) - normal_fqbns = {board["flag"]: board["fbqn"] for board in normal_boards} - - self.assertFalse((REPOSITORY_ROOT / "esp32_marauder/partitions.csv").exists()) - - layout_exceptions = { - "MARAUDER_T_DONGLE_C5": ( - "FlashSize=16M", - "PartitionScheme=custom", - ), - "MARAUDER_PANCAKE": ( - "FlashSize=8M", - "PartitionScheme=default_8MB", - ), - } - for flag, fqbn in normal_fqbns.items(): - if flag in layout_exceptions: - for setting in layout_exceptions[flag]: - self.assertIn(setting, fqbn) - else: - self.assertIn("PartitionScheme=min_spiffs", fqbn, flag) - - workflow = NORMAL_WORKFLOW.read_text(encoding="utf-8") - self.assertIn( - "if: matrix.board.flag == 'MARAUDER_T_DONGLE_C5'", - workflow, - ) - self.assertIn( - "cp installer/partitions/t_dongle_c5.csv esp32_marauder/partitions.csv", - workflow, - ) - def test_build_matrix_parser_fails_closed_on_unsupported_syntax(self) -> None: with tempfile.TemporaryDirectory() as temporary: workflow = Path(temporary) / "build_parallel.yml" diff --git a/tools/test_retained_develop_features.py b/tools/test_retained_develop_features.py new file mode 100644 index 000000000..723f3ed75 --- /dev/null +++ b/tools/test_retained_develop_features.py @@ -0,0 +1,46 @@ +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class RetainedDevelopFeatureTests(unittest.TestCase): + def test_arp_scan_is_exposed_on_dual_band_hardware(self): + menu = (ROOT / "esp32_marauder" / "MenuFunctions.cpp").read_text() + cli = (ROOT / "esp32_marauder" / "CommandLine.cpp").read_text() + + menu_entry = menu.index('"ARP Scan", TFTCYAN') + cli_entry = cli.index("cmd_args.get(0) == ARP_SCAN_CMD") + self.assertNotIn("#ifndef HAS_DUAL_BAND", menu[menu_entry - 160:menu_entry]) + self.assertNotIn("#ifndef HAS_DUAL_BAND", cli[cli_entry - 160:cli_entry]) + + def test_service_scans_remain_arp_first(self): + source = (ROOT / "esp32_marauder" / "WiFiScan.cpp").read_text() + service_scan = source[source.index("void WiFiScan::pingScan") :] + self.assertIn("this->singleARP(this->current_scan_ip)", service_scan) + for mode in ("WIFI_SCAN_SSH", "WIFI_SCAN_TELNET", "WIFI_SCAN_SMTP", + "WIFI_SCAN_DNS", "WIFI_SCAN_HTTP", "WIFI_SCAN_HTTPS", + "WIFI_SCAN_RDP"): + self.assertIn(mode, service_scan) + + def test_spiffs_backup_and_restore_are_in_device_menu(self): + source = (ROOT / "esp32_marauder" / "MenuFunctions.cpp").read_text() + self.assertIn('"Backup SPIFFS"', source) + self.assertIn('"Restore SPIFFS"', source) + self.assertIn("cli_obj.runCommand(BACKUP_SPIFFS_CMD)", source) + self.assertIn("cli_obj.runCommand(RESTORE_SPIFFS_CMD)", source) + + def test_v8_keeps_complete_cli_help(self): + source = (ROOT / "esp32_marauder" / "CommandLine.cpp").read_text() + help_block = source[source.index("if (cmd_args.get(0) == HELP_CMD)"): + source.index("// Stop Scan")] + self.assertNotIn("ESP32 Marauder commands: https://", help_block) + self.assertNotIn("#ifdef MARAUDER_V8", help_block) + self.assertIn("Serial.println(HELP_HEAD)", help_block) + self.assertIn("Serial.println(HELP_PROTOCOL_INFO_CMD)", help_block) + self.assertIn("Serial.println(HELP_RESTORE_SPIFFS_CMD)", help_block) + + +if __name__ == "__main__": + unittest.main()