From 00ba2770b6c7b8e0a44284dced98c79923b3cf66 Mon Sep 17 00:00:00 2001 From: Ondra Medek Date: Wed, 7 Mar 2018 08:18:47 +0100 Subject: [PATCH] RFCT move crc computation to records.Crc, add test --- fitparse/base.py | 37 ++++++++++++++++++++-------------- fitparse/records.py | 46 +++++++++++++++++++++++++++++++++++++++++++ fitparse/utils.py | 19 ------------------ tests/test.py | 9 ++++----- tests/test_records.py | 25 +++++++++++++++++++++++ 5 files changed, 97 insertions(+), 39 deletions(-) create mode 100644 tests/test_records.py diff --git a/fitparse/base.py b/fitparse/base.py index a37d9e0..bef1639 100644 --- a/fitparse/base.py +++ b/fitparse/base.py @@ -12,11 +12,11 @@ from fitparse.processors import FitFileDataProcessor from fitparse.profile import FIELD_TYPE_TIMESTAMP, MESSAGE_TYPES from fitparse.records import ( - DataMessage, FieldData, FieldDefinition, DevFieldDefinition, DefinitionMessage, MessageHeader, - BASE_TYPES, BASE_TYPE_BYTE, DevField, + Crc, DataMessage, FieldData, FieldDefinition, DevFieldDefinition, DefinitionMessage, MessageHeader, + BASE_TYPES, BASE_TYPE_BYTE, add_dev_data_id, add_dev_field_description, get_dev_type ) -from fitparse.utils import calc_crc, fileish_open, FitParseError, FitEOFError, FitCRCError, FitHeaderError +from fitparse.utils import fileish_open, FitParseError, FitEOFError, FitCRCError, FitHeaderError class FitFile(object): @@ -24,6 +24,7 @@ def __init__(self, fileish, check_crc=True, data_processor=None): self._file = fileish_open(fileish, 'rb') self.check_crc = check_crc + self._crc = None self._processor = data_processor or FitFileDataProcessor() # Get total filesize @@ -55,12 +56,20 @@ def _read(self, size): if size <= 0: return None data = self._file.read(size) - self._crc = calc_crc(data, self._crc) + if size != len(data): + raise FitEOFError("Tried to read %d bytes from .FIT file but got %d" % (size, len(data))) + + if self.check_crc: + self._crc.update(data) self._bytes_left -= len(data) return data def _read_struct(self, fmt, endian='<', data=None, always_tuple=False): - fmt_with_endian = "%s%s" % (endian, fmt) + if fmt.startswith('<') or fmt.startswith('>'): + # fmt contains endian + fmt_with_endian = fmt + else: + fmt_with_endian = "%s%s" % (endian, fmt) size = struct.calcsize(fmt_with_endian) if size <= 0: raise FitParseError("Invalid struct format: %s" % fmt_with_endian) @@ -68,21 +77,19 @@ def _read_struct(self, fmt, endian='<', data=None, always_tuple=False): if data is None: data = self._read(size) - if size != len(data): - raise FitEOFError("Tried to read %d bytes from .FIT file but got %d" % (size, len(data))) - unpacked = struct.unpack(fmt_with_endian, data) # Flatten tuple if it's got only one value return unpacked if (len(unpacked) > 1) or always_tuple else unpacked[0] def _read_and_assert_crc(self, allow_zero=False): # CRC Calculation is little endian from SDK - crc_expected, crc_actual = self._crc, self._read_struct('H') - - if (crc_actual != crc_expected) and not (allow_zero and (crc_actual == 0)): - if self.check_crc: - raise FitCRCError('CRC Mismatch [expected = 0x%04X, actual = 0x%04X]' % ( - crc_expected, crc_actual)) + crc_computed, crc_read = self._crc.value, self._read_struct(Crc.FMT) + if not self.check_crc: + return + if crc_computed == crc_read or (allow_zero and crc_read == 0): + return + raise FitCRCError('CRC Mismatch [computed: 0x%04X, read: 0x%04X]' % ( + crc_computed, crc_read)) ########## # Private Data Parsing Methods @@ -94,7 +101,7 @@ def _parse_file_header(self): self._bytes_left = -1 self._complete = False self._compressed_ts_accumulator = 0 - self._crc = 0 + self._crc = Crc() self._local_mesgs = {} self._messages = [] diff --git a/fitparse/records.py b/fitparse/records.py index 9924e68..75e691f 100644 --- a/fitparse/records.py +++ b/fitparse/records.py @@ -331,6 +331,52 @@ def render(self, raw_value): return raw_value +class Crc(object): + """FIT file CRC computation.""" + + CRC_TABLE = ( + 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, + 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400, + ) + + FMT = '' % (self.__class__.__name__, self.value or "-") + + def __str__(self): + return self.format(self.value) + + def update(self, byte_arr): + """Read bytes and update the CRC computed.""" + if byte_arr: + self.value = self.compute(byte_arr, self.value) + + @staticmethod + def format(value): + """Format CRC value to string.""" + return '0x%04X' % value + + @classmethod + def compute(cls, byte_arr, crc=0): + """Compute CRC for input bytes.""" + for byte in bytearray(byte_arr): + # Taken verbatim from FIT SDK docs + tmp = cls.CRC_TABLE[crc & 0xF] + crc = (crc >> 4) & 0x0FFF + crc = crc ^ tmp ^ cls.CRC_TABLE[byte & 0xF] + + tmp = cls.CRC_TABLE[crc & 0xF] + crc = (crc >> 4) & 0x0FFF + crc = crc ^ tmp ^ cls.CRC_TABLE[(byte >> 4) & 0xF] + return crc + + def parse_string(string): try: end = string.index(0x00) diff --git a/fitparse/utils.py b/fitparse/utils.py index caed865..9f4a367 100644 --- a/fitparse/utils.py +++ b/fitparse/utils.py @@ -16,25 +16,6 @@ class FitHeaderError(FitParseError): pass -CRC_TABLE = ( - 0x0000, 0xCC01, 0xD801, 0x1400, 0xF001, 0x3C00, 0x2800, 0xE401, - 0xA001, 0x6C00, 0x7800, 0xB401, 0x5000, 0x9C01, 0x8801, 0x4400, -) - - -def calc_crc(byte_arr, crc=0): - for byte in bytearray(byte_arr): - # Taken verbatim from FIT SDK docs - tmp = CRC_TABLE[crc & 0xF] - crc = (crc >> 4) & 0x0FFF - crc = crc ^ tmp ^ CRC_TABLE[byte & 0xF] - - tmp = CRC_TABLE[crc & 0xF] - crc = (crc >> 4) & 0x0FFF - crc = crc ^ tmp ^ CRC_TABLE[(byte >> 4) & 0xF] - return crc - - METHOD_NAME_SCRUBBER = re.compile(r'\W|^(?=\d)') UNIT_NAME_TO_FUNC_REPLACEMENTS = ( ('/', ' per '), diff --git a/tests/test.py b/tests/test.py index 8b34082..7ed92d8 100755 --- a/tests/test.py +++ b/tests/test.py @@ -2,15 +2,14 @@ import csv import datetime -import io import os from struct import pack import sys from fitparse import FitFile from fitparse.processors import UTC_REFERENCE, StandardUnitsDataProcessor -from fitparse.records import BASE_TYPES -from fitparse.utils import calc_crc, FitEOFError, FitCRCError, FitHeaderError +from fitparse.records import BASE_TYPES, Crc +from fitparse.utils import FitEOFError, FitCRCError, FitHeaderError if sys.version_info >= (2, 7): import unittest @@ -65,8 +64,8 @@ def generate_fitfile(data=None, endian='<'): # Prototcol version 1.0, profile version 1.52 header = pack('<2BHI4s', 14, 16, 152, len(fit_data), b'.FIT') - file_data = header + pack('= (2, 7): + import unittest +else: + import unittest2 as unittest + + +class RecordsTestCase(unittest.TestCase): + def test_crc(self): + crc = Crc() + self.assertEqual(0, crc.value) + crc.update(b'\x0e\x10\x98\x00(\x00\x00\x00.FIT') + self.assertEqual(0xace7, crc.value) + # 0 must not change the crc + crc.update(0) + self.assertEqual(0xace7, crc.value) + + +if __name__ == '__main__': + unittest.main()