diff --git a/packages/libdivecomputer_plugin/test/native/CMakeLists.txt b/packages/libdivecomputer_plugin/test/native/CMakeLists.txt index af36a9784..b5e7091a2 100644 --- a/packages/libdivecomputer_plugin/test/native/CMakeLists.txt +++ b/packages/libdivecomputer_plugin/test/native/CMakeLists.txt @@ -80,9 +80,48 @@ target_include_directories(test_hw_ostc3_read 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). 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$") +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_descriptor_match_integration COMMAND test_descriptor_match_integration) add_test(NAME test_hw_ostc3_read COMMAND test_hw_ostc3_read) +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 000000000..dc4ebe528 Binary files /dev/null and b/packages/libdivecomputer_plugin/test/native/fixtures/dive1_raw.bin differ 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..8388c8db9 --- /dev/null +++ b/packages/libdivecomputer_plugin/test/native/test_parse_raw_dive.c @@ -0,0 +1,113 @@ +#include +#include +#include +#include +#include +#include "libdc_wrapper.h" + +/* 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; } + 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; +} + +/* 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: 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; + 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_load_fixture_missing(); + 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..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,6 +3,7 @@ #include "dive_converter.h" #include "serial_scanner.h" +#include #include #include #include @@ -160,8 +161,41 @@ 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")); + // 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. + // 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", + "Invalid dive computer model number: " + std::to_string(model))); + return; + } + + 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() {