From 863786de8654f3002ece0ffbbed4abe62fa36ef1 Mon Sep 17 00:00:00 2001 From: Stuart White Date: Tue, 16 Jun 2026 18:06:15 -0600 Subject: [PATCH 1/3] feat(dive detail): implement parse of raw data on Windows --- .../test/native/CMakeLists.txt | 37 +++++++ .../test/native/fixtures/dive1_raw.bin | Bin 0 -> 400 bytes .../test/native/test_parse_raw_dive.c | 96 ++++++++++++++++++ .../windows/dive_computer_host_api_impl.cc | 23 ++++- 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 packages/libdivecomputer_plugin/test/native/fixtures/dive1_raw.bin create mode 100644 packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c diff --git a/packages/libdivecomputer_plugin/test/native/CMakeLists.txt b/packages/libdivecomputer_plugin/test/native/CMakeLists.txt index 0cfc8555e..cfd2ddd89 100644 --- a/packages/libdivecomputer_plugin/test/native/CMakeLists.txt +++ b/packages/libdivecomputer_plugin/test/native/CMakeLists.txt @@ -33,7 +33,44 @@ target_include_directories(test_descriptor_matcher PRIVATE ${CONFIG_DIR} ) +# test_parse_raw_dive requires the full libdivecomputer + wrapper linked in. +# Use the Windows config on Windows, macOS config otherwise. +if(WIN32) + set(PLATFORM_CONFIG_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../windows/config") +else() + set(PLATFORM_CONFIG_DIR "${CONFIG_DIR}") +endif() + +# Collect libdivecomputer sources needed for parsing (exclude platform I/O +# files that don't compile cross-platform). +file(GLOB LIBDC_ALL_SOURCES "${LIBDC_DIR}/src/*.c") +list(FILTER LIBDC_ALL_SOURCES EXCLUDE REGEX "serial_posix\\.c$") +if(NOT WIN32) + list(FILTER LIBDC_ALL_SOURCES EXCLUDE REGEX "serial_win32\\.c$") +endif() + +add_executable(test_parse_raw_dive + test_parse_raw_dive.c + ${WRAPPER_DIR}/libdc_wrapper.c + ${WRAPPER_DIR}/libdc_download.c + ${LIBDC_ALL_SOURCES} +) +target_include_directories(test_parse_raw_dive PRIVATE + ${WRAPPER_DIR} + ${LIBDC_DIR}/include + ${LIBDC_DIR}/src + ${PLATFORM_CONFIG_DIR} +) +target_compile_definitions(test_parse_raw_dive PRIVATE HAVE_CONFIG_H) +if(WIN32) + target_compile_definitions(test_parse_raw_dive PRIVATE _CRT_SECURE_NO_WARNINGS) + target_link_libraries(test_parse_raw_dive PRIVATE SetupAPI.lib ws2_32.lib) +endif() + enable_testing() add_test(NAME test_dive_converter COMMAND test_dive_converter) add_test(NAME test_serial_callbacks COMMAND test_serial_callbacks) add_test(NAME test_descriptor_matcher COMMAND test_descriptor_matcher) +add_test(NAME test_parse_raw_dive COMMAND test_parse_raw_dive + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" +) diff --git a/packages/libdivecomputer_plugin/test/native/fixtures/dive1_raw.bin b/packages/libdivecomputer_plugin/test/native/fixtures/dive1_raw.bin new file mode 100644 index 0000000000000000000000000000000000000000..dc4ebe52877fae5dfa0f2c4309a87095f72d9073 GIT binary patch literal 400 zcmaiu&np9P7{(vl4wN|9N+IRor{{Tl>M-DrdXi{ubk+|w^Z{^MfF@7B18}%%=y1Avf&b|E0nA^s zOCGt^iEg#2on7c;i)ls=df0~%db!LHLfl{s$2g+QE~88!!DAls)LvJ-!5wdTZ65f< z6d##kmRaSPV}S+pq9jOY{*Iav4lnXG^s)J E3sxY5$^ZZW literal 0 HcmV?d00001 diff --git a/packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c b/packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c new file mode 100644 index 000000000..29dc2e44f --- /dev/null +++ b/packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include +#include +#include "libdc_wrapper.h" + +/* Load a binary file into a malloc'd buffer. Returns size, or 0 on failure. */ +static unsigned int load_fixture(const char *path, unsigned char **out) { + FILE *f = fopen(path, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long len = ftell(f); + fseek(f, 0, SEEK_SET); + if (len <= 0) { fclose(f); return 0; } + *out = (unsigned char *)malloc((size_t)len); + size_t read = fread(*out, 1, (size_t)len, f); + fclose(f); + return (unsigned int)read; +} + +/* Error path: NULL arguments should return INVALIDARGS. */ +static void test_null_args(void) { + libdc_parsed_dive_t result; + char err[256] = {0}; + + int rc = libdc_parse_raw_dive(NULL, "Leonardo", 1, (const unsigned char *)"x", 1, &result, err, sizeof(err)); + assert(rc != 0); + + rc = libdc_parse_raw_dive("Cressi", "Leonardo", 1, NULL, 0, &result, err, sizeof(err)); + assert(rc != 0); + + rc = libdc_parse_raw_dive("Cressi", "Leonardo", 1, (const unsigned char *)"x", 1, NULL, err, sizeof(err)); + assert(rc != 0); + + printf("PASS: test_null_args\n"); +} + +/* Error path: unknown descriptor should return NODEVICE. */ +static void test_unknown_descriptor(void) { + libdc_parsed_dive_t result; + char err[256] = {0}; + unsigned char dummy[16] = {0}; + + int rc = libdc_parse_raw_dive("BogusVendor", "BogusProduct", 9999, dummy, sizeof(dummy), &result, err, sizeof(err)); + assert(rc != 0); + assert(strlen(err) > 0); + printf("PASS: test_unknown_descriptor (error: %s)\n", err); +} + +/* Happy path: parse real Cressi Leonardo dive data from fixture. */ +static void test_parse_cressi_leonardo(void) { + unsigned char *data = NULL; + unsigned int size = load_fixture("fixtures/dive1_raw.bin", &data); + assert(size == 400); + assert(data != NULL); + + libdc_parsed_dive_t result; + char err[256] = {0}; + + int rc = libdc_parse_raw_dive("Cressi", "Leonardo", 1, data, size, &result, err, sizeof(err)); + if (rc != 0) { + fprintf(stderr, "FAIL: parse returned %d: %s\n", rc, err); + free(data); + assert(0 && "libdc_parse_raw_dive failed"); + } + + /* Basic sanity checks on parsed output. */ + assert(result.max_depth > 0.0); + assert(result.duration > 0); + assert(result.sample_count > 0); + assert(result.samples != NULL); + + /* Verify samples are time-ordered and depths are non-negative. */ + for (unsigned int i = 0; i < result.sample_count; i++) { + assert(result.samples[i].depth >= 0.0); + if (i > 0) { + assert(result.samples[i].time_ms >= result.samples[i - 1].time_ms); + } + } + + printf("PASS: test_parse_cressi_leonardo (depth=%.1fm, duration=%us, samples=%u)\n", + result.max_depth, result.duration, result.sample_count); + + free(result.samples); + free(result.events); + free(data); +} + +int main(void) { + test_null_args(); + test_unknown_descriptor(); + test_parse_cressi_leonardo(); + printf("\nAll parse_raw_dive tests passed.\n"); + return 0; +} diff --git a/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc b/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc index d1393c510..dc73eba4d 100644 --- a/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc +++ b/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc @@ -160,8 +160,27 @@ void DiveComputerHostApiImpl::ParseRawDiveData( int64_t model, const std::vector& data, std::function reply)> result) { - result(FlutterError("UNSUPPORTED", - "Raw dive parsing not yet implemented on Windows")); + libdc_parsed_dive_t dive = {}; + char error_buf[256] = {}; + + int rc = libdc_parse_raw_dive( + vendor.c_str(), product.c_str(), + static_cast(model), + data.data(), static_cast(data.size()), + &dive, error_buf, sizeof(error_buf)); + + if (rc != 0) { + std::string msg = std::string("Failed to parse raw dive data: ") + error_buf; + free(dive.samples); + free(dive.events); + result(FlutterError("PARSE_ERROR", msg)); + return; + } + + auto parsed = ConvertParsedDive(dive); + free(dive.samples); + free(dive.events); + result(parsed); } ErrorOr DiveComputerHostApiImpl::GetLibdivecomputerVersion() { From 6421f0bdf54b9911388445129101714e577f414b Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Thu, 2 Jul 2026 12:59:24 -0400 Subject: [PATCH 2/3] fix(libdc/windows): harden reparse - model bounds check, safe test fixture loader Address PR #345 review: - Validate model range before the int64->unsigned int cast in ParseRawDiveData so a corrupt/out-of-range model returns a clear PARSE_ERROR instead of a silently-wrapped cast and misleading "no descriptor" failure. - load_fixture: check malloc, treat short reads as failure, and null the out-param on every failure path (fixes UB on allocation failure); add test_load_fixture_missing to cover the contract. - Add CONFIGURE_DEPENDS to the libdivecomputer source glob so the native test target re-syncs when the submodule adds/removes sources. --- .../test/native/CMakeLists.txt | 6 +++-- .../test/native/test_parse_raw_dive.c | 23 ++++++++++++++++--- .../windows/dive_computer_host_api_impl.cc | 13 +++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/libdivecomputer_plugin/test/native/CMakeLists.txt b/packages/libdivecomputer_plugin/test/native/CMakeLists.txt index ebfa3420c..b5e7091a2 100644 --- a/packages/libdivecomputer_plugin/test/native/CMakeLists.txt +++ b/packages/libdivecomputer_plugin/test/native/CMakeLists.txt @@ -89,8 +89,10 @@ else() endif() # Collect libdivecomputer sources needed for parsing (exclude platform I/O -# files that don't compile cross-platform). -file(GLOB LIBDC_ALL_SOURCES "${LIBDC_DIR}/src/*.c") +# files that don't compile cross-platform). CONFIGURE_DEPENDS re-runs the glob +# on every build so the target stays in sync when the submodule adds/removes +# sources, without a manual CMake cache wipe. +file(GLOB LIBDC_ALL_SOURCES CONFIGURE_DEPENDS "${LIBDC_DIR}/src/*.c") list(FILTER LIBDC_ALL_SOURCES EXCLUDE REGEX "serial_posix\\.c$") if(NOT WIN32) list(FILTER LIBDC_ALL_SOURCES EXCLUDE REGEX "serial_win32\\.c$") diff --git a/packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c b/packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c index 29dc2e44f..8388c8db9 100644 --- a/packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c +++ b/packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c @@ -5,17 +5,24 @@ #include #include "libdc_wrapper.h" -/* Load a binary file into a malloc'd buffer. Returns size, or 0 on failure. */ +/* Load a binary file into a malloc'd buffer. On success returns the byte count + and sets *out to the malloc'd buffer (caller frees). On any failure returns 0 + and sets *out to NULL, so the caller can safely free/assert without touching + an uninitialized or partially-filled buffer. */ static unsigned int load_fixture(const char *path, unsigned char **out) { + *out = NULL; FILE *f = fopen(path, "rb"); if (!f) return 0; fseek(f, 0, SEEK_END); long len = ftell(f); fseek(f, 0, SEEK_SET); if (len <= 0) { fclose(f); return 0; } - *out = (unsigned char *)malloc((size_t)len); - size_t read = fread(*out, 1, (size_t)len, f); + unsigned char *buf = (unsigned char *)malloc((size_t)len); + if (!buf) { fclose(f); return 0; } + size_t read = fread(buf, 1, (size_t)len, f); fclose(f); + if (read != (size_t)len) { free(buf); return 0; } + *out = buf; return (unsigned int)read; } @@ -36,6 +43,15 @@ static void test_null_args(void) { printf("PASS: test_null_args\n"); } +/* Error path: a missing fixture must report failure and null the out pointer. */ +static void test_load_fixture_missing(void) { + unsigned char *data = (unsigned char *)0x1; /* poison: must be overwritten */ + unsigned int size = load_fixture("fixtures/does_not_exist.bin", &data); + assert(size == 0); + assert(data == NULL); + printf("PASS: test_load_fixture_missing\n"); +} + /* Error path: unknown descriptor should return NODEVICE. */ static void test_unknown_descriptor(void) { libdc_parsed_dive_t result; @@ -89,6 +105,7 @@ static void test_parse_cressi_leonardo(void) { int main(void) { test_null_args(); + test_load_fixture_missing(); test_unknown_descriptor(); test_parse_cressi_leonardo(); printf("\nAll parse_raw_dive tests passed.\n"); diff --git a/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc b/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc index dc73eba4d..3be0ec7ba 100644 --- a/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc +++ b/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -160,6 +161,18 @@ void DiveComputerHostApiImpl::ParseRawDiveData( int64_t model, const std::vector& data, std::function reply)> result) { + // model arrives as an int64 across the Pigeon boundary but libdivecomputer + // expects an unsigned int descriptor id. Reject out-of-range values up front + // so a corrupt/unexpected model yields a clear error instead of a silently + // wrapped cast and a misleading "no descriptor" failure downstream. + constexpr int64_t kMaxModel = std::numeric_limits::max(); + if (model < 0 || model > kMaxModel) { + result(FlutterError( + "PARSE_ERROR", + "Invalid dive computer model number: " + std::to_string(model))); + return; + } + libdc_parsed_dive_t dive = {}; char error_buf[256] = {}; From 5ea15de87d573f6590f2dded48ff87a58429b59f Mon Sep 17 00:00:00 2001 From: Eric Griffin Date: Thu, 2 Jul 2026 13:38:31 -0400 Subject: [PATCH 3/3] fix(libdc/windows): use UINT_MAX to avoid windows.h max() macro The model bounds check in 6421f0bd used std::numeric_limits::max(), which collides with the function-like max() macro from windows.h (pulled in transitively via the Flutter/BLE plugin headers), breaking the MSVC build with C2589/C2059. UINT_MAX is macro-safe and matches the plugin's existing limit-macro convention (dive_converter.cc). Reproduced the collision under clang to confirm root cause and fix. --- .../windows/dive_computer_host_api_impl.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc b/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc index 3be0ec7ba..4767780d7 100644 --- a/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc +++ b/packages/libdivecomputer_plugin/windows/dive_computer_host_api_impl.cc @@ -3,11 +3,11 @@ #include "dive_converter.h" #include "serial_scanner.h" +#include #include #include #include #include -#include #include #include @@ -165,7 +165,9 @@ void DiveComputerHostApiImpl::ParseRawDiveData( // expects an unsigned int descriptor id. Reject out-of-range values up front // so a corrupt/unexpected model yields a clear error instead of a silently // wrapped cast and a misleading "no descriptor" failure downstream. - constexpr int64_t kMaxModel = std::numeric_limits::max(); + // UINT_MAX (not std::numeric_limits::max()) avoids the windows.h max() macro, + // and the signed 64-bit bound keeps the comparison free of /W4 warnings. + constexpr int64_t kMaxModel = UINT_MAX; if (model < 0 || model > kMaxModel) { result(FlutterError( "PARSE_ERROR",