Final-review fixes for Plan 2 (fit-rewriter). Every failure mode below now surfaces as FitFormatError so Plan 3 can classify invalid FIT input as a non-retryable activity error (spec 10.4). - Range-check numeric values against the field's declared size before struct.pack, so an oversized serial number or a 1-byte product field raises FitFormatError instead of leaking a raw struct.error. - Reject zero-size field definitions during parsing. A zero-size device_info field 0 read back as device_index == 0 via int.from_bytes(b"", ...), which could have let a paired sensor be rewritten as an Edge 1030 Plus (spec 10.2). - Add DeviceFieldValue.is_creator so callers can tell the creator device_info record from sensor records instead of silently keeping whichever record appeared last. - Implement the missing spec 10.4 post-patch step: read the patched buffer back and verify file_id 1/2/8 and creator device_info 2/4/27 hold the target values. A field that could not be written (e.g. a product_name field too small for the target string) now fails the whole conversion rather than producing a silent partial patch. Verification runs before the output is written, so a half-rewritten file never lands on disk. - Use the field's actual endianness in _read_field_value's fallback path. - Add curated re-exports in app/fit/__init__.py for Plan 3. - Document _iter_data_fields' caller invariant (validate the container first; end_offset is not clamped). - Extend the preservation fixture with a product_name string field so the zero-filling string write path is covered by the byte-preservation proof, and test convert_fit_device against a 12-byte header. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
49 lines
1.1 KiB
Python
49 lines
1.1 KiB
Python
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FieldDefinition:
|
|
num: int
|
|
size: int
|
|
base_type: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LocalDefinition:
|
|
global_message_num: int
|
|
endian: str
|
|
fields: tuple[FieldDefinition, ...]
|
|
developer_field_size: int
|
|
|
|
|
|
GARMIN_MANUFACTURER_ID = 1
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GarminDevice:
|
|
manufacturer_id: int = GARMIN_MANUFACTURER_ID
|
|
product_id: int = 3570
|
|
product_name: str = "Edge 1030 Plus"
|
|
serial_number: int | None = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FitConversionResult:
|
|
source_path: Path
|
|
output_path: Path
|
|
patched_field_count: int
|
|
header_crc: int | None
|
|
file_crc: int
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DeviceFieldValue:
|
|
global_message_num: int
|
|
field_num: int
|
|
value: int | str
|
|
#: True when this value came from the creator device record. ``file_id``
|
|
#: values are always creator values (a FIT file has exactly one file_id);
|
|
#: ``device_info`` values are creator values only when device_index == 0.
|
|
is_creator: bool
|