From 97cc4efa897af3ccb52314a962d24663ffcc69d0 Mon Sep 17 00:00:00 2001 From: odudex Date: Wed, 8 Jul 2026 11:28:29 -0300 Subject: [PATCH 1/5] components: add cUR submodule Add cUR (https://github.com/odudex/cUR), a pure-C implementation of BCR-2020-005 UR encoding, pinned at the head of its main branch. No other changes - the source migration follows in the next commit. --- .gitmodules | 3 +++ components/cUR | 1 + 2 files changed, 4 insertions(+) create mode 160000 components/cUR diff --git a/.gitmodules b/.gitmodules index ea471b674..62667ea86 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "components/esp32_bc-ur"] path = components/esp32_bc-ur url = https://github.com/Blockstream/esp32_bc-ur +[submodule "components/cUR"] + path = components/cUR + url = https://github.com/odudex/cUR diff --git a/components/cUR b/components/cUR new file mode 160000 index 000000000..87c301467 --- /dev/null +++ b/components/cUR @@ -0,0 +1 @@ +Subproject commit 87c301467b9370ab5cb005739c9b50a1a48f8efe From c445b83a38b3aea21497245d0d7379afd0397fe4 Mon Sep 17 00:00:00 2001 From: odudex Date: Wed, 8 Jul 2026 11:29:33 -0300 Subject: [PATCH 2/5] ur: use cUR instead of esp32_bc-ur Move the UR transport layer from the C++20 bc-ur library (and its C shim with placement-new sizing constants) to cUR, a pure-C implementation of BCR-2020-005, used as the UR transport envelope only - all payload CBOR remains TinyCBOR in bcur.c. - main/bcur.c and main/selfcheck.c moved to the cUR API: heap encoder/decoder handles, results borrowed from the decoder, fragments freed with free(). Encoder output is always uppercase. - collect_any_bcur() now replaces the decoder via qr_data->ctx on hard failure, and bcur_scan_qr() re-reads it after scanning. - selfcheck: a duplicate part is deduped before being counted, so the 'processed parts' expectation differs when the same part is presented twice. - Build with UR_ENVELOPE_ONLY to skip cUR's payload-type codecs; libjade links the same component's host static lib (bundled SHA-256, no mbedcrypto/wally dependencies). The esp32_bc-ur component is left in place (now unused) so it can be removed separately once the migration has settled. --- CMakeLists.txt | 3 + libjade/CMakeLists.txt | 15 ++-- main/CMakeLists.txt | 2 +- main/bcur.c | 113 +++++++++++++----------- main/process/mnemonic.c | 1 - main/selfcheck.c | 189 ++++++++++++++++------------------------ 6 files changed, 147 insertions(+), 176 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 377c3e4ef..cae593fc4 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.16) if(DEFINED ESP_PLATFORM AND ESP_PLATFORM EQUAL 1) set(EXTRA_COMPONENT_DIRS bootloader_components/bootloader_support) + # Jade does its own payload CBOR (TinyCBOR) - only the UR transport + # layer of the cUR component is needed + set(UR_ENVELOPE_ONLY ON CACHE BOOL "" FORCE) include($ENV{IDF_PATH}/tools/cmake/project.cmake) idf_build_set_property(DEPENDENCIES_LOCK dependencies.lock.${IDF_TARGET}) project(jade) diff --git a/libjade/CMakeLists.txt b/libjade/CMakeLists.txt index fffa9e405..0b97bd91f 100644 --- a/libjade/CMakeLists.txt +++ b/libjade/CMakeLists.txt @@ -105,14 +105,15 @@ set(COMMON_SRC ${IDF_PATH}/components/http_parser/http_parser.c ) -add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../components/esp32_bc-ur - ${CMAKE_CURRENT_BINARY_DIR}/bcur) -target_include_directories(bcur PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../components/libwally-core/upstream/include) -target_link_libraries(bcur PRIVATE mbedcrypto) +# Jade does its own payload CBOR (TinyCBOR) - only the UR transport +# layer of the cUR component is needed +set(UR_ENVELOPE_ONLY ON CACHE BOOL "" FORCE) +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../components/cUR + ${CMAKE_CURRENT_BINARY_DIR}/cur) # libjade.so add_library(jade SHARED ${COMMON_SRC}) -target_link_libraries(jade PRIVATE m z bcur cbor_target otpauth_migrate_target mbedtls mbedcrypto mbedx509) +target_link_libraries(jade PRIVATE m z ur cbor_target otpauth_migrate_target mbedtls mbedcrypto mbedx509) target_include_directories(jade PRIVATE ${COMMON_INCLUDES}) set_source_files_properties(libjade.c PROPERTIES COMPILE_OPTIONS @@ -121,12 +122,12 @@ target_compile_definitions(jade PRIVATE LIBJADE_BUILD) # libjade.a add_library(jade_static STATIC ${COMMON_SRC}) -target_link_libraries(jade_static PRIVATE m z mbedtls mbedcrypto mbedx509 bcur) +target_link_libraries(jade_static PRIVATE m z mbedtls mbedcrypto mbedx509 ur) target_include_directories(jade_static PRIVATE ${COMMON_INCLUDES}) target_compile_definitions(jade_static PRIVATE LIBJADE_BUILD) add_executable(libjade_daemon daemon.c) -target_link_libraries(libjade_daemon PRIVATE jade_static bcur cbor_target otpauth_migrate_target m z mbedtls mbedcrypto mbedx509 Threads::Threads) +target_link_libraries(libjade_daemon PRIVATE jade_static ur cbor_target otpauth_migrate_target m z mbedtls mbedcrypto mbedx509 Threads::Threads) target_include_directories(libjade_daemon PRIVATE ${COMMON_INCLUDES}) # Assets diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 724e0126e..82f265a49 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -52,7 +52,7 @@ idf_component_register(SRC_DIRS "." "${usbdir}" "${wallydirs}" "${secpdir}" - PRIV_REQUIRES assets libwally-core libsodium esp32-rotary-encoder esp32-quirc bootloader_support app_update nvs_flash bt autogenlang cbor esp_netif esp32_bsdiff esp32_deflate nghttp esp32_bc-ur driver mbedtls http_parser esp_hw_support efuse esp_eth esp_http_server esp_lcd usb vfs app_trace spi_flash google-otpauth-migrate ${extra_priv_requires} + PRIV_REQUIRES assets libwally-core libsodium esp32-rotary-encoder esp32-quirc bootloader_support app_update nvs_flash bt autogenlang cbor esp_netif esp32_bsdiff esp32_deflate nghttp cUR driver mbedtls http_parser esp_hw_support efuse esp_eth esp_http_server esp_lcd usb vfs app_trace spi_flash google-otpauth-migrate ${extra_priv_requires} EMBED_FILES ${PROJECT_DIR}/pinserver_public_key.pub ${logo_files} ${qemu_display_file}) if(CONFIG_ETH_USE_OPENETH) diff --git a/main/bcur.c b/main/bcur.c index 0044fd9ab..fd2ab1f34 100644 --- a/main/bcur.c +++ b/main/bcur.c @@ -11,8 +11,8 @@ #include "utils/util.h" #include -#include -#include +#include +#include // PSBT serialisation functions bool deserialise_psbt(const uint8_t* bytes, size_t bytes_len, struct wally_psbt** psbt_out); @@ -181,25 +181,27 @@ bool bcur_parse_bip39_wrapper( // Decode bcur string bool ret = false; - uint8_t decoder[URDECODER_SIZE]; - urcreate_placement_decoder(decoder, sizeof(decoder)); - if (!urreceive_part_decoder(decoder, bcur) || !uris_success_decoder(decoder)) { + ur_decoder_t* const decoder = ur_decoder_new(); + JADE_ASSERT(decoder); + if (ur_decoder_receive_part(decoder, bcur) != UR_DECODER_OK) { JADE_LOGW("Unable to decode bcur bip39 string from single part"); goto cleanup; } - // Read the result - char const* type = NULL; - uint8_t* result = NULL; - size_t result_len = 0; - urresult_ur_decoder(decoder, &result, &result_len, &type); - if (!type || !result || !result_len || strcasecmp(BCUR_TYPE_CRYPTO_BIP39, type)) { + // Read the result - borrowed from the decoder, freed with it + // NOTE: guaranteed non-NULL and populated when the decoder state is UR_DECODER_OK + const ur_result_t* const result = ur_decoder_get_result(decoder); + JADE_ASSERT(result); + JADE_ASSERT(result->cbor_data); + JADE_ASSERT(result->cbor_len); + JADE_ASSERT(result->type); + if (strcasecmp(BCUR_TYPE_CRYPTO_BIP39, result->type)) { JADE_LOGW("Unable to decode bcur bip39 string to expected type %s", BCUR_TYPE_CRYPTO_BIP39); goto cleanup; } // Decode the cbor - if (!bcur_parse_bip39(result, result_len, mnemonic, mnemonic_len, written)) { + if (!bcur_parse_bip39(result->cbor_data, result->cbor_len, mnemonic, mnemonic_len, written)) { JADE_LOGW("Failed to parse bcur bip39 cbor message"); goto cleanup; } @@ -208,7 +210,7 @@ bool bcur_parse_bip39_wrapper( ret = true; cleanup: - urfree_placement_decoder(decoder); + ur_decoder_free(decoder); return ret; } @@ -626,22 +628,25 @@ static bool collect_any_bcur(qr_data_t* qr_data) // The scanned data looks like a bcur code or fragment, add it to the bcur decoder // and return true only when the bcur decoder says the message is complete. - const bool processed_part = urreceive_part_decoder(qr_data->ctx, (const char*)qr_data->data); + const ur_decoder_state_t state = ur_decoder_receive_part(qr_data->ctx, (const char*)qr_data->data); - // On hard failure, reset the decoder - if (uris_failure_decoder(qr_data->ctx)) { + // On hard failure (terminal but unsuccessful - eg. checksum mismatch), reset the decoder + // NOTE: transient errors (eg. a misread frame) are not terminal - keep feeding parts + // NOTE: the caller must fetch the decoder back from qr_data->ctx as it may be replaced here + if (ur_decoder_state_is_terminal(state) && state != UR_DECODER_OK) { JADE_LOGE("Failure to scan bcur data - resetting the decoder"); - urfree_placement_decoder(qr_data->ctx); - urcreate_placement_decoder(qr_data->ctx, URDECODER_SIZE); + ur_decoder_free(qr_data->ctx); + qr_data->ctx = ur_decoder_new(); + JADE_ASSERT(qr_data->ctx); return false; } // Update associated progress bar - be a bit defensive here - const bool decoded = uris_success_decoder(qr_data->ctx); - const size_t nreceived = urreceived_parts_count_decoder(qr_data->ctx); - if (processed_part && nreceived) { + const bool decoded = state == UR_DECODER_OK; + const size_t nreceived = ur_decoder_received_parts_count(qr_data->ctx); + if (!ur_decoder_state_is_error(state) && nreceived) { // NOTE: can only call 'expected' once we have received at least one part - const size_t nexpected = urexpected_part_count_decoder(qr_data->ctx); + const size_t nexpected = ur_decoder_expected_part_count(qr_data->ctx); // If fully decoded show full bar - but if not fully decoded // don't show a full bar - pause at 'almost done' if required. @@ -666,34 +671,37 @@ bool bcur_scan_qr(const char* prompt_text, char** output_type, uint8_t** output, JADE_INIT_OUT_PPTR(output); JADE_INIT_OUT_SIZE(output_len); - uint8_t urdecoder[URDECODER_SIZE]; - urcreate_placement_decoder(urdecoder, sizeof(urdecoder)); + ur_decoder_t* urdecoder = ur_decoder_new(); + JADE_ASSERT(urdecoder); progress_bar_t progress_bar = {}; qr_data_t qr_data = { .len = 0, .is_valid = collect_any_bcur, .ctx = urdecoder, .progress_bar = &progress_bar }; // Scan qr code using the bcur decoder to collate multiple frames if required - if (!jade_camera_scan_qr(&qr_data, prompt_text, QR_GUIDE_SHOW, help_url)) { + // NOTE: collect_any_bcur() may replace the decoder (on hard failure), so + // refresh the local pointer from qr_data.ctx once scanning ends. + const bool scanned = jade_camera_scan_qr(&qr_data, prompt_text, QR_GUIDE_SHOW, help_url); + urdecoder = qr_data.ctx; + if (!scanned) { // User exited without completing scanning - urfree_placement_decoder(urdecoder); + ur_decoder_free(urdecoder); return false; } // Copy output into output params - caller takes ownership - if (uris_success_decoder(urdecoder)) { + if (ur_decoder_get_state(urdecoder) == UR_DECODER_OK) { // bcur message scanned - extract from decoder and return the payload - uint8_t* result = NULL; - size_t result_len = 0; - const char* result_type = NULL; - urresult_ur_decoder(urdecoder, &result, &result_len, &result_type); + // NOTE: the result is borrowed from the decoder, and freed with it + const ur_result_t* const result = ur_decoder_get_result(urdecoder); JADE_ASSERT(result); - JADE_ASSERT(result_len); - JADE_ASSERT(result_type); + JADE_ASSERT(result->cbor_data); + JADE_ASSERT(result->cbor_len); + JADE_ASSERT(result->type); // Copy payload and bc-ur type - *output = JADE_MALLOC_PREFER_SPIRAM(result_len + offset); - memcpy(*output + offset, result, result_len); - *output_len = result_len + offset; - *output_type = strdup(result_type); + *output = JADE_MALLOC_PREFER_SPIRAM(result->cbor_len + offset); + memcpy(*output + offset, result->cbor_data, result->cbor_len); + *output_len = result->cbor_len + offset; + *output_type = strdup(result->type); } else { // Not a bc-ur code - copy straight payload and append a nul-terminator. // Leave bc-ur type as NULL to indicate data was not a bc-ur payload. @@ -705,7 +713,7 @@ bool bcur_scan_qr(const char* prompt_text, char** output_type, uint8_t** output, } // Free the decoder and return true (as we scanned data successfully) - urfree_placement_decoder(urdecoder); + ur_decoder_free(urdecoder); return true; } @@ -741,9 +749,9 @@ void bcur_create_qr_icons(const uint8_t* payload, const size_t len, const char* JADE_LOGI("BC-UR encoding payload length %u as type %s", len, bcur_type); JADE_LOGI("Targetting qr-code version %u, capacity %u (alphanumeric mode), using max fragment size %u", qr_version, qrcode_alphanumeric_capacity, bcur_max_fragment_size); - uint8_t encoder[URENCODER_SIZE]; - urcreate_placement_encoder(encoder, sizeof(encoder), bcur_type, payload, len, bcur_max_fragment_size, 0, 8); - const size_t min_num_fragments = urseqlen_encoder(encoder); // the number of 'pure' data fragments + ur_encoder_t* const encoder = ur_encoder_new(bcur_type, payload, len, bcur_max_fragment_size, 0, 8); + JADE_ASSERT(encoder); + const size_t min_num_fragments = ur_encoder_seq_len(encoder); // the number of 'pure' data fragments const size_t num_fragments = BCUR_NUM_FRAGMENTS(min_num_fragments); // add some fountain-code fragments JADE_ASSERT(num_fragments >= min_num_fragments); JADE_LOGI("Encoded payload length %u as %u pure fragments and %u fountain-code fragments", len, min_num_fragments, @@ -753,11 +761,12 @@ void bcur_create_qr_icons(const uint8_t* payload, const size_t len, const char* uint8_t* qrbuffer = JADE_MALLOC(qrcode_getBufferSize(qr_version)); // Convert to 'num_fragments' qr-code icons - const bool force_uppercase = true; // fetch bcur fragment as uppercase to conform to 'alphanumeric' qr mode + // NOTE: fragments are uppercase, to conform to 'alphanumeric' qr mode Icon* const qr_icons = JADE_MALLOC(num_fragments * sizeof(Icon)); for (int ifrag = 0; ifrag < num_fragments; ++ifrag) { char* fragment = NULL; - urnext_part_encoder(encoder, force_uppercase, &fragment); + const bool have_part = ur_encoder_next_part(encoder, &fragment); + JADE_ASSERT(have_part); const size_t fragment_len = strlen(fragment); JADE_LOGI("Fragment %u, making qr-code icon with data (length: %u): %s", ifrag, fragment_len, fragment); @@ -768,15 +777,15 @@ void bcur_create_qr_icons(const uint8_t* payload, const size_t len, const char* QRCode qrcode; const int qret = qrcode_initText(&qrcode, qrbuffer, qr_version, BCUR_QR_ECC, fragment); JADE_ASSERT(qret == 0); - urfree_encoded_encoder(fragment); + free(fragment); // Convert fragment to Icon qrcode_toIcon(&qrcode, qr_icons + ifrag, QR_SCALE_FACTOR[qr_version]); } - JADE_ASSERT(uris_complete_encoder(encoder)); + JADE_ASSERT(ur_encoder_is_complete(encoder)); free(qrbuffer); - urfree_placement_encoder(encoder); + ur_encoder_free(encoder); // Return the created icons *icons = qr_icons; @@ -810,12 +819,12 @@ bool bcur_check_fragment_sizes(void) size_t max = 0; for (size_t len = maxlen - 8; len < sizeof(payload); ++len) { - uint8_t encoder[URENCODER_SIZE]; - urcreate_placement_encoder(encoder, sizeof(encoder), type, payload, len, maxlen, 0, 8); + ur_encoder_t* const encoder = ur_encoder_new(type, payload, len, maxlen, 0, 8); + JADE_ASSERT(encoder); char* fragment = NULL; - urnext_part_encoder(encoder, true, &fragment); - JADE_ASSERT(fragment); + const bool have_part = ur_encoder_next_part(encoder, &fragment); + JADE_ASSERT(have_part); const size_t fraglen = strlen(fragment); if (fraglen + 4 > capacity) { // In truth fraglen == capacity is valid, but later parts (and of larger payloads) @@ -827,8 +836,8 @@ bool bcur_check_fragment_sizes(void) if (fraglen > max) { max = fraglen; } - urfree_encoded_encoder(fragment); - urfree_placement_encoder(encoder); + free(fragment); + ur_encoder_free(encoder); } JADE_LOGI("max: %u (of target/limit %u)", max, capacity); } diff --git a/main/process/mnemonic.c b/main/process/mnemonic.c index db726f90b..2b885cb93 100644 --- a/main/process/mnemonic.c +++ b/main/process/mnemonic.c @@ -19,7 +19,6 @@ #include "process_utils.h" -#include #include #define MAX_NUM_FINAL_WORDS 128 diff --git a/main/selfcheck.c b/main/selfcheck.c index eb5d61ebe..b54ac0475 100644 --- a/main/selfcheck.c +++ b/main/selfcheck.c @@ -25,9 +25,9 @@ #include #include -#include -#include #include +#include +#include int register_multisig_file(const char* multisig_file, size_t multisig_file_len, const char** errmsg); @@ -478,20 +478,20 @@ bool test_multisig_files(jade_process_t* process) #define FREE_DECODER_AND_FAIL(d) \ do { \ - urfree_placement_decoder(d); \ + ur_decoder_free(d); \ FAIL(); \ } while (false) #define FREE_ENCODER_AND_FAIL(e) \ do { \ - urfree_placement_encoder(e); \ + ur_encoder_free(e); \ FAIL(); \ } while (false) #define FREE_ENCODED_PARTS(p) \ do { \ for (int i = 0; i < sizeof(p) / sizeof(p[0]); ++i) { \ - urfree_encoded_encoder(p[i]); \ + free(p[i]); \ } \ } while (false) @@ -526,119 +526,89 @@ static bool test_bcur_decode_encode(void) // 1. Try decoder { // Check decoder with a message of 2 'pure' fragments - uint8_t decoder[URDECODER_SIZE]; - urcreate_placement_decoder(decoder, sizeof(decoder)); - if (uris_success_decoder(decoder)) { + ur_decoder_t* const decoder = ur_decoder_new(); + JADE_ASSERT(decoder); + if (ur_decoder_get_state(decoder) != UR_DECODER_PROCESSING) { FREE_DECODER_AND_FAIL(decoder); } - if (uris_complete_decoder(decoder)) { + if (ur_decoder_received_parts_count(decoder) != 0) { FREE_DECODER_AND_FAIL(decoder); } - if (uris_failure_decoder(decoder)) { - FREE_DECODER_AND_FAIL(decoder); - } - if (urreceived_parts_count_decoder(decoder) != 0) { - FREE_DECODER_AND_FAIL(decoder); - } - // NOTE: Can't call urexpected_part_count_decoder() until first part processed + // NOTE: Can't call ur_decoder_expected_part_count() until first part processed - // send first qr - if (!urreceive_part_decoder(decoder, qr_part1of2)) { - FREE_DECODER_AND_FAIL(decoder); - } - if (uris_failure_decoder(decoder)) { - FREE_DECODER_AND_FAIL(decoder); - } - if (uris_success_decoder(decoder)) { - // Should NOT be complete yet + // send first qr - should be processed, but NOT complete yet + if (ur_decoder_receive_part(decoder, qr_part1of2) != UR_DECODER_PROCESSING) { FREE_DECODER_AND_FAIL(decoder); } - if (urreceived_parts_count_decoder(decoder) != 1) { + if (ur_decoder_received_parts_count(decoder) != 1) { FREE_DECODER_AND_FAIL(decoder); } - if (urexpected_part_count_decoder(decoder) != 2) { + if (ur_decoder_expected_part_count(decoder) != 2) { FREE_DECODER_AND_FAIL(decoder); } - // send first qr again - should be ignored/harmless - if (!urreceive_part_decoder(decoder, qr_part1of2)) { - FREE_DECODER_AND_FAIL(decoder); - } - if (uris_failure_decoder(decoder)) { - FREE_DECODER_AND_FAIL(decoder); - } - if (uris_success_decoder(decoder)) { - // Should NOT be complete yet + // send first qr again - should be ignored/harmless, still NOT complete + if (ur_decoder_receive_part(decoder, qr_part1of2) != UR_DECODER_PROCESSING) { FREE_DECODER_AND_FAIL(decoder); } - if (urreceived_parts_count_decoder(decoder) != 1) { + if (ur_decoder_received_parts_count(decoder) != 1) { FREE_DECODER_AND_FAIL(decoder); } - if (urexpected_part_count_decoder(decoder) != 2) { + if (ur_decoder_expected_part_count(decoder) != 2) { FREE_DECODER_AND_FAIL(decoder); } - // send second qr - if (!urreceive_part_decoder(decoder, qr_part2of2)) { + // send second qr - should now be complete + if (ur_decoder_receive_part(decoder, qr_part2of2) != UR_DECODER_OK) { FREE_DECODER_AND_FAIL(decoder); } - if (uris_failure_decoder(decoder)) { + if (ur_decoder_received_parts_count(decoder) != 2) { FREE_DECODER_AND_FAIL(decoder); } - if (!uris_success_decoder(decoder)) { - // Should now be complete - FREE_DECODER_AND_FAIL(decoder); - } - if (urreceived_parts_count_decoder(decoder) != 2) { - FREE_DECODER_AND_FAIL(decoder); - } - if (urexpected_part_count_decoder(decoder) != 2) { + if (ur_decoder_expected_part_count(decoder) != 2) { FREE_DECODER_AND_FAIL(decoder); } - // read the result - const char* type = NULL; - uint8_t* result = NULL; - size_t result_len = 0; - urresult_ur_decoder(decoder, &result, &result_len, &type); - JADE_ASSERT(type); - JADE_ASSERT(result_len); + // read the result - borrowed from the decoder, freed with it + const ur_result_t* const result = ur_decoder_get_result(decoder); JADE_ASSERT(result); - if (strncmp(expected_type, type, strlen(expected_type))) { + JADE_ASSERT(result->type); + JADE_ASSERT(result->cbor_len); + JADE_ASSERT(result->cbor_data); + if (strncmp(expected_type, result->type, strlen(expected_type))) { FREE_DECODER_AND_FAIL(decoder); } - if (result_len != payload_len || memcmp(result, payload, result_len)) { + if (result->cbor_len != payload_len || memcmp(result->cbor_data, payload, result->cbor_len)) { FREE_DECODER_AND_FAIL(decoder); } - urfree_placement_decoder(decoder); + ur_decoder_free(decoder); } // 2. Try encoder { // If we encode the data, the first two parts should match the original payloads // ie. the 'pure' fragments (ie. the actual data split into two and encoded) - uint8_t encoder[URENCODER_SIZE]; - urcreate_placement_encoder( - encoder, sizeof(encoder), expected_type, payload, payload_len, encoder_max_fragment_len, 0, 10); - const bool force_uppercase = true; + ur_encoder_t* const encoder + = ur_encoder_new(expected_type, payload, payload_len, encoder_max_fragment_len, 0, 10); + JADE_ASSERT(encoder); char* parts[3] = { NULL, NULL, NULL }; - urnext_part_encoder(encoder, force_uppercase, &parts[0]); + ur_encoder_next_part(encoder, &parts[0]); if (!parts[0] || strncmp(parts[0], qr_part1of2, strlen(qr_part1of2))) { FREE_ENCODED_PARTS(parts); FREE_ENCODER_AND_FAIL(encoder); } - if (uris_complete_encoder(encoder)) { + if (ur_encoder_is_complete(encoder)) { // Should NOT be complete yet FREE_ENCODED_PARTS(parts); FREE_ENCODER_AND_FAIL(encoder); } - urnext_part_encoder(encoder, force_uppercase, &parts[1]); + ur_encoder_next_part(encoder, &parts[1]); if (!parts[1] || strncmp(parts[1], qr_part2of2, strlen(qr_part2of2))) { FREE_ENCODED_PARTS(parts); FREE_ENCODER_AND_FAIL(encoder); } - if (!uris_complete_encoder(encoder)) { + if (!ur_encoder_is_complete(encoder)) { // Should now be complete FREE_ENCODED_PARTS(parts); FREE_ENCODER_AND_FAIL(encoder); @@ -646,12 +616,12 @@ static bool test_bcur_decode_encode(void) // We can continue to generate additional parts - these are fountain-code fragments // which can stand in for any missed fragments. NOTE: the sequence-numbers appear 'overflowed'. - urnext_part_encoder(encoder, force_uppercase, &parts[2]); + ur_encoder_next_part(encoder, &parts[2]); if (!parts[2] || strncmp(parts[2], "UR:CRYPTO-PSBT/3-2/", strlen("UR:CRYPTO-PSBT/3-2/"))) { FREE_ENCODED_PARTS(parts); FREE_ENCODER_AND_FAIL(encoder); } - urfree_placement_encoder(encoder); + ur_encoder_free(encoder); // Check fountain encoding / redundancy with fresh decoders - incl. getting a 'later' part first // Check all 2-of-3 combinations - any 2 distinct parts should be sufficient. @@ -664,46 +634,40 @@ static bool test_bcur_decode_encode(void) const size_t initial_expected_received = i == 2 ? 0 : 1; for (size_t j = 0; j < 3; ++j) { - uint8_t decoder[URDECODER_SIZE]; - urcreate_placement_decoder(decoder, sizeof(decoder)); + ur_decoder_t* const decoder = ur_decoder_new(); + JADE_ASSERT(decoder); - // Present first part - if (!urreceive_part_decoder(decoder, parts[i])) { + // Present first part - should be processed, but NOT complete yet + if (ur_decoder_receive_part(decoder, parts[i]) != UR_DECODER_PROCESSING) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - if (uris_success_decoder(decoder)) { - // Should NOT be complete yet + if (ur_decoder_processed_parts_count(decoder) != 1) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - if (urprocessed_parts_count_decoder(decoder) != 1) { + if (ur_decoder_expected_part_count(decoder) != 2) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - if (urexpected_part_count_decoder(decoder) != 2) { - FREE_ENCODED_PARTS(parts); - FREE_DECODER_AND_FAIL(decoder); - } - if (urreceived_parts_count_decoder(decoder) != initial_expected_received) { + if (ur_decoder_received_parts_count(decoder) != initial_expected_received) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } // Present second part - if (!urreceive_part_decoder(decoder, parts[j])) { - FREE_ENCODED_PARTS(parts); - FREE_DECODER_AND_FAIL(decoder); - } - if (urprocessed_parts_count_decoder(decoder) != 2) { + const ur_decoder_state_t state = ur_decoder_receive_part(decoder, parts[j]); + if (ur_decoder_state_is_error(state)) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - if (urexpected_part_count_decoder(decoder) != 2) { + // NOTE: a duplicate part is deduped before it is counted, so + // 'processed' remains 1 if the same part is presented twice + if (ur_decoder_processed_parts_count(decoder) != (i != j ? 2 : 1)) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - if (uris_failure_decoder(decoder)) { + if (ur_decoder_expected_part_count(decoder) != 2) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } @@ -715,43 +679,41 @@ static bool test_bcur_decode_encode(void) // the 'pure' data part is received (and can be combined with the fountain // part to generate the missing data part). if (i != j) { - if (urreceived_parts_count_decoder(decoder) != 2) { + if (ur_decoder_received_parts_count(decoder) != 2) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - if (!uris_success_decoder(decoder)) { + if (state != UR_DECODER_OK) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - // Check payload is as expected - const char* type = NULL; - uint8_t* result = NULL; - size_t result_len = 0; - urresult_ur_decoder(decoder, &result, &result_len, &type); - JADE_ASSERT(type); - JADE_ASSERT(result_len); + // Check payload is as expected - result borrowed from the decoder + const ur_result_t* const result = ur_decoder_get_result(decoder); JADE_ASSERT(result); - if (strncmp(expected_type, type, strlen(expected_type))) { + JADE_ASSERT(result->type); + JADE_ASSERT(result->cbor_len); + JADE_ASSERT(result->cbor_data); + if (strncmp(expected_type, result->type, strlen(expected_type))) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - if (result_len != payload_len || memcmp(result, payload, result_len)) { + if (result->cbor_len != payload_len || memcmp(result->cbor_data, payload, result->cbor_len)) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } } else { // Same part received twice - does not increment 'received_parts' - if (urreceived_parts_count_decoder(decoder) != initial_expected_received) { + if (ur_decoder_received_parts_count(decoder) != initial_expected_received) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } - if (uris_success_decoder(decoder)) { + if (state != UR_DECODER_PROCESSING) { FREE_ENCODED_PARTS(parts); FREE_DECODER_AND_FAIL(decoder); } } - urfree_placement_decoder(decoder); + ur_decoder_free(decoder); } } FREE_ENCODED_PARTS(parts); @@ -783,28 +745,25 @@ bool test_bcur_decode_bad_cases(void) // Various bad cases - test the decoder ignores them for (size_t i = 0; i < ncases; ++i) { - uint8_t decoder[URDECODER_SIZE]; - urcreate_placement_decoder(decoder, sizeof(decoder)); + ur_decoder_t* const decoder = ur_decoder_new(); + JADE_ASSERT(decoder); - // Present first part - should be ignored and all counts - // of 'parts seen' should remain zero, and 'is complete' - // and 'is success' should be false. - if (urreceive_part_decoder(decoder, cases[i])) { - FREE_DECODER_AND_FAIL(decoder); - } - if (urprocessed_parts_count_decoder(decoder)) { + // Present first part - should be rejected with a transient (non-terminal) + // error, and all counts of 'parts seen' should remain zero. + const ur_decoder_state_t state = ur_decoder_receive_part(decoder, cases[i]); + if (!ur_decoder_state_is_error(state)) { FREE_DECODER_AND_FAIL(decoder); } - if (urreceived_parts_count_decoder(decoder)) { + if (ur_decoder_state_is_terminal(state)) { FREE_DECODER_AND_FAIL(decoder); } - if (uris_complete_decoder(decoder)) { + if (ur_decoder_processed_parts_count(decoder)) { FREE_DECODER_AND_FAIL(decoder); } - if (uris_success_decoder(decoder)) { + if (ur_decoder_received_parts_count(decoder)) { FREE_DECODER_AND_FAIL(decoder); } - urfree_placement_decoder(decoder); + ur_decoder_free(decoder); } return true; } From 35bcb82e7f93cf0f24b93681d8c9fe6ca9c22cff Mon Sep 17 00:00:00 2001 From: odudex Date: Wed, 8 Jul 2026 17:28:19 -0300 Subject: [PATCH 3/5] ur: use weighted estimate for scan progress Drive the animated-QR progress bar with ur_decoder_estimated_percent_complete_weighted() instead of the pure received/expected fragment counts. The weighted estimate also credits mixed fountain parts that have not yet resolved a pure fragment, so progress keeps moving where the count-based bar would stall at 'almost done' until the final reconstruction. Capped at 99% until the decode actually completes. --- main/bcur.c | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/main/bcur.c b/main/bcur.c index fd2ab1f34..1bc6f0049 100644 --- a/main/bcur.c +++ b/main/bcur.c @@ -641,22 +641,18 @@ static bool collect_any_bcur(qr_data_t* qr_data) return false; } - // Update associated progress bar - be a bit defensive here + // Update associated progress bar using the weighted estimate, which also + // credits mixed fountain parts that have not yet resolved a pure fragment. + // If not fully decoded don't show a full bar - cap at 'almost done'. const bool decoded = state == UR_DECODER_OK; - const size_t nreceived = ur_decoder_received_parts_count(qr_data->ctx); - if (!ur_decoder_state_is_error(state) && nreceived) { - // NOTE: can only call 'expected' once we have received at least one part - const size_t nexpected = ur_decoder_expected_part_count(qr_data->ctx); - - // If fully decoded show full bar - but if not fully decoded - // don't show a full bar - pause at 'almost done' if required. + if (!ur_decoder_state_is_error(state)) { if (decoded) { - update_progress_bar(qr_data->progress_bar, nexpected, nexpected); - } else if (nreceived < nexpected) { - update_progress_bar(qr_data->progress_bar, nexpected, nreceived); + update_progress_bar(qr_data->progress_bar, 100, 100); + } else { + const float estimate = ur_decoder_estimated_percent_complete_weighted(qr_data->ctx); + const size_t pcnt = estimate * 100.0f; + update_progress_bar(qr_data->progress_bar, 100, pcnt < 99 ? pcnt : 99); } - // else, appear to have all pieces but not fully decoded ... - // just leave progress bar showing whatever 'almost done' level. } // Return true if complete From 96888b2b49b5ed861b8c77c9165506a05f511d7a Mon Sep 17 00:00:00 2001 From: odudex Date: Wed, 8 Jul 2026 11:29:55 -0300 Subject: [PATCH 4/5] components: add k_quirc submodule Add k_quirc (https://github.com/odudex/k_quirc), a rewritten ESP32-optimized QR decoder (bilinear/adaptive thresholding, span-based flood fill, SPIRAM-aware allocation), pinned at the head of its master branch. No other changes - the source migration follows in the next commit. --- .gitmodules | 4 ++++ components/k_quirc | 1 + 2 files changed, 5 insertions(+) create mode 160000 components/k_quirc diff --git a/.gitmodules b/.gitmodules index 62667ea86..8cf048e0c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,7 @@ [submodule "components/cUR"] path = components/cUR url = https://github.com/odudex/cUR +[submodule "components/k_quirc"] + path = components/k_quirc + url = https://github.com/odudex/k_quirc + branch = master diff --git a/components/k_quirc b/components/k_quirc new file mode 160000 index 000000000..f0dd57a93 --- /dev/null +++ b/components/k_quirc @@ -0,0 +1 @@ +Subproject commit f0dd57a938573476113ade547d1328e73a72b6b3 From 91846f9075c0ddc3ee30081c1ae3385dc91e416f Mon Sep 17 00:00:00 2001 From: odudex Date: Wed, 8 Jul 2026 11:30:15 -0300 Subject: [PATCH 5/5] qr: use k_quirc instead of esp32-quirc Rewrite the thin wrapper in main/qrscan.c to the k_quirc_* API, folding quirc_extract + quirc_decode into a single k_quirc_decode() and dropping the caller-allocated datastream scratch buffer (now managed internally). k_quirc_end() is called with find_inverted=false to preserve current behavior. Downstream callers use the qr_data_t / jade_camera_scan_qr / scan_qr wrapper API and are unaffected. Also update the three build references: main/CMakeLists.txt PRIV_REQUIRES, libjade include dirs, and the libjade.c amalgamated .c includes. The vendored esp32-quirc component is left in place (now unused) so it can be removed separately once the migration has settled. --- libjade/CMakeLists.txt | 4 +-- libjade/libjade.c | 9 +++--- main/CMakeLists.txt | 2 +- main/qrscan.c | 67 ++++++++++++++++++------------------------ main/qrscan.h | 7 ++--- 5 files changed, 38 insertions(+), 51 deletions(-) diff --git a/libjade/CMakeLists.txt b/libjade/CMakeLists.txt index 0b97bd91f..60b515330 100644 --- a/libjade/CMakeLists.txt +++ b/libjade/CMakeLists.txt @@ -90,8 +90,8 @@ set(COMMON_INCLUDES ${CMAKE_CURRENT_SOURCE_DIR}/../components/libwally-core/upstream/include ${CMAKE_CURRENT_SOURCE_DIR}/../components/assets ${CMAKE_CURRENT_SOURCE_DIR}/../components/esp32_bsdiff - ${CMAKE_CURRENT_SOURCE_DIR}/../components/esp32-quirc - ${CMAKE_CURRENT_SOURCE_DIR}/../components/esp32-quirc/lib + ${CMAKE_CURRENT_SOURCE_DIR}/../components/k_quirc/include + ${CMAKE_CURRENT_SOURCE_DIR}/../components/k_quirc/src ${CMAKE_CURRENT_SOURCE_DIR}/../components/libwally-core/upstream ${CMAKE_CURRENT_SOURCE_DIR}/../components/libwally-core/upstream/src ${CMAKE_CURRENT_SOURCE_DIR}/../components/libwally-core/upstream/src/ccan diff --git a/libjade/libjade.c b/libjade/libjade.c index 4cb988e57..c07edec82 100644 --- a/libjade/libjade.c +++ b/libjade/libjade.c @@ -66,11 +66,10 @@ #undef ESP_PLATFORM #include "components/esp32_deflate/deflate.c" // qrCode encoding/decoding -#include "components/esp32-quirc/lib/decode.c" -#include "components/esp32-quirc/lib/identify.c" -#include "components/esp32-quirc/lib/quirc.c" -#include "components/esp32-quirc/lib/version_db.c" -#include "components/esp32-quirc/openmv/collections.c" +#include "components/k_quirc/src/k_quirc.c" +#include "components/k_quirc/src/k_quirc_decode.c" +#include "components/k_quirc/src/k_quirc_identify.c" +#include "components/k_quirc/src/k_quirc_version.c" // bspatch #include "components/esp32_bsdiff/bspatch.c" diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 82f265a49..f66ea87bb 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -52,7 +52,7 @@ idf_component_register(SRC_DIRS "." "${usbdir}" "${wallydirs}" "${secpdir}" - PRIV_REQUIRES assets libwally-core libsodium esp32-rotary-encoder esp32-quirc bootloader_support app_update nvs_flash bt autogenlang cbor esp_netif esp32_bsdiff esp32_deflate nghttp cUR driver mbedtls http_parser esp_hw_support efuse esp_eth esp_http_server esp_lcd usb vfs app_trace spi_flash google-otpauth-migrate ${extra_priv_requires} + PRIV_REQUIRES assets libwally-core libsodium esp32-rotary-encoder k_quirc bootloader_support app_update nvs_flash bt autogenlang cbor esp_netif esp32_bsdiff esp32_deflate nghttp cUR driver mbedtls http_parser esp_hw_support efuse esp_eth esp_http_server esp_lcd usb vfs app_trace spi_flash google-otpauth-migrate ${extra_priv_requires} EMBED_FILES ${PROJECT_DIR}/pinserver_public_key.pub ${logo_files} ${qemu_display_file}) if(CONFIG_ETH_USE_OPENETH) diff --git a/main/qrscan.c b/main/qrscan.c index e96202c92..90ec3bd4b 100644 --- a/main/qrscan.c +++ b/main/qrscan.c @@ -1,5 +1,5 @@ #ifndef AMALGAMATED_BUILD -#include +#include "k_quirc.h" #include #include "camera.h" @@ -7,7 +7,6 @@ #include "jade_assert.h" #include "qrscan.h" #include "sensitive.h" -#include "utils/malloc_ext.h" #include "utils/util.h" #define SCAN_MARGIN 20 @@ -18,50 +17,48 @@ static bool qr_extract_payload(qr_data_t* qr_data) { JADE_ASSERT(qr_data); JADE_ASSERT(qr_data->q); - JADE_ASSERT(qr_data->ds); qr_data->data[0] = '\0'; qr_data->len = 0; - const int count = quirc_count(qr_data->q); + const int count = k_quirc_count(qr_data->q); if (count <= 0) { return false; } JADE_LOGI("Detected %d QR codes in image.", count); // Store the first string we manage to extract - initialise to empty string. - struct quirc_data data; - SENSITIVE_PUSH(&data, sizeof(data)); + k_quirc_result_t result; + SENSITIVE_PUSH(&result, sizeof(result)); // Look for a string for (int i = 0; i < count; ++i) { - struct quirc_code code; - quirc_extract(qr_data->q, i, &code); - - const quirc_decode_error_t error_status = quirc_decode(&code, &data, qr_data->ds); - if (error_status != QUIRC_SUCCESS) { - JADE_LOGW("QUIRC error %s", quirc_strerror(error_status)); - } else if (data.data_type == QUIRC_DATA_TYPE_KANJI) { - JADE_LOGW("QUIRC unexpected data type: %d", data.data_type); - } else if (!data.payload_len) { + const k_quirc_error_t error_status = k_quirc_decode(qr_data->q, i, &result); + if (error_status != K_QUIRC_SUCCESS) { + JADE_LOGW("QUIRC error %s", k_quirc_strerror(error_status)); + } else if (!result.valid) { + JADE_LOGW("QUIRC invalid result"); + } else if (result.data.data_type == K_QUIRC_DATA_TYPE_KANJI) { + JADE_LOGW("QUIRC unexpected data type: %d", result.data.data_type); + } else if (!result.data.payload_len) { JADE_LOGW("QUIRC empty string"); - } else if (data.payload_len >= sizeof(qr_data->data)) { - JADE_LOGW("QUIRC data too long to handle: %u", data.payload_len); - JADE_ASSERT(data.payload_len <= sizeof(data.payload)); + } else if (result.data.payload_len >= sizeof(qr_data->data)) { + JADE_LOGW("QUIRC data too long to handle: %u", result.data.payload_len); + JADE_ASSERT(result.data.payload_len <= sizeof(result.data.payload)); } else { // The payload appears to be a nul terminated string, but the // 'payload_len' seems to be the string length not including that // terminator. // To avoid any confusion or grey areas, we copy the bytes, // and then explicitly add the nul terminator ourselves. - memcpy(qr_data->data, data.payload, data.payload_len); - qr_data->data[data.payload_len] = '\0'; - qr_data->len = data.payload_len; - SENSITIVE_POP(&data); + memcpy(qr_data->data, result.data.payload, result.data.payload_len); + qr_data->data[result.data.payload_len] = '\0'; + qr_data->len = result.data.payload_len; + SENSITIVE_POP(&result); return true; } } - SENSITIVE_POP(&data); + SENSITIVE_POP(&result); return false; } @@ -79,7 +76,7 @@ static bool qr_recognize( // Checked qr image buffer exists and is an acceptable size int quirc_width = 0, quirc_height = 0; - uint8_t* const quirc_image = quirc_begin(qr_data->q, &quirc_width, &quirc_height); + uint8_t* const quirc_image = k_quirc_begin(qr_data->q, &quirc_width, &quirc_height); JADE_ASSERT(quirc_image); JADE_ASSERT(quirc_width <= width); JADE_ASSERT(quirc_height <= height); @@ -95,7 +92,7 @@ static bool qr_recognize( memcpy(quirc_image + (y * quirc_width), data + ((y + yoffset) * width) + xoffset, quirc_width); } } - quirc_end(qr_data->q); + k_quirc_end(qr_data->q, false); // If no QR data can be recognised/extracted, return false if (!qr_extract_payload(qr_data) || !qr_data->len) { @@ -126,14 +123,12 @@ bool scan_qr(const size_t width, const size_t height, const uint8_t* data, const JADE_ASSERT(!qr_data->q); // Create the quirc structs - qr_data->q = quirc_new(); + qr_data->q = k_quirc_new(); JADE_ASSERT(qr_data->q); - qr_data->ds = JADE_MALLOC_DRAM(sizeof(struct datastream)); - JADE_ASSERT(qr_data->ds); // Also correctly size the internal image buffer since we know the size of the camera images. const uint16_t scan_width = min_u16(CAMERA_IMAGE_WIDTH, CAMERA_IMAGE_HEIGHT) - SCAN_MARGIN; - const int qret = quirc_resize(qr_data->q, scan_width, scan_width); + const int qret = k_quirc_resize(qr_data->q, scan_width, scan_width); JADE_ASSERT(qret == 0); qr_data->len = 0; @@ -143,10 +138,8 @@ bool scan_qr(const size_t width, const size_t height, const uint8_t* data, const const bool ret = qr_recognize(width, height, data, len, qr_data); // Destroy the quirc structs created above - quirc_destroy(qr_data->q); + k_quirc_destroy(qr_data->q); qr_data->q = NULL; - free(qr_data->ds); - qr_data->ds = NULL; // Any scanned qr code will be in the qr_data passed return ret && qr_data->len > 0; @@ -166,15 +159,13 @@ bool jade_camera_scan_qr( #ifdef CONFIG_HAS_CAMERA // Create the quirc structs (reused for each frame) - destroyed below JADE_ASSERT(!qr_data->q); - qr_data->q = quirc_new(); + qr_data->q = k_quirc_new(); JADE_ASSERT(qr_data->q); - qr_data->ds = JADE_MALLOC_DRAM(sizeof(struct datastream)); - JADE_ASSERT(qr_data->ds); // Also correctly size the internal image buffer since we know the size of the camera images. // This image buffer is then reused for every camera image frame processed. const uint16_t scan_width = min_u16(CAMERA_IMAGE_WIDTH, CAMERA_IMAGE_HEIGHT) - SCAN_MARGIN; - const int qret = quirc_resize(qr_data->q, scan_width, scan_width); + const int qret = k_quirc_resize(qr_data->q, scan_width, scan_width); JADE_ASSERT(qret == 0); qr_data->len = 0; @@ -188,10 +179,8 @@ bool jade_camera_scan_qr( help_url, qr_data->progress_bar); // Destroy the quirc structs created above - quirc_destroy(qr_data->q); + k_quirc_destroy(qr_data->q); qr_data->q = NULL; - free(qr_data->ds); - qr_data->ds = NULL; // Any scanned qr code will be in the qr_data passed return qr_data->len > 0; diff --git a/main/qrscan.h b/main/qrscan.h index f60d884c0..65438c832 100644 --- a/main/qrscan.h +++ b/main/qrscan.h @@ -11,7 +11,7 @@ // An extracted QR code string #define QR_MAX_PAYLOAD_LENGTH 1024 -struct quirc; +struct k_quirc; typedef struct _qr_data_t qr_data_t; // Function to tell whether the extracted qr data is valid for the callers purposes @@ -32,9 +32,8 @@ struct _qr_data_t { // Any progress-bar associated with this (potentially multi-frame) scanning progress_bar_t* progress_bar; - // Cached internal quirc structs - caller should set to NULL - struct quirc* q; - struct datastream* ds; + // Cached internal quirc decoder - caller should set to NULL + struct k_quirc* q; }; #ifdef CONFIG_DEBUG_MODE