From cf5dfeb87b39b98bd33c920311a0796fc9d00d10 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 3 Nov 2026 00:37:33 -0800 Subject: [PATCH] Fix constrained firmware builds and CI checks --- .github/workflows/run-unit-tests.yml | 15 +- .../AsyncElegantOTA/src/AsyncElegantOTA.h | 9 +- docs/research/firmware_memory_budget.md | 6 +- docs/terminal_chat_cli.md | 4 +- examples/companion_radio/main.cpp | 11 + examples/companion_radio/ui-new/UITask.cpp | 9 +- scripts/check_nrf52_companion_crypto.py | 4 +- scripts/check_nrf52_companion_reader.py | 17 +- scripts/generate_webconfig_html.py | 11 +- src/helpers/CommonCLI.cpp | 12 + src/helpers/CompanionReaderConfig.h | 7 +- src/helpers/WirelessControl.h | 18 ++ src/helpers/esp32/StaticHtml.h | 267 ++++++++++++++++++ src/helpers/ota/OtaTinf.c | 2 +- test/fixtures/cli_reply_text.json | 6 +- test/test_compact_elegant_ota.py | 155 ++++++++++ test/test_companion_headless_buttons.py | 9 +- test/test_companion_no_wireless.py | 132 +++++++++ test/test_companion_reader.py | 20 +- test/test_lr2021_calibration.py | 9 +- test/test_pocket_reader.py | 2 +- test/test_stm32_wireless_specialization.py | 46 +++ test/test_webconfig_gzip.py | 7 +- tools/bible/README.md | 8 +- .../lilygo_tbeam_universal/platformio.ini | 2 + webui/pages/elegant_ota_compact.html | 53 ++++ webui/vendor/README.md | 7 + webui/vendor/spark-md5-3.0.2.LICENSE.txt | 13 + webui/vendor/spark-md5-3.0.2.MIT.txt | 21 ++ webui/vendor/spark-md5-3.0.2.min.js | 1 + 30 files changed, 839 insertions(+), 44 deletions(-) create mode 100644 test/test_compact_elegant_ota.py create mode 100644 test/test_companion_no_wireless.py create mode 100644 test/test_stm32_wireless_specialization.py create mode 100644 webui/pages/elegant_ota_compact.html create mode 100644 webui/vendor/README.md create mode 100644 webui/vendor/spark-md5-3.0.2.LICENSE.txt create mode 100644 webui/vendor/spark-md5-3.0.2.MIT.txt create mode 100644 webui/vendor/spark-md5-3.0.2.min.js diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index 4bb2aa969..aa8cf9b62 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -52,12 +52,12 @@ jobs: --firmware-version v1.18.0.1-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota - - name: Verify linked Companion crypto, retained Reader and ARM stack use + - name: Verify linked Companion crypto, Reader exclusion and ARM stack use run: | python3 -m pip install unicorn==2.1.4 python3 -B test/test_cc310_shared_scratch.py -v python3 -B scripts/check_nrf52_companion_crypto.py .pio/build/RAK_4631_companion_radio_full/firmware.elf - python3 -B scripts/check_nrf52_companion_reader.py .pio/build/RAK_4631_companion_radio_full/firmware.elf + python3 -B scripts/check_nrf52_companion_reader.py --expect-disabled .pio/build/RAK_4631_companion_radio_full/firmware.elf python3 -B scripts/check_nrf52_companion_ota_workspace.py .pio/build/RAK_4631_companion_radio_full/firmware.elf - name: Qualify XIAO Full Companion with Meshtext and direct OTA @@ -293,6 +293,14 @@ jobs: run: pio run -e muzi_base_repeater -e muzi_base_room_server -e muzi_base_companion_radio_usb -e muzi_base_companion_radio_ble - name: Qualify Muzi Full Companion run: bash build.sh build-firmware muzi_base_companion_radio_full --firmware-version v1.18.0.2-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade + - name: Upload universal build failure logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: universal-lora-build-logs + path: out/build-logs/*.log + if-no-files-found: ignore + retention-days: 7 headless-kiss-smoke: runs-on: ubuntu-latest @@ -1168,6 +1176,8 @@ jobs: - name: Verify WiFi, ESP-NOW and combined wireless controls run: | python3 -B test/test_wireless_control.py -v + python3 -B test/test_stm32_wireless_specialization.py -v + python3 -B test/test_companion_no_wireless.py -v python3 -B test/test_serial_wifi_sessions.py -v python3 -B test/test_companion_wifi_listener.py -v python3 -B test/test_wifi_compatibility.py -v @@ -1404,6 +1414,7 @@ jobs: python3 -B test/test_webconfig_ui.py python3 -B test/test_webconfig_regulators.py -v python3 -B test/test_webconfig_gzip.py + python3 -B test/test_compact_elegant_ota.py -v python3 -B test/test_webconfig_secret_masking.py python3 -B test/test_webconfig_station_ownership.py -v python3 -B test/test_mqtt_canonical_wifi.py -v diff --git a/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.h b/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.h index e1e06a211..60879d5ea 100644 --- a/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.h +++ b/arch/esp32/AsyncElegantOTA/src/AsyncElegantOTA.h @@ -21,7 +21,14 @@ #include "ESPAsyncWebServer.h" #include "FS.h" -#include "elegantWebpage.h" +#if defined(ESP32) && defined(MESH_COMPACT_ELEGANT_OTA) && MESH_COMPACT_ELEGANT_OTA + // Keep the full AsyncElegantOTA upload protocol; only its page is smaller. + #include + #define ELEGANT_HTML MESH_HTML_ELEGANT_OTA_COMPACT + #define ELEGANT_HTML_SIZE MESH_HTML_ELEGANT_OTA_COMPACT_LEN +#else + #include "elegantWebpage.h" +#endif class AsyncElegantOtaClass{ diff --git a/docs/research/firmware_memory_budget.md b/docs/research/firmware_memory_budget.md index bda0f12d2..1868ec7dd 100644 --- a/docs/research/firmware_memory_budget.md +++ b/docs/research/firmware_memory_budget.md @@ -477,8 +477,10 @@ these are crypto function frames only, excluding callers, interrupts and other tasks. The loop stack stays at 8 KiB. A full firmware RAM check still applies. CI runs `scripts/check_nrf52_companion_crypto.py` on the actual Full RAK ELF. -It checks the 960-byte table and retained Reader corpus, executes two RFC 8032 -vectors, and rejects crypto function stack use above 2 KiB. Host tests compare +It checks the 960-byte table, executes two RFC 8032 vectors, and rejects crypto +function stack use above 2 KiB. Reader is now disabled by default in Companion +builds; an explicit `COMPANION_FEATURE_READER=1` retains the implementation and +corpus. Host tests compare 1,791 scalars and 128 keypair/signature/shared-secret cases against the original implementation, plus scalar boundary/random and sanitizer tests. Clean Full builds also cover T1000-E and T096. These builds are synthetic coverage for diff --git a/docs/terminal_chat_cli.md b/docs/terminal_chat_cli.md index a32c93f47..cd722dc6f 100644 --- a/docs/terminal_chat_cli.md +++ b/docs/terminal_chat_cli.md @@ -256,7 +256,9 @@ for the state machine and limitations. ## Commands -ESP32 and nRF52 Full Companions include an offline World English Bible lookup: +Custom ESP32 and nRF52 Full Companion USB builds can opt into an offline World +English Bible lookup with `COMPANION_FEATURE_READER=1`. Standard builds leave +it disabled to preserve firmware space: ```text get reader 3:16 diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index fd67dc261..01a477969 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -3672,6 +3672,16 @@ static bool companionNativeUsbLightSleepReady() { bool handleCompanionWirelessCommand(const char* command, char* reply, size_t size, CompanionWirelessSource source) { +#if defined(STM32_PLATFORM) && !defined(BLE_PIN_CODE) && !defined(WIFI_SSID) \ + && !defined(WITH_BRIDGE) && !defined(WITH_MQTT_BRIDGE) \ + && !defined(WITH_UDP_BRIDGE) && !defined(WITH_ESPNOW_BRIDGE) \ + && !defined(WITH_WEBCONFIG) && !defined(ETHERNET_ENABLED) \ + && !MESH_ENABLE_EMERGENCY_POWER \ + && (!defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW) + // This build has no wireless transport whose state can change. + (void)source; + return mesh::wireless::handleNoServices(command, reply, size); +#else #if defined(BLE_PIN_CODE) const auto wireless_command = mesh::wireless::parse(command); if (reply && size && companion_bluetooth_off_at != 0 @@ -3700,6 +3710,7 @@ bool handleCompanionWirelessCommand(const char* command, char* reply, size_t siz } } return mesh::wireless::control().handle(command, reply, size, millis(), requester); +#endif } #if COMPANION_RTC_TERMINAL_INPUT diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d8ff27f35..2a33f38c4 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1739,12 +1739,6 @@ public: movePage(-1); return true; } -#if COMPANION_FEATURE_READER - if (key == KEY_ENTER && isRadioPage()) { - _task->showReader(); - return true; - } -#endif if (key == KEY_NEXT || key == KEY_RIGHT) { movePage(1); if (_page == HomePage::RECENT) { @@ -3157,7 +3151,8 @@ char UITask::handleLongPress(char c) { // The radio page's reader gesture, including exit, takes precedence over // the global early-boot rescue gesture just like the WiFi setup page. if (_display != NULL && isReaderActive()) { closeReader(); return 0; } - if (_display != NULL && curr == home && static_cast(home)->isRadioPage()) { + if (c == KEY_ENTER && _display != NULL && curr == home + && static_cast(home)->isRadioPage()) { showReader(); return 0; } diff --git a/scripts/check_nrf52_companion_crypto.py b/scripts/check_nrf52_companion_crypto.py index aa6fdff2c..8b53b3022 100644 --- a/scripts/check_nrf52_companion_crypto.py +++ b/scripts/check_nrf52_companion_crypto.py @@ -43,8 +43,6 @@ def check(path): if elf.symbols.get("base", (0, 0))[1] != 960: raise ValueError("Companion must retain the 960-byte Ed25519 base table") reader = "_ZN4mesh5bible9generatedL10readerDataE" - if reader not in elf.symbols: - raise ValueError("Companion Reader corpus is missing") path_cache = elf.symbols.get("_ZZN4mesh18contactPathStorageEvE4pool") if path_cache and elf.sections[path_cache[2]][1] != 8: # SHT_NOBITS raise ValueError("Contact path cache has a flash load image; expected zero-initialized storage") @@ -102,7 +100,7 @@ def check(path): return { "passed": True, "elf_sha256": hashlib.sha256(elf.data).hexdigest(), "base_table_bytes": elf.symbols["base"][1], - "reader_data_bytes": elf.symbols[reader][1], "vectors": rows, + "reader_data_bytes": elf.symbols.get(reader, (0, 0))[1], "vectors": rows, "zero_initialized_path_cache_bytes": path_cache[1] if path_cache else 0, "stack_limit_bytes": 2048, "scope": "Crypto function frames only; excludes caller/ISR/task stacks and physical timing.", diff --git a/scripts/check_nrf52_companion_reader.py b/scripts/check_nrf52_companion_reader.py index 857aca6b6..ee813e261 100644 --- a/scripts/check_nrf52_companion_reader.py +++ b/scripts/check_nrf52_companion_reader.py @@ -23,6 +23,19 @@ ROOT = Path(__file__).resolve().parents[1] TYPOGRAPHY = str.maketrans({'\u00a0':' ', '\u2013':'-', '\u2014':'-', '\u2018':"'", '\u2019':"'", '\u201c':'"', '\u201d':'"'}) +def check_disabled(path): + elf = FirmwareElf(path) + if elf.unpack('H', 18)[0] != 40: + raise ValueError('Expected ARM ELF') + retained = [name for name in elf.symbols + if 'readerData' in name or 'replyVerse' in name + or 'handleReaderCommand' in name] + if retained: + raise ValueError('Reader code or corpus unexpectedly linked: ' + repr(retained)) + return {'passed': True, 'reader': 'disabled', + 'elf_sha256': hashlib.sha256(elf.data).hexdigest()} + + def check(path, every_page=False): elf = FirmwareElf(path) if elf.unpack('H', 18)[0] != 40: @@ -132,8 +145,10 @@ if __name__ == '__main__': parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('elf') parser.add_argument('--every-page', action='store_true') + parser.add_argument('--expect-disabled', action='store_true') args = parser.parse_args() try: - print(json.dumps(check(args.elf, args.every_page), indent=2)) + print(json.dumps(check_disabled(args.elf) if args.expect_disabled + else check(args.elf, args.every_page), indent=2)) except (ValueError, OSError) as error: parser.exit(1, f'FAILED: {error}\n') diff --git a/scripts/generate_webconfig_html.py b/scripts/generate_webconfig_html.py index b70bec3fe..1d3f6375a 100644 --- a/scripts/generate_webconfig_html.py +++ b/scripts/generate_webconfig_html.py @@ -118,7 +118,16 @@ def main(): "#pragma once", "#include ", "#include "] for page in sorted(Path("webui/pages").glob("*.html")): symbol = "MESH_HTML_" + page.stem.upper() - compressed = compress_page(page.read_bytes()) + page_bytes = page.read_bytes() + if page.name == "elegant_ota_compact.html": + # Inline the pinned, locally vendored MD5 implementation: the OTA + # page must work without internet and the server verifies its hash. + marker = b"/* MESH_SPARK_MD5 */" + if page_bytes.count(marker) != 1: + raise ValueError("compact OTA page must contain one MD5 marker") + vendor = Path("webui/vendor/spark-md5-3.0.2.min.js").read_bytes() + page_bytes = page_bytes.replace(marker, vendor) + compressed = compress_page(page_bytes) static_lines.append("const uint32_t %s_LEN = %d;" % (symbol, len(compressed))) append_byte_array(static_lines, symbol, compressed) static_header = Path("src/helpers/esp32/StaticHtml.h") diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b29270f9a..5a214381e 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3750,8 +3750,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re #if MESH_ENABLE_TX_BEACONS if (handleBeaconCommand(command, reply, 157)) return; #endif +#if defined(STM32_PLATFORM) && defined(STM32WLxx) && defined(USE_STM32WL) \ + && !defined(WITH_WEBCONFIG) && !defined(WITH_MQTT_BRIDGE) \ + && !defined(WITH_UDP_BRIDGE) && !defined(WITH_ESPNOW_BRIDGE) \ + && !defined(WITH_BRIDGE) && !defined(ETHERNET_ENABLED) \ + && !defined(BLE_PIN_CODE) && !defined(WIFI_SSID) \ + && !MESH_ENABLE_EMERGENCY_POWER \ + && (!defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW) + // The integrated STM32WL target has no 2.4 GHz services. Keep the same + // unsupported-command replies without retaining the state machine. + if (mesh::wireless::handleNoServices(command, reply, 160)) return; +#else if (mesh::wireless::control().handle(command, reply, 160, millis(), _callbacks->wirelessCommandSource(sender_timestamp))) return; +#endif // Infrastructure callers can already have reflected a three-byte CLI // request prefix into the same 160-byte reply. Reserve it for bulk pages. #if MESH_ENABLE_CARRIER_TOOLS || MESH_ENABLE_MORSE_RX diff --git a/src/helpers/CompanionReaderConfig.h b/src/helpers/CompanionReaderConfig.h index 258535495..615528d18 100644 --- a/src/helpers/CompanionReaderConfig.h +++ b/src/helpers/CompanionReaderConfig.h @@ -1,12 +1,7 @@ #pragma once #ifndef COMPANION_FEATURE_READER - #if defined(COMPANION_RADIO_FULL) && defined(ENABLE_USB_INTERFACE) \ - && (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) - #define COMPANION_FEATURE_READER 1 - #else - #define COMPANION_FEATURE_READER 0 - #endif + #define COMPANION_FEATURE_READER 0 #endif #ifndef COMPANION_READER_COMPACT_STREAM diff --git a/src/helpers/WirelessControl.h b/src/helpers/WirelessControl.h index 79a47d5e2..f2f01ade3 100644 --- a/src/helpers/WirelessControl.h +++ b/src/helpers/WirelessControl.h @@ -47,6 +47,24 @@ inline Command parse(const char* s) { return {scope, Action::Invalid}; } +// Match Control::handle when the build has no wireless services, without +// retaining the state-transition machinery on flash-constrained devices. +inline bool handleNoServices(const char* text, char* reply, size_t size) { + const auto command = parse(text); + if (command.action == Action::None || !reply || !size) return false; + if (command.action == Action::Invalid) { + snprintf(reply, size, MESH_CLI_REPLY_TEXT("Error: use set wifi/espnow on|off [force]; set 2.4ghz on [all|force]|off [force]", "Error: set wifi/espnow on|off [force]; set 2.4ghz on [all|force]|off [force]")); + } else if (command.action == Action::Get) { + if (command.scope == All) + snprintf(reply, size, "wifi=unavailable, bluetooth=unavailable, espnow=unavailable"); + else + snprintf(reply, size, "%s unavailable", command.scope == WiFi ? "wifi" : "espnow"); + } else { + snprintf(reply, size, MESH_CLI_REPLY_TEXT("Error: requested wireless service unavailable in this build/boot", "Error: wireless service unavailable this build/boot")); + } + return true; +} + class Backend { public: virtual uint8_t available() const = 0; // Services usable in this boot. diff --git a/src/helpers/esp32/StaticHtml.h b/src/helpers/esp32/StaticHtml.h index 60f0a7d83..bc25ab357 100644 --- a/src/helpers/esp32/StaticHtml.h +++ b/src/helpers/esp32/StaticHtml.h @@ -3,6 +3,273 @@ #pragma once #include #include +const uint32_t MESH_HTML_ELEGANT_OTA_COMPACT_LEN = 4211; +const uint8_t MESH_HTML_ELEGANT_OTA_COMPACT[] PROGMEM = { + 0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0xff,0x94,0x52,0x55,0xa2,0xac,0x36, + 0x18,0x7e,0x9f,0x55,0xa4,0xa9,0x0d,0xb7,0xc8,0xe8,0x91,0x19,0xa0,0xee,0xee,0x2e, + 0x21,0xf9,0x19,0xd2,0x03,0x84,0x9b,0x84,0x91,0x4b,0x8f,0x6c,0xa6,0xb6,0x8e,0xee, + 0xa4,0x2b,0xe9,0x8f,0x5d,0x7b,0xeb,0xc3,0x97,0xfc,0xee,0xe1,0x33,0x42,0x71,0x7b, + 0xaa,0x80,0x64,0xb6,0xc8,0xe3,0x49,0xd8,0x7d,0x61,0x06,0x4c,0xc4,0x61,0x01,0x96, + 0x11,0x9e,0x31,0x6d,0xc0,0x46,0xb4,0xb6,0xa9,0x77,0x41,0x07,0x69,0xc9,0x0a,0x88, + 0xe8,0x5e,0xc2,0xa1,0x52,0xda,0x52,0xc2,0x55,0x69,0xa1,0x44,0xab,0x83,0x14,0x36, + 0x8b,0x04,0xec,0x25,0x07,0xaf,0x63,0x5c,0x59,0x4a,0x2b,0x59,0xee,0x19,0xce,0x72, + 0x88,0xe6,0x14,0xb3,0x58,0x69,0x73,0x88,0x3f,0x04,0x93,0xbd,0xae,0x34,0x90,0x8f, + 0xbf,0x78,0x35,0x0c,0x7a,0x59,0x68,0xec,0x09,0xbf,0x49,0xa2,0xc4,0xa9,0x49,0x31, + 0xea,0x66,0x7e,0x56,0x1d,0x89,0x61,0xa5,0xf1,0x0c,0x68,0x99,0x6e,0x0b,0x76,0xec, + 0x03,0x6f,0x96,0x17,0x1a,0x0a,0xe4,0xf5,0x4e,0x96,0x9b,0x05,0xd2,0x84,0xd5,0x56, + 0x6d,0x2b,0x26,0x84,0x2c,0x77,0x9b,0x19,0x99,0xb7,0xfa,0x5c,0x96,0xe0,0x65,0x20, + 0x77,0x19,0xc6,0xf2,0x57,0xeb,0xeb,0x49,0x2a,0x21,0x17,0xd8,0x53,0x33,0xb8,0xb6, + 0x66,0x64,0x76,0x9d,0xd4,0xd6,0xaa,0x12,0xcb,0xad,0x6a,0xdb,0xa7,0x96,0x65,0x86, + 0x29,0xed,0xa0,0x69,0xc6,0xc0,0xfe,0x1a,0x1d,0xba,0xe0,0xd7,0x95,0x56,0x3b,0x0d, + 0xc6,0x34,0x7d,0x45,0xf3,0xd9,0xec,0xf9,0xeb,0xc9,0xb3,0xc6,0x32,0x5b,0xa3,0x2c, + 0x93,0x16,0x3c,0x53,0x31,0x0e,0x9b,0x4a,0xe3,0x38,0x34,0xab,0xb6,0x6a,0x0f,0x3a, + 0xcd,0xd5,0xa1,0xe3,0x36,0xac,0x3c,0x1d,0x30,0x07,0x5c,0x4f,0xc2,0xa0,0xef,0x3c, + 0x0c,0xfa,0xe1,0xb7,0x03,0x68,0xf7,0x31,0x7f,0x6a,0x4c,0x28,0x08,0x2b,0x22,0x45, + 0x44,0xa5,0xc0,0x89,0x4b,0x7b,0xa2,0xf1,0x67,0xe8,0x81,0x85,0x91,0x7e,0xec,0x64, + 0x54,0xfc,0x7b,0xf7,0x47,0x18,0x54,0x18,0x64,0x6c,0x38,0x0e,0x73,0xd8,0x41,0x29, + 0xe2,0x2f,0x2b,0xc1,0x2c,0x90,0x76,0xf1,0x61,0x30,0xc8,0x26,0x61,0xce,0x12,0xc0, + 0xe5,0x77,0x03,0xe8,0x74,0x11,0xd5,0x18,0x58,0xd1,0x61,0xdd,0xad,0x88,0x92,0x3d, + 0xcb,0x6b,0x64,0x52,0xa9,0x8b,0x03,0xd3,0x28,0xe0,0x19,0xf0,0x2b,0x10,0x31,0x79, + 0x6b,0x10,0x61,0xc8,0x36,0xd2,0xff,0x8f,0x98,0x83,0x39,0x19,0x0b,0x05,0x6d,0x63, + 0x8d,0xcc,0x18,0x2d,0x0c,0xc6,0x36,0xc6,0xc0,0x24,0x55,0xba,0x77,0xa3,0xf1,0xbb, + 0x05,0xdb,0x01,0x99,0xfa,0x89,0x2c,0x89,0xd2,0xa4,0xfd,0xfd,0xdd,0x03,0xe7,0xa1, + 0x73,0xa2,0xc7,0x32,0xa4,0x18,0x7c,0xba,0x82,0x46,0x9a,0x71,0x0e,0x15,0xde,0x6f, + 0xeb,0xe8,0x0e,0xde,0xb4,0x75,0xc3,0x64,0xfd,0xfa,0x07,0xf3,0x9e,0xa1,0x5d,0x98, + 0xba,0xca,0x15,0x13,0x14,0xc7,0xd9,0xfe,0x61,0xd0,0xeb,0xd0,0x63,0x3c,0x8b,0xce, + 0x6a,0x64,0x28,0xc1,0xd3,0x8d,0x28,0xde,0xc8,0xc3,0x96,0x91,0xca,0xa4,0xc0,0x75, + 0x61,0x77,0xa3,0xd9,0xb8,0xde,0xfe,0x88,0x28,0xd1,0x2a,0x87,0x87,0x5c,0xdc,0x2f, + 0xd4,0x70,0x2d,0x2b,0x1b,0x07,0xf7,0x08,0x5e,0x97,0xbe,0xf2,0x0a,0xb1,0x26,0x4b, + 0x7f,0xe6,0x2f,0x5c,0xf2,0x6a,0x29,0xf4,0x3f,0x7f,0x93,0xd7,0x75,0xfd,0xc0,0x25, + 0xd3,0x0f,0xdf,0xfd,0x82,0x7c,0xfc,0x19,0xf9,0xfa,0x8b,0xb7,0x3e,0xf9,0xc0,0xd9, + 0x12,0x03,0x40,0x0e,0x90,0xd4,0x32,0xd8,0xe3,0xca,0x95,0xf6,0xc9,0xbd,0x60,0x32, + 0x4d,0xeb,0x92,0x5b,0xa9,0xca,0x69,0xca,0xb8,0x55,0xfa,0xe4,0x34,0x32,0x9d,0xb6, + 0xdd,0xaa,0x94,0xc0,0xb1,0x52,0xda,0x9a,0x28,0x8a,0xa8,0x4a,0x7e,0x05,0x6e,0xa9, + 0xd3,0x14,0x4a,0xd4,0x39,0xf8,0xa3,0x6a,0xf0,0x9a,0x3a,0xd7,0x90,0x1b,0x20,0x8f, + 0x7c,0x05,0xa4,0xb2,0x84,0xd6,0x75,0xcc,0x40,0x5f,0x78,0xa1,0x17,0xfa,0xac,0x10, + 0x4e,0xd3,0xd3,0x63,0xda,0xde,0xbf,0xd9,0x33,0x4d,0x76,0xb9,0x4a,0xb6,0x56,0x9f, + 0x9a,0x96,0x88,0x0e,0x12,0x6b,0x3d,0x5c,0x73,0x66,0x79,0x36,0x05,0xa7,0x17,0x1a, + 0xc8,0xd3,0xeb,0x96,0xf2,0x3f,0x6f,0x47,0xf0,0xe1,0x1b,0xeb,0xc7,0x0a,0xb9,0x76, + 0x1e,0x35,0x55,0x97,0x7d,0x1a,0xcc,0x47,0x6b,0xac,0xcf,0x58,0x2d,0xb1,0x8d,0x6d, + 0x9b,0x87,0x09,0xb1,0x5c,0x44,0x0f,0x4d,0x99,0x9b,0x38,0x8d,0x06,0x5b,0xeb,0x92, + 0xb0,0x97,0x92,0x17,0x56,0x8b,0xcb,0xd5,0xe5,0xd9,0xf9,0xe2,0x72,0x7d,0xed,0x66, + 0x70,0xfc,0x99,0x67,0x3a,0xfa,0x1e,0x97,0xe6,0xd2,0x39,0x62,0x81,0x58,0x22,0x56, + 0x88,0x35,0xe2,0x0c,0x71,0x8e,0xb8,0x40,0x5c,0x22,0x18,0x22,0x41,0x70,0x84,0x40, + 0x00,0x22,0xa5,0x3f,0x6e,0xc7,0x74,0x84,0x17,0xe5,0xf4,0xbe,0x8b,0x49,0xdd,0xa3, + 0x6b,0x5c,0xeb,0x34,0x2c,0xea,0x0a,0x9a,0x0e,0xaf,0x7b,0xdf,0x71,0x7b,0xf2,0x88, + 0x5a,0x67,0x3b,0x54,0x36,0x68,0xc3,0xd0,0xfc,0xc6,0xe2,0x38,0x5e,0x2e,0x3c,0x83, + 0x75,0x5f,0x3f,0x0c,0x8b,0xc7,0xc0,0x4f,0x3c,0x07,0xf4,0xba,0x72,0xba,0x71,0xb2, + 0xe8,0xf8,0xfd,0xec,0x47,0x37,0xc1,0x6f,0xfe,0xa3,0xcb,0xf1,0x5b,0xfc,0xc7,0xe1, + 0x54,0x18,0x47,0x10,0xc3,0xc0,0x6e,0xf4,0x7f,0x6c,0x86,0x83,0x4e,0x42,0x86,0xc1, + 0xb4,0xa0,0x71,0xed,0x59,0x65,0xc0,0xb0,0xfc,0xb9,0x76,0x5c,0xf6,0xf3,0x2a,0xcb, + 0xf3,0xae,0xd4,0x78,0x54,0xea,0xd3,0xf2,0x0b,0xdd,0x16,0x92,0x4a,0x31,0x64,0x1b, + 0x58,0x5d,0xe5,0x91,0xa6,0xf8,0xdf,0x64,0xfc,0xb4,0x54,0x60,0x7d,0x01,0x48,0x95, + 0x47,0xa1,0x06,0x0b,0x32,0x37,0x9b,0xb2,0x0f,0xce,0xa7,0x20,0xf4,0xf3,0xee,0xf7, + 0xad,0x0d,0x77,0xf1,0xa8,0x69,0x29,0x00,0xdb,0x02,0x94,0x0a,0x8f,0x4e,0x15,0x1e, + 0x0c,0x58,0x82,0x0a,0x5a,0xf9,0xa4,0xb3,0xd0,0xcf,0xbb,0xc1,0x13,0xb9,0xc1,0xa3, + 0xd1,0xd3,0x01,0xd6,0x05,0x28,0x75,0x1e,0x8d,0x8a,0x78,0xec,0xe7,0xa6,0x95,0x73, + 0xde,0x78,0x6b,0x95,0xf0,0x58,0x7e,0xdf,0xc6,0x70,0x15,0x13,0x8a,0x9a,0x0c,0xfe, + 0x63,0xa4,0x4e,0x4c,0x9b,0x59,0x63,0x28,0x0c,0x77,0x13,0x02,0xc3,0x07,0x3a,0xda, + 0x55,0x8c,0x06,0xbc,0x94,0x61,0x52,0xfb,0x35,0x9e,0xc4,0x36,0x97,0xfc,0x41,0x25, + 0x3c,0xe2,0xbc,0xfa,0x9f,0x47,0x7b,0x21,0x5c,0x91,0x59,0x31,0xf4,0x58,0x6f,0x60, + 0x22,0x4a,0x52,0x1e,0x83,0xbc,0x17,0x34,0x84,0x05,0xa2,0x98,0x8a,0xa2,0x97,0x5a, + 0x90,0x55,0xca,0xd4,0x93,0xbd,0x21,0x82,0x48,0x4c,0xe1,0x43,0x50,0xf5,0x42,0x59, + 0x2a,0x54,0x11,0x53,0x10,0xa8,0x97,0x32,0xb9,0x4c,0x31,0x40,0x2f,0x54,0x91,0x92, + 0xc2,0x79,0xea,0x01,0xdf,0x41,0x49,0xea,0x24,0x9e,0x3c,0x04,0x41,0x7a,0x29,0x89, + 0x02,0x84,0xb1,0x47,0x7b,0xc1,0x88,0x89,0x98,0x8b,0xc6,0xa8,0xc7,0x18,0xc4,0x4d, + 0x4c,0xb8,0xfe,0x46,0x5d,0x6f,0x97,0x8f,0xaf,0x03,0xd5,0x0b,0x6e,0x51,0x6e,0xa0, + 0x27,0xc9,0x0e,0x52,0xbc,0x93,0x2e,0xb7,0xf3,0xc7,0xd7,0xe5,0x67,0x3f,0xe4,0x65, + 0x04,0x17,0x7e,0x9a,0xea,0x20,0xc9,0x3b,0xe9,0x7c,0x3b,0x7d,0x7c,0x1d,0x24,0xf4, + 0xe6,0x2a,0x81,0x08,0xc8,0x4b,0xa4,0x87,0x28,0xdf,0x45,0xa7,0xdb,0xf5,0xe3,0xeb, + 0x21,0xa2,0x5e,0x12,0x92,0x15,0x42,0xfc,0xf2,0xd0,0xe1,0xe1,0xdf,0x3d,0xd6,0x2b, + 0x08,0x66,0xe9,0x85,0xa1,0x07,0xd4,0x9b,0x24,0xc1,0x29,0x65,0xca,0xb1,0x5e,0xee, + 0xa4,0x91,0x22,0x36,0xe5,0x68,0x2f,0x25,0x33,0x89,0xd4,0x9c,0x72,0xaa,0x37,0xf3, + 0x54,0x75,0x95,0x9c,0x72,0xb4,0x17,0x08,0x95,0x24,0x5e,0x34,0x04,0x49,0x2f,0x64, + 0x88,0x4b,0x39,0x66,0x9e,0xa3,0x6f,0xb8,0x98,0xc0,0x08,0x53,0x10,0xa4,0x17,0x54, + 0xd5,0x13,0xea,0x31,0x24,0x71,0x2f,0x83,0x92,0x44,0xea,0xd0,0x13,0xbd,0x21,0xc4, + 0x84,0x53,0x43,0xa6,0x20,0x70,0x2f,0x14,0xbb,0x53,0x84,0xe8,0x9f,0xa2,0xfd,0xb2, + 0xff,0xcc,0x4d,0x22,0xcd,0xf2,0x29,0xd1,0x43,0x92,0x6f,0x92,0xfd,0xbc,0x3f,0x18, + 0xd9,0x8b,0x89,0xd9,0x42,0xd5,0xe5,0x09,0x01,0x0e,0x09,0xde,0x24,0xfb,0x69,0x7f, + 0x66,0x83,0x94,0x22,0x21,0x73,0x7e,0x39,0xfc,0x70,0xf8,0x9b,0x63,0xbf,0xee,0x07, + 0x42,0x7b,0x89,0x09,0x95,0x99,0xbf,0x08,0xf2,0x20,0xd4,0x2f,0x02,0xf4,0x82,0x09, + 0x55,0x31,0x39,0x4d,0x14,0xda,0x1b,0x38,0x38,0x4b,0xc4,0x66,0x8a,0xe8,0x05,0x33, + 0xad,0x70,0x19,0x1a,0xa8,0x17,0xa8,0x34,0x84,0x5c,0x69,0xa4,0x90,0xde,0x3c,0xc1, + 0x51,0x08,0x9d,0x28,0xa8,0x97,0x58,0x9a,0x04,0x33,0x8f,0x10,0xd2,0x2b,0x98,0x8d, + 0x51,0x51,0x23,0x84,0xf7,0x16,0x4e,0x5c,0xc8,0x9a,0x08,0xaa,0x97,0x2b,0x89,0xab, + 0x66,0x4c,0x00,0xe0,0x5e,0xca,0x48,0x58,0x8a,0x8d,0x04,0xb0,0xde,0x4c,0x28,0x94, + 0x8d,0x69,0x44,0xe0,0x5e,0x55,0x26,0x92,0x6e,0xf8,0xc3,0x70,0xd9,0x3f,0xcf,0xb7, + 0x7b,0xde,0xdf,0x77,0x45,0xa5,0x0b,0xa5,0xea,0x93,0xe1,0x07,0xc3,0x5f,0x8c,0xf3, + 0xfe,0x79,0xba,0xdd,0xe3,0xfe,0x69,0x15,0xec,0x59,0x50,0xbc,0x49,0xe8,0x90,0xf0, + 0x4b,0x72,0xda,0x3f,0xaf,0xb7,0x7b,0xdf,0xcf,0x2f,0xa7,0x70,0x31,0x2d,0xb2,0x97, + 0xc6,0x0e,0x4d,0xbc,0x34,0xd7,0xfd,0xf3,0x72,0xbb,0x07,0xfe,0x9d,0xab,0x85,0x8a, + 0x92,0xd9,0xcb,0x83,0x87,0x07,0xf8,0x1d,0x04,0xee,0x0d,0x41,0xa4,0x69,0x16,0x18, + 0x92,0xa4,0x17,0xb2,0xb4,0xd2,0x9d,0x7c,0x4a,0x3a,0x56,0x6f,0x30,0x96,0xa9,0x07, + 0xbd,0x98,0x4c,0x8b,0x39,0xaa,0xa6,0xa2,0xbc,0x83,0x32,0x44,0x20,0x62,0x35,0x04, + 0xc1,0x7a,0x09,0x39,0x10,0xaa,0x53,0x8f,0xf7,0x82,0x39,0xa1,0x52,0x92,0xc6,0x22, + 0xe9,0x0d,0x42,0x05,0x83,0xeb,0x98,0xf4,0x18,0x83,0x19,0x0b,0x05,0x4d,0x45,0xe8, + 0x05,0x30,0x31,0x48,0xa2,0xa6,0x26,0xee,0x2d,0x90,0x91,0xc1,0x56,0x53,0x52,0xf5, + 0x12,0x15,0x4d,0x33,0xc3,0xbf,0x40,0xff,0xb1,0x63,0x1d,0xbc,0x8d,0x83,0x50,0xf8, + 0xaf,0x5c,0x37,0x14,0xea,0x7a,0x65,0x9d,0x4d,0xa7,0xc6,0x4d,0x8d,0x1b,0x5a,0x91, + 0x75,0xca,0x6c,0xc8,0x39,0xb8,0x8a,0x5d,0x35,0x55,0xea,0xff,0x7e,0xef,0x61,0xec, + 0xac,0xdb,0x7b,0x75,0x04,0xf8,0x00,0xbd,0xef,0xc1,0x07,0x0a,0x1f,0xbe,0x7a,0x44, + 0x87,0x61,0xa1,0x5b,0x4e,0x24,0xba,0x0c,0x0b,0xdd,0x72,0x23,0xd1,0x63,0x58,0xe8, + 0x96,0x17,0x89,0x3e,0x83,0x02,0x5a,0x2b,0x4f,0xa9,0x6e,0xfc,0x96,0xa4,0xfa,0x19, + 0x65,0x5a,0xa9,0x68,0x47,0x5c,0x06,0xe0,0x3d,0x10,0x29,0xec,0x40,0x86,0x75,0x3f, + 0x90,0x4c,0xf8,0x74,0x6e,0xfa,0xdb,0x12,0x32,0x8d,0x44,0x6a,0xa1,0x75,0x76,0x99, + 0xf4,0x07,0xe7,0x19,0x91,0x94,0x91,0x55,0x80,0x39,0x34,0x0c,0x9b,0x9b,0xb0,0x4b, + 0xf1,0xf6,0xd8,0xc4,0x3d,0xc0,0x5d,0x9f,0xe6,0xe6,0x11,0x68,0x62,0xad,0x53,0x7d, + 0xd3,0x99,0x4e,0x3b,0x77,0xa4,0xf3,0x35,0x84,0x3b,0x6d,0x19,0x31,0x02,0x9f,0xcc, + 0x89,0x34,0x35,0x5d,0x77,0x23,0xc3,0x07,0x1b,0x5e,0xf4,0x49,0x12,0x4e,0xb9,0x5a, + 0x4a,0xa4,0x56,0x3c,0x50,0x57,0xe0,0x00,0xa2,0x79,0x31,0x10,0x6d,0xa7,0xe1,0xb9, + 0xf8,0x0a,0x69,0x79,0xfc,0xc8,0x6d,0x40,0xcb,0x6b,0x36,0x5a,0xfc,0xa8,0x82,0x7d, + 0x5e,0xa2,0x4d,0x20,0xcc,0xcd,0xe4,0xac,0x23,0x63,0x9e,0x4d,0xae,0x79,0x9c,0xf0, + 0x51,0x99,0x06,0xa6,0x10,0x0a,0x85,0x79,0xd4,0x75,0x22,0xe6,0xc9,0xab,0x23,0xf1, + 0x72,0xdb,0xac,0xf4,0xa6,0x8b,0xcf,0x7d,0x75,0x45,0xe4,0x51,0xdd,0xe7,0x92,0x52, + 0x9a,0xa7,0x62,0x1d,0xa7,0x41,0x11,0xaa,0x22,0x1c,0x60,0x4c,0xd1,0xb6,0xf9,0x47, + 0x7f,0xa3,0xa5,0x25,0x35,0xf3,0x80,0x8e,0x43,0xe7,0x38,0xbb,0x58,0xd3,0xfb,0x75, + 0x15,0x84,0x21,0x91,0x7b,0x7e,0x18,0x7a,0x34,0x5f,0x1e,0xe5,0xb8,0xcd,0x45,0x4f, + 0x20,0x87,0x44,0x9e,0xd4,0x6a,0x1b,0x69,0xe1,0x0c,0xba,0x14,0xd4,0xa9,0xaf,0x04, + 0x8c,0x84,0x9d,0xe7,0xb0,0x50,0x42,0x1d,0x36,0x03,0x2c,0xe1,0xdf,0xca,0x92,0x97, + 0x45,0xa2,0xb0,0x87,0xd6,0x44,0x3b,0x28,0xc7,0xc4,0x3a,0x3c,0xa5,0xc4,0x9a,0xdb, + 0xbc,0x99,0xd3,0xdd,0x63,0x48,0x3f,0x11,0xd7,0xe8,0xf2,0x3e,0x56,0x19,0x81,0x49, + 0xe8,0x0e,0xc0,0xf0,0x60,0x24,0x57,0x61,0x47,0xc3,0xf7,0x70,0x5c,0x74,0x40,0xc7, + 0x8f,0x44,0x9c,0x98,0x7a,0x2d,0x12,0xb0,0x39,0xef,0x25,0x6c,0xc4,0xa2,0xa1,0x55, + 0xa9,0xac,0xaa,0x55,0x89,0xce,0xaf,0x15,0x4c,0x49,0x07,0xe5,0xa1,0xab,0x4b,0xaa, + 0xe9,0x08,0xac,0x87,0xea,0x74,0xad,0x97,0x3e,0x54,0x83,0xdb,0x07,0xaf,0xa5,0xca, + 0x9a,0xe7,0x1a,0xb4,0x2b,0x35,0x75,0xbe,0xbf,0x9a,0xf0,0x88,0xfe,0x97,0x50,0x29, + 0xa1,0x29,0x78,0x72,0x44,0x15,0xe2,0x49,0xc5,0xf6,0x36,0x1f,0xeb,0xcc,0xc6,0x90, + 0xd9,0x38,0xf4,0x83,0xb1,0x4e,0x2c,0x65,0xc2,0x58,0x77,0x6d,0x75,0x72,0x32,0x3e, + 0x6c,0x32,0x7f,0x1f,0x42,0xb1,0x55,0x10,0xa1,0xbc,0x8c,0xb2,0x88,0x80,0x01,0x66, + 0x45,0x80,0xe5,0x0b,0x74,0x66,0xad,0xec,0xd0,0x0c,0x17,0x4e,0x93,0xc1,0x5a,0x75, + 0x3b,0xce,0xac,0x71,0x22,0x15,0xd9,0xde,0xa6,0x39,0x6c,0x07,0x76,0xeb,0xeb,0x71, + 0x7b,0x34,0x88,0xe3,0x64,0x9b,0xd2,0x2d,0x21,0xb6,0x6b,0x7d,0xdf,0xf1,0x6d,0xb7, + 0xd3,0xed,0xf9,0x5d,0xb7,0xd3,0xa8,0x77,0x5b,0x0d,0xa7,0xd5,0x6f,0xa1,0xaf,0xd1, + 0xe8,0xd5,0x5a,0x2e,0xb8,0xad,0x6b,0x1e,0xe5,0x8c,0xdf,0x15,0x84,0xe2,0xf4,0x56, + 0x90,0xd9,0x7e,0xbd,0x06,0xd6,0x04,0x65,0xe4,0xce,0xd4,0xf8,0x44,0xe3,0xf8,0xdd, + 0x13,0x51,0x53,0xc2,0x60,0xac,0x55,0x2b,0x09,0x83,0xf4,0x97,0x54,0xc0,0x8b,0x79, + 0x79,0xbe,0xf0,0x6d,0xb5,0x90,0x2f,0x6e,0x86,0xc3,0xc1,0x14,0x39,0x56,0x06,0x2a, + 0xb8,0xb7,0x5b,0x4b,0x7d,0x16,0x38,0xd6,0x59,0x82,0x73,0xac,0x34,0x96,0x3d,0x30, + 0x66,0x49,0xc5,0x92,0xce,0x17,0x2e,0x67,0xdc,0x99,0x5c,0x13,0x30,0xbc,0xcd,0xf9, + 0x44,0xf6,0xb1,0x80,0xff,0x7b,0x1b,0x75,0x20,0x87,0xd8,0x17,0xda,0x95,0xf1,0xfa, + 0xbc,0x93,0x8d,0x40,0x66,0x33,0x84,0x99,0x39,0xd2,0xf6,0xbb,0xe2,0x8d,0x6b,0xc1, + 0x51,0x1b,0xf8,0x7e,0x5f,0xe1,0x54,0xac,0x98,0x65,0x2f,0x3d,0x31,0xe1,0x7a,0xef, + 0xbd,0x37,0x16,0xe4,0x98,0x1c,0x06,0x07,0xc4,0x76,0x7f,0x57,0xde,0xf3,0x65,0x99, + 0xd1,0x48,0x48,0x68,0x61,0x5b,0xda,0x55,0x5b,0x9a,0x5e,0x85,0x98,0x65,0xab,0xce, + 0x6e,0x56,0xd8,0x04,0x57,0x87,0x08,0x65,0x0c,0x37,0xab,0xca,0x85,0x2f,0x4a,0x65, + 0x3c,0x4a,0x51,0x31,0xcf,0xea,0x60,0x63,0x47,0xf0,0x3b,0xc4,0x67,0x83,0xc3,0xfb, + 0xa0,0x88,0x24,0x46,0x78,0xcd,0xc1,0xe7,0x45,0x1a,0x29,0x59,0x8a,0x06,0xc8,0xc0, + 0x8b,0x23,0x2e,0xd4,0x7f,0x92,0xbb,0x5f,0x97,0x4d,0x95,0x70,0xf9,0x19,0x75,0x12, + 0x25,0x98,0xce,0xba,0x7a,0x46,0x02,0x44,0x57,0x94,0xad,0xdc,0x00,0x27,0x39,0x9e, + 0x83,0xdd,0x56,0x7f,0xac,0x27,0x96,0x42,0x63,0xd0,0x1d,0x58,0x46,0x00,0xb2,0x26, + 0x81,0x43,0xe2,0x1a,0xb9,0x70,0x00,0xc4,0x42,0x4b,0xa2,0xc8,0x29,0x4c,0xb4,0xc5, + 0x42,0x12,0xb4,0x1f,0x04,0xcc,0x41,0x36,0xf0,0x5d,0x6a,0x41,0xcd,0xc5,0xd8,0x42, + 0x6d,0x23,0x89,0x60,0xf0,0xc6,0x9d,0x99,0xbc,0x11,0xe5,0x53,0x31,0xfb,0x09,0x52, + 0xb8,0x92,0xef,0x12,0xd6,0x5f,0xbf,0x69,0xf0,0x3a,0x75,0xed,0x4d,0x73,0x0d,0x7e, + 0xbd,0x5d,0x0f,0x04,0xaf,0x85,0xdc,0x86,0x04,0x15,0x15,0x18,0x8a,0xd7,0x49,0xbc, + 0xe4,0x63,0x5e,0x24,0xd0,0x2a,0x9f,0x3e,0xba,0x79,0xb9,0x5c,0x2c,0xcb,0x82,0x17, + 0x0a,0x4a,0xbb,0x1f,0xbe,0x18,0x2e,0x0d,0xf0,0x80,0xd2,0xf4,0x8d,0x6d,0x05,0x00, + 0xf0,0x09,0xf6,0xe9,0x4d,0xad,0x57,0xc7,0xe5,0x00,0xa0,0xbb,0xcc,0x06,0xa0,0x1c, + 0x30,0xa3,0xf6,0x38,0x94,0x78,0xd7,0x3c,0x08,0xcf,0xfc,0x6c,0xa8,0x50,0x03,0x06, + 0xa4,0x3f,0x32,0xe8,0xf6,0x7c,0x2d,0xbb,0x2b,0xd6,0x79,0x40,0x98,0x22,0x4d,0xa3, + 0x51,0x6c,0x04,0xf9,0xe6,0x29,0x69,0x47,0x5c,0x75,0x70,0x50,0x25,0x0e,0x30,0x36, + 0x2f,0x2b,0x1e,0x41,0xbc,0x5c,0xe6,0x3b,0xe0,0xfe,0x3c,0xf7,0xfb,0x04,0xb6,0xdc, + 0x92,0x94,0x45,0x12,0x0b,0x5e,0xc0,0x5f,0x4b,0xe5,0x7a,0x3c,0xcb,0xaa,0x5a,0xf8, + 0x35,0x87,0xfd,0x74,0xc0,0x7a,0x15,0xaf,0x9b,0x5c,0x38,0x16,0x21,0x44,0x2b,0x81, + 0x26,0x44,0xc3,0x5a,0x61,0x21,0x61,0xca,0xf8,0xe9,0xc3,0x66,0x87,0x41,0x10,0x39, + 0xe6,0xbb,0x7c,0x58,0x38,0x64,0x53,0xa2,0x35,0x55,0x24,0x37,0xa4,0x82,0x9d,0x7a, + 0xfe,0xa4,0xbc,0x94,0x15,0x71,0xb5,0xa3,0x7a,0x1a,0xac,0x90,0x82,0x48,0x5c,0x9e, + 0x94,0x55,0x9c,0xc0,0x86,0x8e,0x93,0x6d,0xe9,0xf1,0x6d,0xf0,0xf8,0xb6,0xf2,0xf8, + 0xda,0xd7,0xe1,0xf6,0x24,0xfa,0x86,0xed,0x49,0xac,0x60,0xd9,0xd4,0xf3,0x71,0xdb, + 0xe7,0x10,0x93,0x0e,0x98,0x50,0xac,0xbf,0x61,0xd8,0xf0,0xd8,0xea,0x48,0xff,0xd1, + 0x8e,0x2f,0x09,0x5a,0x2e,0xd3,0x2f,0xb7,0xa0,0xa2,0xca,0x62,0x55,0x71,0x34,0x13, + 0x5b,0xe9,0x75,0x53,0xef,0x90,0x0e,0x56,0x19,0x8d,0x4a,0xd9,0x27,0x91,0x68,0x87, + 0xf4,0x1e,0x5b,0xb9,0x69,0x92,0x18,0xa0,0x56,0xe1,0x20,0x4d,0xc2,0x34,0x94,0xd5, + 0xdb,0xfe,0x5a,0x91,0x7f,0x8f,0x66,0x9e,0x44,0x7a,0x35,0xa4,0x45,0xb2,0x94,0x59, + 0xd6,0x3d,0xd3,0xca,0x45,0x83,0xad,0x4d,0xdd,0x3d,0xae,0x29,0x0c,0xf7,0xcc,0x46, + 0x68,0xf3,0xb8,0x9e,0xeb,0x93,0xbe,0x21,0xd4,0x7b,0xe4,0x37,0xdb,0xd1,0x20,0xe4, + 0xd1,0xb6,0xe8,0x18,0xb7,0x8a,0xb7,0x28,0x6e,0x1c,0xaa,0x3a,0x7c,0x3a,0x4a,0xf9, + 0xc7,0x3b,0x01,0x62,0xea,0x1d,0xe3,0x14,0x48,0xb4,0x87,0x1e,0x2b,0x48,0x14,0xe8, + 0x3a,0x19,0xc4,0x24,0x62,0xf9,0xb9,0x49,0x19,0x8c,0x6a,0xcb,0x7c,0x78,0x4e,0x24, + 0x4d,0x68,0xcf,0x4d,0x0e,0x58,0x05,0x82,0x26,0xec,0xec,0xb4,0xeb,0x33,0x11,0x81, + 0x19,0x23,0xd9,0xf1,0x01,0xe7,0xb3,0x4e,0x60,0x80,0x8a,0x46,0x90,0x93,0x1d,0xf5, + 0x8f,0xf4,0x14,0x34,0xa4,0xc7,0x78,0x54,0xb4,0x25,0x21,0x9c,0x3d,0x14,0x65,0x7a, + 0xc0,0x5d,0x6a,0x45,0xf9,0x0b,0x25,0x9a,0x1a,0xe1,0xe8,0x20,0x01,0x39,0xd7,0xcb, + 0xab,0x76,0x79,0xd1,0xc1,0xb5,0xa3,0xba,0x5c,0x91,0xc5,0xa8,0x6b,0x02,0x5a,0xa7, + 0x2f,0xb6,0x35,0x68,0x1d,0x3f,0x1e,0xa9,0x6a,0xca,0x21,0x86,0xcb,0x1d,0x5b,0xd3, + 0x94,0xe7,0x5c,0xf0,0x91,0xa1,0x17,0xaa,0x15,0x23,0x91,0x59,0x31,0xd2,0xf4,0x92, + 0x57,0x81,0x64,0xc8,0xdb,0x11,0x45,0xe7,0x62,0x95,0xa5,0xd6,0x13,0xd7,0x49,0x1e, + 0x4b,0x8c,0x94,0xc7,0x7e,0x36,0xb1,0x85,0xfe,0xff,0x1f,0x53,0x5c,0x05,0x2c,0x33, + 0xe2,0x62,0xa7,0x1c,0xfb,0x94,0x6b,0xe4,0xd2,0x0e,0xd8,0xc1,0xda,0x8c,0xb0,0x2e, + 0xb1,0x81,0x3a,0xec,0xb7,0xc5,0x48,0x72,0xc4,0xa7,0x0c,0xbd,0xc5,0x7c,0x9d,0xf5, + 0xb0,0x65,0xda,0x26,0x20,0x9c,0xef,0x4f,0xf7,0x69,0x37,0xb5,0xad,0x63,0x82,0x61, + 0xeb,0xb4,0xa2,0x5e,0xe8,0x43,0xfa,0x52,0x5c,0x55,0x56,0xe9,0x1c,0x3a,0x93,0x98, + 0x60,0x55,0x5d,0xdc,0x07,0x3c,0x5f,0x54,0x0d,0x67,0xbd,0x35,0xd6,0xe9,0x3a,0x00, + 0x6c,0x9d,0x17,0x4e,0xde,0x55,0xcc,0x75,0x80,0xee,0x15,0xf4,0xad,0xb7,0xa9,0xe0, + 0xa2,0x2a,0xd7,0xe7,0xdd,0x23,0x98,0x51,0x80,0xea,0x08,0x35,0x9b,0xbe,0x6b,0x03, + 0x37,0xd5,0x87,0x6d,0x7a,0xfa,0x5e,0x74,0xdc,0x4e,0x94,0x9d,0xa4,0x17,0x41,0x6e, + 0xfe,0xbf,0x0d,0xc8,0xb5,0xd1,0xd1,0xcd,0xe8,0x80,0xf5,0xff,0xe5,0xde,0xe4,0x48, + 0x38,0xdc,0xa7,0xe8,0x32,0x43,0x2e,0x1e,0xd2,0xd8,0x82,0x24,0xce,0x73,0x69,0x47, + 0x16,0x9a,0xe6,0x12,0xf5,0x3d,0x3e,0x98,0x6d,0xe7,0x1a,0xe5,0x08,0xe1,0x86,0x1b, + 0x9b,0xc5,0xb0,0xef,0x09,0xcc,0x6c,0x53,0xb6,0x6a,0xae,0x83,0xbd,0xd3,0x56,0xc7, + 0x27,0x1e,0xc3,0xc2,0x39,0x6d,0x6f,0xb8,0x23,0x0e,0x52,0x70,0x3a,0xdb,0x70,0xd3, + 0xeb,0xa5,0xe0,0x14,0x75,0xb0,0x78,0x4f,0xf5,0x55,0x95,0xc3,0x09,0x32,0x58,0x64, + 0xa7,0xac,0xc7,0x8e,0x09,0x57,0x2c,0x3c,0x83,0x03,0xa3,0x34,0x05,0xa8,0xa7,0x01, + 0xcf,0xd8,0xd7,0x2f,0x67,0x46,0x23,0x14,0x49,0x8e,0x3c,0x46,0x69,0x99,0x34,0x0b, + 0x5e,0x08,0x8c,0x9f,0xab,0x39,0xc7,0x8f,0x97,0x76,0x6e,0xa6,0x63,0x0f,0x67,0x1f, + 0x3d,0xe6,0xd3,0xdc,0xe2,0x30,0x18,0xcd,0x39,0x7a,0x2c,0x54,0x54,0x69,0x18,0x71, + 0x18,0x9e,0xf6,0x81,0xb0,0x9e,0x69,0x1c,0x06,0xd5,0x10,0x86,0xf8,0xa2,0x4c,0xb9, + 0x85,0xf0,0x7b,0xc3,0xc1,0x20,0x3c,0xe7,0x38,0xdb,0x77,0x11,0xc2,0xc4,0x93,0x3a, + 0xbd,0xb6,0x06,0x48,0xdf,0x12,0x72,0xfb,0xcc,0x04,0x61,0x75,0xa9,0xa9,0xb1,0xf6, + 0xd6,0xf0,0xf4,0x4a,0xca,0x05,0x69,0x56,0xc7,0x1b,0x39,0x4f,0x23,0x5c,0x0d,0x25, + 0x0d,0x67,0x0d,0xab,0x1d,0x0a,0x40,0x15,0x4f,0x8a,0x61,0xee,0xf9,0x26,0x4c,0x2e, + 0x40,0xa9,0x73,0xb0,0x56,0x36,0x67,0x55,0xf5,0x16,0xa0,0x50,0xfc,0xab,0x4a,0x95, + 0xa6,0x0e,0x04,0xdf,0x16,0x97,0xd5,0x84,0xb4,0xda,0x0b,0xb5,0x90,0xb3,0x38,0xaf, + 0x39,0x23,0x32,0x1c,0x0f,0x2d,0xde,0x7a,0x23,0xa7,0x73,0xd7,0xf5,0x18,0xaf,0xe7, + 0xef,0x25,0x71,0x32,0xe7,0x53,0xaf,0x28,0xd7,0x6a,0xb0,0x02,0xf7,0x56,0x2c,0x10, + 0x73,0x5e,0x18,0x87,0x83,0x09,0x97,0x65,0x51,0x73,0xe9,0xf8,0x11,0xce,0x60,0x7e, + 0xa2,0x97,0x82,0xf2,0x23,0x13,0xf3,0xaa,0xdc,0x1a,0x5d,0xad,0x2a,0x50,0xd1,0xbb, + 0xa9,0xe8,0x8e,0x9a,0x22,0xde,0x84,0xfa,0x8c,0xca,0x78,0x26,0x14,0x35,0xda,0x6f, + 0x35,0xd0,0x95,0x86,0x3d,0xc0,0x4d,0x4b,0xa6,0xb8,0x0d,0x3a,0xb7,0xd5,0x80,0x75, + 0x4c,0xd0,0xe2,0x07,0x59,0xba,0xbf,0xef,0xe9,0xf9,0x66,0x8f,0x4d,0xbc,0xd1,0x9f, + 0x7f,0x8c,0xbc,0x89,0x01,0x98,0xc7,0x55,0x8a,0x23,0xc5,0x00,0x76,0xf5,0xf1,0x83, + 0x6f,0xbf,0xf1,0xb4,0x40,0x34,0x0c,0x6a,0x05,0xfc,0x49,0x85,0xf0,0xae,0x58,0xc3, + 0xd2,0xae,0x35,0xc2,0x95,0xe4,0xa3,0x42,0xa7,0x2c,0x12,0xe8,0xdc,0x1f,0xa3,0x6e, + 0x7a,0x51,0xb4,0x62,0x06,0x45,0x14,0x4d,0xf8,0xb1,0xfe,0xf2,0x4b,0xeb,0x0b,0xbc, + 0x77,0x0c,0x5b,0x7f,0xc0,0x0a,0xa8,0x21,0xff,0x0b,0xea,0x6c,0x17,0x5c,0xa5,0xa2, + 0xc5,0xa3,0xe8,0x1e,0xc5,0xa3,0xa2,0x2c,0xf8,0x62,0x09,0xb2,0x64,0x38,0xcd,0x1c, + 0xb4,0x4e,0x09,0x57,0x2d,0x3f,0x0c,0xf8,0x81,0xe4,0xe8,0xcb,0x8c,0xa9,0x1a,0xce, + 0x06,0xe5,0xe5,0xc8,0x31,0x89,0xa3,0xde,0x0d,0xca,0x2e,0xd8,0x57,0xe4,0xc6,0x58, + 0xa6,0xe2,0x94,0x57,0x12,0x02,0x47,0xb1,0x71,0xc2,0x5c,0xae,0x97,0xb3,0x19,0x76, + 0xd5,0xb3,0x7e,0x32,0x6f,0x8a,0x8f,0xd1,0x37,0x3f,0x7c,0x03,0x73,0x47,0x44,0x55, + 0x47,0x34,0x55,0x7d,0x9d,0xd8,0x01,0x0d,0x38,0x47,0x32,0xce,0xcd,0x2a,0x0d,0x40, + 0x9f,0x25,0xcc,0x9e,0x34,0xf1,0x2e,0xc7,0x79,0xd2,0xe4,0xb1,0xc0,0xb9,0x76,0xa9, + 0x44,0xdd,0x2c,0x60,0xa0,0xdd,0x23,0x0c,0x12,0x0f,0x3c,0xc3,0x31,0xa6,0x8d,0x67, + 0x8c,0x51,0x2f,0x97,0x4d,0x9e,0x82,0x45,0x85,0x84,0x85,0x7e,0xcc,0x47,0xb5,0x34, + 0x14,0x4f,0xa5,0xd3,0xd0,0xba,0x2b,0x87,0x18,0x56,0x39,0x43,0x8b,0x6f,0x82,0x20, + 0xca,0xd5,0x23,0xb4,0x97,0x3a,0xc9,0xd2,0x46,0x40,0x17,0x9f,0x01,0xdd,0x34,0xb1, + 0x90,0x0c,0x33,0x89,0x7a,0x36,0xed,0x83,0x28,0x51,0xc3,0x80,0x20,0x84,0x5f,0xac, + 0x78,0x40,0x39,0xee,0x81,0x0d,0xc6,0x8e,0xdf,0x55,0xa4,0x95,0xd5,0x82,0xfc,0x01, + 0x1f,0xae,0xc4,0x22,0x96,0x5e,0x82,0x08,0xa8,0x85,0x5c,0x7e,0x71,0xf7,0xce,0x0d, + 0x21,0x96,0x8f,0x68,0x09,0x68,0x10,0xae,0xc4,0xd3,0x92,0x7b,0xe0,0x68,0xcf,0x47, + 0x55,0xf0,0x1b,0x63,0xa1,0xbd,0x89,0x11,0xe3,0xa3,0x38,0x3e,0x7e,0x6a,0xf1,0x15, + 0x8f,0xa0,0x04,0xa0,0xb1,0xf7,0xe0,0xfe,0xe3,0x27,0x9e,0xaf,0x6b,0x14,0xd8,0x50, + 0x6f,0x6f,0x65,0x62,0x7e,0xb9,0xe2,0x32,0x9b,0xc0,0xd5,0x11,0x46,0x81,0x4b,0x82, + 0xfa,0x08,0x18,0xba,0xed,0x0b,0xae,0xb1,0xb3,0x19,0x7d,0x52,0x85,0x17,0x6f,0x81, + 0x1b,0x81,0x29,0xc9,0xf6,0x9c,0xd0,0x91,0x2f,0x02,0xaa,0xb2,0x69,0xfd,0x81,0xa4, + 0x79,0xfa,0x15,0x8c,0xd7,0xaf,0x2b,0x1f,0x94,0x22,0xce,0x59,0xd8,0x17,0x5c,0x34, + 0xb0,0x8f,0xa1,0xe5,0x4d,0xba,0x74,0x27,0xde,0x17,0x90,0xfd,0x2b,0x47,0x72,0x13, + 0x1b,0x6e,0x9c,0x69,0x00,0xd5,0x12,0xa3,0x08,0xe6,0x4b,0xcf,0x7b,0xea,0xe7,0x2b, + 0x12,0x10,0x1f,0x9f,0xb3,0x43,0x19,0x7e,0x29,0x15,0x9c,0xad,0x2c,0xcf,0x81,0xee, + 0x46,0x59,0x8a,0xc0,0x9b,0x2a,0x51,0x46,0x33,0x28,0x3d,0x10,0x99,0xe3,0x1b,0x4f, + 0x9e,0x3c,0x00,0xa1,0xba,0x64,0x27,0x1e,0x9b,0x5a,0x8b,0xba,0x44,0x3f,0x01,0x95, + 0x58,0xd8,0x23,0xeb,0x50,0x52,0x74,0x99,0x85,0x94,0x57,0x52,0xb6,0xa4,0x2c,0x0a, + 0xae,0xee,0xa0,0x8b,0x14,0x48,0x89,0x6a,0x27,0xf0,0x7a,0x89,0xc7,0x1b,0x65,0x25, + 0x0e,0x21,0x9e,0xc4,0x45,0xc2,0x73,0xa0,0xdf,0x8b,0x5f,0x63,0xa4,0x61,0xd4,0xa9, + 0xf8,0x22,0x00,0xd3,0x41,0x4d,0x0e,0xec,0xa9,0xb4,0xc4,0xff,0x2e,0xd6,0x76,0x75, + 0xc2,0x18,0x55,0x0f,0x53,0x94,0x44,0x7e,0xfb,0x56,0xa8,0xcd,0x26,0x5f,0xe5,0xa3, + 0x2c,0x53,0x8c,0x31,0x4a,0x25,0x43,0x1e,0xd9,0x23,0xf3,0x15,0xc3,0x8f,0xe6,0x2c, + 0xb5,0x8e,0x3f,0x77,0x03,0xff,0xd1,0x8f,0x43,0xfd,0x05,0x90,0x09,0x2d,0xa4,0x1f, + 0x35,0x00,0x00, +}; const uint32_t MESH_HTML_LIGHTWEIGHT_OTA_LEN = 619; const uint8_t MESH_HTML_LIGHTWEIGHT_OTA[] PROGMEM = { 0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00,0x02,0xff,0x74,0x52,0xc5,0x9a,0xdb,0x30, diff --git a/src/helpers/ota/OtaTinf.c b/src/helpers/ota/OtaTinf.c index 7ebd9c315..356432cd0 100644 --- a/src/helpers/ota/OtaTinf.c +++ b/src/helpers/ota/OtaTinf.c @@ -11,7 +11,7 @@ /* nRF52's Arduino recipe defaults to -Ofast, which more than doubles this * decoder. Keep the size policy local to tinf instead of changing MeshCore. */ #pragma GCC push_options -#if defined(NRF52_PLATFORM) +#if defined(NRF52_PLATFORM) && __GNUC__ >= 12 #pragma GCC optimize ("Oz") #else #pragma GCC optimize ("Os") diff --git a/test/fixtures/cli_reply_text.json b/test/fixtures/cli_reply_text.json index 3f0662e39..a5d33a316 100644 --- a/test/fixtures/cli_reply_text.json +++ b/test/fixtures/cli_reply_text.json @@ -1481,10 +1481,10 @@ } ], "noncompact_tokens_sha256": { - "src/helpers/CommonCLI.cpp": "e060c15118b94db76a41391b1fee872759ddcf308576b60eb1ae6ddec9444b8a", - "examples/simple_repeater/MyMesh.cpp": "bf614d2a4c672d33cbfbf2d4338f522c21e877abcfa7566f0e39a9fb6eecbbd9", + "src/helpers/CommonCLI.cpp": "c39dce89a0222f32d3da3f66f9c130491c93cf1e14e7eefeccb5b57dd63e9612", + "examples/simple_repeater/MyMesh.cpp": "c67ad3818404b7f1bd3fb99cd44aefac788f9c069342eb3124c693a066d03ca4", "src/helpers/RadioProfileCLI.cpp": "8e3d293cb522717b13901c27d7fc471f5c370710919fb3d74d35449bc497552d", - "src/helpers/WirelessControl.h": "0944bfdefb7003b47af52104b4c7447d7e5e65d54b3396790878c5b49e472913", + "src/helpers/WirelessControl.h": "d96f79fbc5736d9e08d4839c76b4af5c6cfccab5d3c2f01226cdbfeed1da56c5", "src/helpers/PowerSavingDiagnostics.h": "18b78f5780046ea8c772c4ce6c337741ae0c027039d9399c94c5f4205d114000", "src/helpers/FloodAdvertCLI.h": "0f378e4d0f6c3b9b79991068a4b4208139484d2144ee1d7c5094d39043463be4", "src/helpers/LedSettings.cpp": "df0681234ca1337148ad59caa935857bc692f98bf18603ca6d099c0382028400", diff --git a/test/test_compact_elegant_ota.py b/test/test_compact_elegant_ota.py new file mode 100644 index 000000000..c471e96b7 --- /dev/null +++ b/test/test_compact_elegant_ota.py @@ -0,0 +1,155 @@ +"""Exercise the compact OTA page against the existing ElegantOTA wire format.""" +from email import policy +from email.parser import BytesParser +import gzip +import hashlib +import http.server +import os +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import threading +import unittest + +from test_webconfig_ui_runtime import BROWSER, run_browser + + +ROOT = Path(__file__).resolve().parents[1] +VENDOR = ROOT / "webui/vendor/spark-md5-3.0.2.min.js" +HEADER = ROOT / "src/helpers/esp32/StaticHtml.h" +SYMBOL = "MESH_HTML_ELEGANT_OTA_COMPACT" + + +def embedded_page(): + source = HEADER.read_text(encoding="utf-8") + length = int(re.search(SYMBOL + r"_LEN = (\d+);", source).group(1)) + array = source.split("const uint8_t " + SYMBOL + "[] PROGMEM = {", 1)[1].split("};", 1)[0] + data = bytes(int(value, 16) for value in re.findall(r"0x([0-9a-f]{2})", array)) + if len(data) != length: + raise AssertionError("compressed OTA page has a bad length") + return gzip.decompress(data).decode("utf-8") + + +class CompactElegantOtaTests(unittest.TestCase): + def test_vendored_incremental_md5_vectors(self): + self.assertEqual(hashlib.sha256(VENDOR.read_bytes()).hexdigest(), + "d80e84c820cc5587a0ba3c8a20652099ea3fa7fc43944e812e56d449c1d9f1c9") + node = shutil.which("node") + if not node: + self.skipTest("Node is unavailable") + script = r''' +const SparkMD5 = require(process.argv[1]); +const crypto = require('crypto'); +if (SparkMD5.hash('') !== 'd41d8cd98f00b204e9800998ecf8427e' || + SparkMD5.hash('abc') !== '900150983cd24fb0d6963f7d28e17f72') process.exit(1); +for (const size of [0, 3, 64, 262144, 700123]) { + const input = Buffer.alloc(size); + for (let i=0;i", r''' +''') + + class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, *_): + pass + + def do_GET(self): + body = (b'{"id":"test node","hardware":"ESP32"}' if self.path == "/update/identity" + else page.encode("utf-8")) + self.send_response(200) + self.send_header("Content-Type", "application/json" if self.path == "/update/identity" else "text/html") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_POST(self): + body = self.rfile.read(int(self.headers["Content-Length"])) + message = BytesParser(policy=policy.default).parsebytes( + b"Content-Type: " + self.headers["Content-Type"].encode("ascii") + + b"\r\nMIME-Version: 1.0\r\n\r\n" + body) + parts = {p.get_param("name", header="Content-Disposition"): p for p in message.iter_parts()} + requests.append((self.path, parts["MD5"].get_payload(decode=True).decode(), + [(name, part.get_filename(), part.get_payload(decode=True)) + for name, part in parts.items() if name != "MD5"])) + failure = len(requests) == 3 + response = b"invalid image" if failure else b"OK" + self.send_response(500 if failure else 200) + self.send_header("Content-Length", str(len(response))) + self.end_headers() + self.wfile.write(response) + + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + with tempfile.TemporaryDirectory(prefix="meshcore-ota-browser-") as folder: + command = [BROWSER, "--headless=new", "--disable-gpu", "--no-first-run", + "--user-data-dir=" + folder, "--virtual-time-budget=15000", "--dump-dom", + "http://127.0.0.1:%d/update" % server.server_port] + if os.name != "nt" and hasattr(os, "geteuid") and os.geteuid() == 0: + command.insert(1, "--no-sandbox") + result = run_browser(command) + self.assertEqual(result.returncode, 0, result.stderr.decode(errors="replace")) + self.assertIn('data-test-busy="yes"', result.stdout.decode(errors="replace")) + self.assertIn('data-test-done="yes"', result.stdout.decode(errors="replace")) + self.assertIn('data-test-identity="test node · ESP32"', result.stdout.decode(errors="replace")) + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + digest = hashlib.md5(payload).hexdigest() + self.assertEqual(requests, [("/update", digest, [(name, name, payload)]) + for name in ("firmware", "filesystem", "firmware")]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_companion_headless_buttons.py b/test/test_companion_headless_buttons.py index f9a9019a1..0a23529a1 100644 --- a/test/test_companion_headless_buttons.py +++ b/test/test_companion_headless_buttons.py @@ -22,7 +22,7 @@ HARNESS = r''' #define UI_HAS_JOYSTICK 0 #define UI_WIFI_SETUP_HOME_PAGE 0 #define MESH_DEBUG_PRINTLN(...) -constexpr char KEY_ENTER='e', KEY_NEXT='n', KEY_PREV='p', KEY_SELECT='s', KEY_UP='u', KEY_DOWN='d'; +constexpr char KEY_ENTER='e', KEY_NEXT='n', KEY_PREV='p', KEY_SELECT='s', KEY_UP='u', KEY_DOWN='d', KEY_LEFT='l', KEY_RIGHT='r'; constexpr int BUTTON_EVENT_CLICK=1, BUTTON_EVENT_LONG_PRESS=2, BUTTON_EVENT_DOUBLE_CLICK=3; constexpr int BUTTON_EVENT_TRIPLE_CLICK=4, BUTTON_EVENT_QUADRUPLE_CLICK=5; uint32_t now_ms=0; @@ -156,6 +156,13 @@ void visible() { event(ui, BUTTON_EVENT_LONG_PRESS); assert(ui.reader_closes==1 && ui.curr==ui.home); ui.home_screen.radio_page=true; +#ifdef UI_HAS_NAV_INPUT + event(ui, BUTTON_EVENT_CLICK); + assert(ui.curr==ui.home && ui.reader_opens==0); +#endif + assert(ui.handleLongPress(KEY_LEFT)==KEY_LEFT); + assert(ui.handleLongPress(KEY_RIGHT)==KEY_RIGHT); + assert(ui.curr==ui.home && ui.reader_opens==0); event(ui, BUTTON_EVENT_LONG_PRESS); assert(ui.reader_opens==1 && ui.curr==ui.reader); #endif diff --git a/test/test_companion_no_wireless.py b/test/test_companion_no_wireless.py new file mode 100644 index 000000000..ce8eb0382 --- /dev/null +++ b/test/test_companion_no_wireless.py @@ -0,0 +1,132 @@ +"""Check the compact no-wireless Companion path against the full controller.""" + +from pathlib import Path +import os +import subprocess +import tempfile +import unittest + +from cpp_source import body + +ROOT = Path(__file__).resolve().parents[1] + +HARNESS = r''' +#include +#include "examples/companion_radio/CompanionWireless.h" +#include +#include +#include +#include + +struct Unavailable : mesh::wireless::Backend { + unsigned sets = 0; + uint8_t available() const override { return 0; } + uint8_t enabled() const override { return 0; } + uint8_t clients() const override { return 0; } + mesh::wireless::Result set(uint8_t, bool) override { + ++sets; + return mesh::wireless::Result::Failed; + } +}; + +@WRAPPER@ + +int main() { + using namespace mesh::wireless; + static Unavailable backend; + Control reference; + reference.begin(backend); + control().begin(backend); + const char* commands[] = { + "", "get wifi", "get espnow", "get 2.4ghz", "get wifi.status", + "set wifi on", "set wifi off", "set wifi off force", "set wifi on force", + "set espnow on", "set espnow off", "set espnow off force", + "set 2.4ghz on", "set 2.4ghz off", "set 2.4ghz off force", + "set 2.4ghz on all", "set 2.4ghz on force", "set 2.4ghz on all force", + " get wifi\t", "set\twifi on", "get wifi junk", "set wifi", + "set wifi off force extra", "set espnow.channel 3", "get bluetooth", + "set bluetooth off", "set 2.4ghzx on", "SET wifi on", "reset wifi" + }; + const CompanionWirelessSource sources[] = { + CompanionWirelessSource::Other, CompanionWirelessSource::Usb, + CompanionWirelessSource::Network, CompanionWirelessSource::Framed + }; + const uint8_t requesters[] = {0, Independent, WiFi, Bluetooth}; + for (const char* command : commands) for (size_t size = 0; size <= 160; ++size) + for (unsigned route = 0; route < 4; ++route) { + char expected[160], actual[160]; + memset(expected, 0x5a, sizeof(expected)); + memset(actual, 0x5a, sizeof(actual)); + const bool original = reference.handle(command, expected, size, 500, requesters[route]); + const bool compact = handleCompanionWirelessCommand(command, actual, size, sources[route]); + assert(original == compact); + assert(memcmp(expected, actual, sizeof(expected)) == 0); + reference.service(1000); + control().service(1000); + assert(!reference.pending() && !control().pending()); + assert(!reference.masterOff() && !control().masterOff()); + assert(!reference.blocked(All) && !control().blocked(All)); + assert(backend.sets == 0); + } + for (const char* command : {"get wifi", "set wifi on", "unknown"}) { + assert(reference.handle(command, nullptr, 160, 500, Independent) + == handleCompanionWirelessCommand(command, nullptr, 160, CompanionWirelessSource::Usb)); + } +} +''' + + +class NoWirelessCompanionTest(unittest.TestCase): + def test_stm32_wrapper_matches_full_controller(self): + wrapper = body((ROOT / "examples/companion_radio/main.cpp").read_text(), + "bool handleCompanionWirelessCommand(") + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "no_wireless.cpp" + source.write_text(HARNESS.replace("@WRAPPER@", wrapper)) + for compact in (0, 1): + binary = Path(directory) / f"check-{compact}" + command = [os.environ.get("CXX", "g++"), "-std=c++17", "-O1", + "-Wall", "-Wextra", "-Werror", "-DSTM32_PLATFORM=1", + f"-DMESH_CLI_COMPACT_REPLIES={compact}", + "-I", str(ROOT / "src"), "-I", str(ROOT), + str(source), "-o", str(binary)] + if os.name != "nt": + command[1:1] = ["-fsanitize=address,undefined", + "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"] + subprocess.run(command, check=True, timeout=60) + subprocess.run([str(binary)], check=True, timeout=30) + + def test_wireless_capabilities_keep_the_full_controller(self): + wrapper = body((ROOT / "examples/companion_radio/main.cpp").read_text(), + "bool handleCompanionWirelessCommand(") + with tempfile.TemporaryDirectory() as directory: + source = Path(directory) / "guard.cpp" + source.write_text('#include \n' + '#include "examples/companion_radio/CompanionWireless.h"\n' + + wrapper) + for flags, compact in ( + (["-DSTM32_PLATFORM=1"], True), + (["-DSTM32_PLATFORM=1", "-DMESH_PRIMARY_ESPNOW=0"], True), + (["-DSTM32_PLATFORM=1", "-DBLE_PIN_CODE=123456"], False), + (["-DSTM32_PLATFORM=1", "-DWIFI_SSID=1"], False), + (["-DSTM32_PLATFORM=1", "-DMESH_PRIMARY_ESPNOW=1"], False), + (["-DSTM32_PLATFORM=1", "-DWITH_BRIDGE=1"], False), + (["-DSTM32_PLATFORM=1", "-DWITH_MQTT_BRIDGE=1"], False), + (["-DSTM32_PLATFORM=1", "-DWITH_UDP_BRIDGE=1"], False), + (["-DSTM32_PLATFORM=1", "-DWITH_ESPNOW_BRIDGE=1"], False), + (["-DSTM32_PLATFORM=1", "-DWITH_WEBCONFIG=1"], False), + (["-DSTM32_PLATFORM=1", "-DETHERNET_ENABLED=1"], False), + (["-DSTM32_PLATFORM=1", "-DMESH_ENABLE_EMERGENCY_POWER=1"], False), + ([], False), + ): + with self.subTest(flags=flags): + result = subprocess.run( + [os.environ.get("CXX", "g++"), "-std=c++17", "-E", "-P", + *flags, "-I", str(ROOT / "src"), "-I", str(ROOT), str(source)], + check=True, capture_output=True, text=True, timeout=60) + self.assertEqual("return mesh::wireless::handleNoServices(" in result.stdout, compact) + self.assertEqual("return mesh::wireless::control().handle(" in result.stdout, not compact) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_companion_reader.py b/test/test_companion_reader.py index 7f162175e..0ed69a279 100644 --- a/test/test_companion_reader.py +++ b/test/test_companion_reader.py @@ -78,7 +78,7 @@ class CompanionReaderTest(unittest.TestCase): "#pragma once\n#include \n#include \n#include \n#include \n" "class Stream { public: virtual size_t write(const uint8_t*,size_t);size_t print(const char*); };\n") flags = ["-mcpu=cortex-m4", "-mthumb", "-Oz", "-fno-inline-small-functions", - "-fstack-usage", "-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", + "-fstack-usage", "-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DCOMPANION_FEATURE_READER=1", "-DENABLE_USB_INTERFACE=1", "-I" + str(directory), "-I" + str(ROOT / "src"), "-I" + str(FIXTURE)] if compact: @@ -239,7 +239,7 @@ class CompanionReaderTest(unittest.TestCase): with tempfile.TemporaryDirectory(prefix="meshcore-reader-") as directory: directory = Path(directory) for platform, compact in (("ESP32_PLATFORM", False), ("NRF52_PLATFORM", False), ("NRF52_PLATFORM", True)): - flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"] + flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"] if compact: flags.append("-DCOMPANION_READER_COMPACT_STREAM=1") obj, binary = directory / "tinf.o", directory / "reader.exe" @@ -264,7 +264,7 @@ class CompanionReaderTest(unittest.TestCase): self.skipTest("host G++ required") for platform, compact, expected in (("NRF52_PLATFORM", False, 3), ("NRF52_PLATFORM", True, 0), ("ESP32_PLATFORM", True, None)): - flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"] + flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"] if compact: flags.append("-DCOMPANION_READER_COMPACT_STREAM=1") result = self.run_checked([cxx, "-E", "-P", *flags, @@ -280,6 +280,8 @@ class CompanionReaderTest(unittest.TestCase): self.skipTest("host G++ required") for flags in ([], ["-DESP32_PLATFORM=1"], ["-DNRF52_PLATFORM=1"], ["-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1"], + ["-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"], + ["-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"], ["-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=0"]): result = self.run_checked([cxx, "-E", "-P", *flags, @@ -448,7 +450,7 @@ int main() { generated = directory / "dispatch.cpp" generated.write_text(harness) for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM"): - flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"] + flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"] obj, binary = directory / "tinf.o", directory / "dispatch.exe" self.run_checked([cc, "-std=c11", "-Os", *flags, "-c", str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(obj)]) @@ -465,7 +467,7 @@ int main() { self.skipTest("host GCC and G++ required") with tempfile.TemporaryDirectory(prefix="meshcore-reader-reader-") as directory: directory = Path(directory) - flags = ["-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"] + flags = ["-DNRF52_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"] obj, binary = directory / "tinf.o", directory / "reader.exe" self.run_checked([cc, "-Os", *flags, "-c", str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(obj)]) @@ -582,7 +584,7 @@ int main() { generated = directory / "interlock.cpp" generated.write_text(harness) for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM"): - flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1"] + flags = [f"-D{platform}=1", "-DCOMPANION_RADIO_FULL=1", "-DENABLE_USB_INTERFACE=1", "-DCOMPANION_FEATURE_READER=1"] obj, binary = directory / "tinf.o", directory / "interlock.exe" self.run_checked([cc, "-std=c11", "-Os", *flags, "-c", str(ROOT / "src/helpers/ota/OtaTinf.c"), "-o", str(obj)]) @@ -594,7 +596,11 @@ int main() { def test_reader_button_routing_and_profile_gate(self): ui = (ROOT / "examples/companion_radio/ui-new/UITask.cpp").read_text(encoding="utf-8") - long_press = ui[ui.index("char UITask::handleLongPress("):] + home = extract_braced(ui, "class HomeScreen") + short_press = extract_braced(home, "bool handleInput(char c) override") + self.assertNotIn("showReader(", short_press) + long_press = extract_braced(ui, "char UITask::handleLongPress(") + self.assertRegex(long_press, r"if\s*\(c\s*==\s*KEY_ENTER[\s\S]*?isRadioPage\(\)\)\s*\{\s*showReader\(\)") self.assertLess(long_press.index("isReaderActive()"), long_press.index("enterCLIRescue()")) self.assertLess(long_press.index("isRadioPage()"), long_press.index("enterCLIRescue()")) self.assertIn("#if COMPANION_FEATURE_READER\n#include \"ReaderScreen.h\"", ui) diff --git a/test/test_lr2021_calibration.py b/test/test_lr2021_calibration.py index 38b9e33fe..3d541e1bf 100644 --- a/test/test_lr2021_calibration.py +++ b/test/test_lr2021_calibration.py @@ -2,12 +2,13 @@ from pathlib import Path import os +import re import shutil import subprocess import tempfile import unittest -from cpp_source import body +from cpp_source import body, _uncommented from test_display_power import FS_STUB @@ -349,9 +350,11 @@ class LR2021CalibrationTests(unittest.TestCase): def test_all_cli_roles_load_before_init_and_service_pending(self): for role in ("simple_repeater", "simple_room_server", "simple_sensor", "companion_radio"): source = (ROOT / "examples" / role / "main.cpp").read_text() - setup = body(source, "void setup()") + setup = _uncommented(body(source, "void setup()")) with self.subTest(role=role): - self.assertLess(setup.index("loadLR2021CalibrationSettings("), setup.index("radio_init()")) + init = re.search(r"\bwhile\s*\([^\n;]*\bradio_init\s*\(\s*\)", setup) + self.assertIsNotNone(init, "startup must initialize the radio") + self.assertLess(setup.index("loadLR2021CalibrationSettings("), init.start()) self.assertIn("loopLR2021CalibrationSettings(radio_driver)", source) self.assertIn("#if defined(USE_LR2021)", source) diff --git a/test/test_pocket_reader.py b/test/test_pocket_reader.py index dad6a82d9..1be14c0ac 100644 --- a/test/test_pocket_reader.py +++ b/test/test_pocket_reader.py @@ -31,7 +31,7 @@ class PocketReaderTest(unittest.TestCase): self.assertIsNotNone(font) selected = directory / "font.c" selected.write_text('#include "u8g2_fonts.h"\n' + font.group()) - flags = ["-g", "-O1", "-DHELTEC_MESH_POCKET=1", "-DCOMPANION_RADIO_FULL=1", + flags = ["-g", "-O1", "-DHELTEC_MESH_POCKET=1", "-DCOMPANION_RADIO_FULL=1", "-DCOMPANION_FEATURE_READER=1", "-DNRF52_PLATFORM=1", "-DENABLE_USB_INTERFACE=1", "-DUI_POCKET_GUI=1", "-DUI_SMALL_MESSAGE_FONT=0", "-DUI_BUTTON_READER_HINT=1", "-DUI_READER_TOUCH_BAR=0", "-I" + str(FIXTURE), "-I" + str(ROOT / "src"), "-I" + str(ROOT), diff --git a/test/test_stm32_wireless_specialization.py b/test/test_stm32_wireless_specialization.py new file mode 100644 index 000000000..8b19a5a7c --- /dev/null +++ b/test/test_stm32_wireless_specialization.py @@ -0,0 +1,46 @@ +"""The STM32WL no-service shortcut must not mask any wireless owner.""" +from pathlib import Path +import shutil +import subprocess +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class Stm32WirelessSpecializationTest(unittest.TestCase): + def test_only_plain_stm32wl_uses_no_service_handler(self): + compiler = shutil.which("gcc") + if not compiler: + self.skipTest("host GCC required") + source = (ROOT / "src/helpers/CommonCLI.cpp").read_text() + anchor = source.index("if (handleBeaconCommand(command, reply, 157))") + start = source.index("#if defined(STM32_PLATFORM) && defined(STM32WLxx)", anchor) + end = source.index("#endif", start) + len("#endif") + branch = source[start:end] + base = ("STM32_PLATFORM", "STM32WLxx", "USE_STM32WL") + cases = [(base, True), + (base[:-1], False), + (("ESP32_PLATFORM",), False), + (base + ("WITH_WEBCONFIG",), False), + (base + ("WITH_MQTT_BRIDGE",), False), + (base + ("WITH_UDP_BRIDGE",), False), + (base + ("WITH_ESPNOW_BRIDGE",), False), + (base + ("WITH_BRIDGE",), False), + (base + ("ETHERNET_ENABLED",), False), + (base + ("BLE_PIN_CODE=123456",), False), + (base + ("WIFI_SSID=custom",), False), + (base + ("MESH_PRIMARY_ESPNOW=1",), False), + (base + ("MESH_ENABLE_EMERGENCY_POWER=1",), False)] + for defines, expected in cases: + with self.subTest(defines=defines): + result = subprocess.run([compiler, "-E", "-P", "-x", "c", "-", + *("-D" + value for value in defines)], + input=branch, text=True, capture_output=True, + check=True) + self.assertEqual("handleNoServices" in result.stdout, expected) + self.assertEqual("control().handle" in result.stdout, not expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_webconfig_gzip.py b/test/test_webconfig_gzip.py index f2379ad23..d98d7b7c2 100644 --- a/test/test_webconfig_gzip.py +++ b/test/test_webconfig_gzip.py @@ -50,7 +50,12 @@ class WebConfigGzipTest(unittest.TestCase): self.assertIn("gzip, 1000 iterations", header) blobs = arrays(header) for page in (ROOT / "webui/pages").glob("*.html"): - self.assertEqual(gzip.decompress(blobs["MESH_HTML_" + page.stem.upper()]), page.read_bytes()) + expected = page.read_bytes() + if page.name == "elegant_ota_compact.html": + marker = b"/* MESH_SPARK_MD5 */" + self.assertEqual(expected.count(marker), 1) + expected = expected.replace(marker, (ROOT / "webui/vendor/spark-md5-3.0.2.min.js").read_bytes()) + self.assertEqual(gzip.decompress(blobs["MESH_HTML_" + page.stem.upper()]), expected) def test_loader_matches_legacy_android_browser_syntax(self): blobs = arrays((ROOT / "src/helpers/esp32/WebConfigHtml.h").read_text()) diff --git a/tools/bible/README.md b/tools/bible/README.md index 56e6ad2f2..b7c636fe0 100644 --- a/tools/bible/README.md +++ b/tools/bible/README.md @@ -1,7 +1,9 @@ # Offline Reader lookup -ESP32 and nRF52 **Full Companion** USB terminals (and the ESP32 TCP terminal) -include an embedded 879-verse corpus from the World English Bible: +Reader is disabled in standard Companion builds to preserve flash space. A +custom ESP32 or nRF52 **Full Companion** USB build can enable it with +`COMPANION_FEATURE_READER=1`. The USB terminal (and ESP32 TCP terminal) then +includes an embedded 879-verse corpus from the World English Bible: ```text get reader 3:16 @@ -23,7 +25,7 @@ last verse of a range. An optional page number refers to the first verse in that command: `get reader 21:17 2`. No radio request can access the reader. No WiFi, phone, API key, filesystem space or bootloader update is needed. Reduced/ordinary Companion and other -roles omit the corpus. `COMPANION_FEATURE_READER=0` excludes it from a Full build. +roles omit the corpus; the opt-in requires a Full Companion USB build. ## On-device Reader diff --git a/variants/lilygo_tbeam_universal/platformio.ini b/variants/lilygo_tbeam_universal/platformio.ini index 50cb07b9a..1f585e434 100644 --- a/variants/lilygo_tbeam_universal/platformio.ini +++ b/variants/lilygo_tbeam_universal/platformio.ini @@ -158,6 +158,7 @@ build_flags = -fno-partial-inlining -mno-target-align -D MESH_UNIVERSAL_LTO=1 + -D MESH_COMPACT_ELEGANT_OTA=1 ; full authenticated OTA protocol, smaller page -D ADVERT_NAME='"T-Beam MQTT Observer"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 @@ -201,6 +202,7 @@ build_flags = -fno-partial-inlining -mno-target-align -D MESH_UNIVERSAL_LTO=1 + -D MESH_COMPACT_ELEGANT_OTA=1 ; full authenticated OTA protocol, smaller page -D ADVERT_NAME='"T-Beam Room Observer"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 diff --git a/webui/pages/elegant_ota_compact.html b/webui/pages/elegant_ota_compact.html new file mode 100644 index 000000000..6bb11289e --- /dev/null +++ b/webui/pages/elegant_ota_compact.html @@ -0,0 +1,53 @@ + + +MeshCore OTA +

MeshCore OTA

Reading device identity…

+
Update type + +
+

+ +

+ diff --git a/webui/vendor/README.md b/webui/vendor/README.md new file mode 100644 index 000000000..71da273c3 --- /dev/null +++ b/webui/vendor/README.md @@ -0,0 +1,7 @@ +`spark-md5-3.0.2.min.js` is SparkMD5 3.0.2 by André Cruz, from +https://github.com/satazor/js-spark-md5/tree/v3.0.2. Its package metadata +offers either WTFPL or MIT; MeshCore uses the MIT option. The upstream WTFPL +license is also retained alongside the file. + +SHA-256: `d80e84c820cc5587a0ba3c8a20652099ea3fa7fc43944e812e56d449c1d9f1c9`. +The OTA page embeds this file locally; it never downloads scripts at runtime. diff --git a/webui/vendor/spark-md5-3.0.2.LICENSE.txt b/webui/vendor/spark-md5-3.0.2.LICENSE.txt new file mode 100644 index 000000000..20553dda3 --- /dev/null +++ b/webui/vendor/spark-md5-3.0.2.LICENSE.txt @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2015 André Cruz + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/webui/vendor/spark-md5-3.0.2.MIT.txt b/webui/vendor/spark-md5-3.0.2.MIT.txt new file mode 100644 index 000000000..de62c9482 --- /dev/null +++ b/webui/vendor/spark-md5-3.0.2.MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 André Cruz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/webui/vendor/spark-md5-3.0.2.min.js b/webui/vendor/spark-md5-3.0.2.min.js new file mode 100644 index 000000000..2ef527dd4 --- /dev/null +++ b/webui/vendor/spark-md5-3.0.2.min.js @@ -0,0 +1 @@ +(function(factory){if(typeof exports==="object"){module.exports=factory()}else if(typeof define==="function"&&define.amd){define(factory)}else{var glob;try{glob=window}catch(e){glob=self}glob.SparkMD5=factory()}})(function(undefined){"use strict";var add32=function(a,b){return a+b&4294967295},hex_chr=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];function cmn(q,a,b,x,s,t){a=add32(add32(a,q),add32(x,t));return add32(a<>>32-s,b)}function md5cycle(x,k){var a=x[0],b=x[1],c=x[2],d=x[3];a+=(b&c|~b&d)+k[0]-680876936|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[1]-389564586|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[2]+606105819|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[3]-1044525330|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[4]-176418897|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[5]+1200080426|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[6]-1473231341|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[7]-45705983|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[8]+1770035416|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[9]-1958414417|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[10]-42063|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[11]-1990404162|0;b=(b<<22|b>>>10)+c|0;a+=(b&c|~b&d)+k[12]+1804603682|0;a=(a<<7|a>>>25)+b|0;d+=(a&b|~a&c)+k[13]-40341101|0;d=(d<<12|d>>>20)+a|0;c+=(d&a|~d&b)+k[14]-1502002290|0;c=(c<<17|c>>>15)+d|0;b+=(c&d|~c&a)+k[15]+1236535329|0;b=(b<<22|b>>>10)+c|0;a+=(b&d|c&~d)+k[1]-165796510|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[6]-1069501632|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[11]+643717713|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[0]-373897302|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[5]-701558691|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[10]+38016083|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[15]-660478335|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[4]-405537848|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[9]+568446438|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[14]-1019803690|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[3]-187363961|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[8]+1163531501|0;b=(b<<20|b>>>12)+c|0;a+=(b&d|c&~d)+k[13]-1444681467|0;a=(a<<5|a>>>27)+b|0;d+=(a&c|b&~c)+k[2]-51403784|0;d=(d<<9|d>>>23)+a|0;c+=(d&b|a&~b)+k[7]+1735328473|0;c=(c<<14|c>>>18)+d|0;b+=(c&a|d&~a)+k[12]-1926607734|0;b=(b<<20|b>>>12)+c|0;a+=(b^c^d)+k[5]-378558|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[8]-2022574463|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[11]+1839030562|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[14]-35309556|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[1]-1530992060|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[4]+1272893353|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[7]-155497632|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[10]-1094730640|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[13]+681279174|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[0]-358537222|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[3]-722521979|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[6]+76029189|0;b=(b<<23|b>>>9)+c|0;a+=(b^c^d)+k[9]-640364487|0;a=(a<<4|a>>>28)+b|0;d+=(a^b^c)+k[12]-421815835|0;d=(d<<11|d>>>21)+a|0;c+=(d^a^b)+k[15]+530742520|0;c=(c<<16|c>>>16)+d|0;b+=(c^d^a)+k[2]-995338651|0;b=(b<<23|b>>>9)+c|0;a+=(c^(b|~d))+k[0]-198630844|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[7]+1126891415|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[14]-1416354905|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[5]-57434055|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[12]+1700485571|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[3]-1894986606|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[10]-1051523|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[1]-2054922799|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[8]+1873313359|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[15]-30611744|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[6]-1560198380|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[13]+1309151649|0;b=(b<<21|b>>>11)+c|0;a+=(c^(b|~d))+k[4]-145523070|0;a=(a<<6|a>>>26)+b|0;d+=(b^(a|~c))+k[11]-1120210379|0;d=(d<<10|d>>>22)+a|0;c+=(a^(d|~b))+k[2]+718787259|0;c=(c<<15|c>>>17)+d|0;b+=(d^(c|~a))+k[9]-343485551|0;b=(b<<21|b>>>11)+c|0;x[0]=a+x[0]|0;x[1]=b+x[1]|0;x[2]=c+x[2]|0;x[3]=d+x[3]|0}function md5blk(s){var md5blks=[],i;for(i=0;i<64;i+=4){md5blks[i>>2]=s.charCodeAt(i)+(s.charCodeAt(i+1)<<8)+(s.charCodeAt(i+2)<<16)+(s.charCodeAt(i+3)<<24)}return md5blks}function md5blk_array(a){var md5blks=[],i;for(i=0;i<64;i+=4){md5blks[i>>2]=a[i]+(a[i+1]<<8)+(a[i+2]<<16)+(a[i+3]<<24)}return md5blks}function md51(s){var n=s.length,state=[1732584193,-271733879,-1732584194,271733878],i,length,tail,tmp,lo,hi;for(i=64;i<=n;i+=64){md5cycle(state,md5blk(s.substring(i-64,i)))}s=s.substring(i-64);length=s.length;tail=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(i=0;i>2]|=s.charCodeAt(i)<<(i%4<<3)}tail[i>>2]|=128<<(i%4<<3);if(i>55){md5cycle(state,tail);for(i=0;i<16;i+=1){tail[i]=0}}tmp=n*8;tmp=tmp.toString(16).match(/(.*?)(.{0,8})$/);lo=parseInt(tmp[2],16);hi=parseInt(tmp[1],16)||0;tail[14]=lo;tail[15]=hi;md5cycle(state,tail);return state}function md51_array(a){var n=a.length,state=[1732584193,-271733879,-1732584194,271733878],i,length,tail,tmp,lo,hi;for(i=64;i<=n;i+=64){md5cycle(state,md5blk_array(a.subarray(i-64,i)))}a=i-64>2]|=a[i]<<(i%4<<3)}tail[i>>2]|=128<<(i%4<<3);if(i>55){md5cycle(state,tail);for(i=0;i<16;i+=1){tail[i]=0}}tmp=n*8;tmp=tmp.toString(16).match(/(.*?)(.{0,8})$/);lo=parseInt(tmp[2],16);hi=parseInt(tmp[1],16)||0;tail[14]=lo;tail[15]=hi;md5cycle(state,tail);return state}function rhex(n){var s="",j;for(j=0;j<4;j+=1){s+=hex_chr[n>>j*8+4&15]+hex_chr[n>>j*8&15]}return s}function hex(x){var i;for(i=0;i>16)+(y>>16)+(lsw>>16);return msw<<16|lsw&65535}}if(typeof ArrayBuffer!=="undefined"&&!ArrayBuffer.prototype.slice){(function(){function clamp(val,length){val=val|0||0;if(val<0){return Math.max(val+length,0)}return Math.min(val,length)}ArrayBuffer.prototype.slice=function(from,to){var length=this.byteLength,begin=clamp(from,length),end=length,num,target,targetArray,sourceArray;if(to!==undefined){end=clamp(to,length)}if(begin>end){return new ArrayBuffer(0)}num=end-begin;target=new ArrayBuffer(num);targetArray=new Uint8Array(target);sourceArray=new Uint8Array(this,begin,num);targetArray.set(sourceArray);return target}})()}function toUtf8(str){if(/[\u0080-\uFFFF]/.test(str)){str=unescape(encodeURIComponent(str))}return str}function utf8Str2ArrayBuffer(str,returnUInt8Array){var length=str.length,buff=new ArrayBuffer(length),arr=new Uint8Array(buff),i;for(i=0;i>2]|=buff.charCodeAt(i)<<(i%4<<3)}this._finish(tail,length);ret=hex(this._hash);if(raw){ret=hexToBinaryString(ret)}this.reset();return ret};SparkMD5.prototype.reset=function(){this._buff="";this._length=0;this._hash=[1732584193,-271733879,-1732584194,271733878];return this};SparkMD5.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash.slice()}};SparkMD5.prototype.setState=function(state){this._buff=state.buff;this._length=state.length;this._hash=state.hash;return this};SparkMD5.prototype.destroy=function(){delete this._hash;delete this._buff;delete this._length};SparkMD5.prototype._finish=function(tail,length){var i=length,tmp,lo,hi;tail[i>>2]|=128<<(i%4<<3);if(i>55){md5cycle(this._hash,tail);for(i=0;i<16;i+=1){tail[i]=0}}tmp=this._length*8;tmp=tmp.toString(16).match(/(.*?)(.{0,8})$/);lo=parseInt(tmp[2],16);hi=parseInt(tmp[1],16)||0;tail[14]=lo;tail[15]=hi;md5cycle(this._hash,tail)};SparkMD5.hash=function(str,raw){return SparkMD5.hashBinary(toUtf8(str),raw)};SparkMD5.hashBinary=function(content,raw){var hash=md51(content),ret=hex(hash);return raw?hexToBinaryString(ret):ret};SparkMD5.ArrayBuffer=function(){this.reset()};SparkMD5.ArrayBuffer.prototype.append=function(arr){var buff=concatenateArrayBuffers(this._buff.buffer,arr,true),length=buff.length,i;this._length+=arr.byteLength;for(i=64;i<=length;i+=64){md5cycle(this._hash,md5blk_array(buff.subarray(i-64,i)))}this._buff=i-64>2]|=buff[i]<<(i%4<<3)}this._finish(tail,length);ret=hex(this._hash);if(raw){ret=hexToBinaryString(ret)}this.reset();return ret};SparkMD5.ArrayBuffer.prototype.reset=function(){this._buff=new Uint8Array(0);this._length=0;this._hash=[1732584193,-271733879,-1732584194,271733878];return this};SparkMD5.ArrayBuffer.prototype.getState=function(){var state=SparkMD5.prototype.getState.call(this);state.buff=arrayBuffer2Utf8Str(state.buff);return state};SparkMD5.ArrayBuffer.prototype.setState=function(state){state.buff=utf8Str2ArrayBuffer(state.buff,true);return SparkMD5.prototype.setState.call(this,state)};SparkMD5.ArrayBuffer.prototype.destroy=SparkMD5.prototype.destroy;SparkMD5.ArrayBuffer.prototype._finish=SparkMD5.prototype._finish;SparkMD5.ArrayBuffer.hash=function(arr,raw){var hash=md51_array(new Uint8Array(arr)),ret=hex(hash);return raw?hexToBinaryString(ret):ret};return SparkMD5});