Narrow field values with truncatingIfNeeded instead of the trapping initializer - #15
Conversation
…nitializer rzfit_swift_string_for_type narrows a FIT_UINT32 into the field's own type (FIT_ENUM, FIT_UINT8, FIT_UINT16, ...) with the exact initializer, which traps when the value does not fit. A structurally valid file can carry such a value - a vendor's private enum, or one corrupt byte - and then FitFile.init takes the whole process down before the caller sees a line of it. This changes the generator template and the generated map to truncatingIfNeeded:, which is what the C SDK does when it reads the same bytes into the narrower type, and adds a test with a value that does not fit an enum and one that does not fit a uint16. Found by a mutation fuzz over a corpus of Garmin recordings in a wing-foiling app that imports a rider's whole year from intervals.icu.
|
Thanks a lot for this, and for the very clear write-up — the trap is real and a library should never take the host app down on a bad (or merely unusual) file. Tests pass locally and the generated diff is exactly the mechanical change described. One refinement I'm going to make as a follow-up on main right after merging: I'll also fix the same class of trap in Merging now — thanks again! |
Follow-up to #15: truncatingIfNeeded avoided the trap but mapped out of range values to a plausible but wrong name (e.g. file 0x1_0001 -> "device"). The generator now emits FIT_X(exactly: val) and falls back to the default "fit_type_<type>_<val>" string, keeping the raw value visible. Also guard FIT_UINT16(val) for field_description native_field_num, which could trap on an out of range or NaN double. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Record the exactly:/fallback choice for out-of-range typed values (PR #15 follow-up) in code-generation, fit-message and fit-file docs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The vendored copy of 1.5.2 existed for one word: the generated map narrowed a FIT_UINT32 into the field's own type with the trapping initializer, so a structurally valid FIT carrying a wider value killed the process inside FitFile.init before a line of ours ran. The mutation fuzz found it; roznet/FitFileParser#15 fixed it upstream and was merged on 21 September 2026. The newest release, 1.5.2, predates the fix by three years, so there is no tag to ask for: the pin is .revision("6f50aa1…") on the merge commit, and Package.resolved records the same forty hex digits. It moves to .exact("…") the day upstream cuts a release. The merge also carries Garmin's FIT SDK 21.158 in place of 21.115, so the move was taken with the goldens and the whole fuzz behind it — swift test (1190 tests, 88 suites) and FUZZ_ALL=1 swift test --filter MutationFuzz over every recording in the corpus (96 damaged mutants refused, 96 repaired ones read or refused, none trapped). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mg2rYYNYmQjnvCxJQwsEb7
What
rzfit_swift_string_for_typenarrows aFIT_UINT32into the field's own type (FIT_ENUM,FIT_UINT8,FIT_UINT16, …) with the exact initializer:FIT_ENUM(val)traps whenvaldoes not fit a byte. A structurally valid FIT can carry such a value — a vendor's private enum, or one corrupt byte in an otherwise good file — and thenFitFile.inittakes the whole process down before the caller sees a line of it. There is nothing to catch: it is a Swift runtime trap, not an error.This PR changes the one line in the generator (
python/fitsdkparser.py,swift_stmt_case_type_function_call) and the generatedrzfit_swift_map.swiftit produces totruncatingIfNeeded:, which is what the C SDK does when it reads the same bytes into the narrower type, and adds a test with a value that does not fit an enum and one that does not fit auint16. The generated diff is 179 lines, all of the shapeFIT_X(val)→FIT_X(truncatingIfNeeded: val).How it was found
A mutation fuzz over a corpus of Garmin recordings in a wing-foiling app that imports a rider's whole year from intervals.icu: one tester's first sync crashed the app on their first activity, the crash was inside the decoder, and no amount of validation on our side (header, CRC, message framing) could see it, because the file is valid. We are shipping a vendored copy with this change until it is upstream; thank you for the library.