Garmin: Fix fit file checksum computation

This commit is contained in:
José Rebelo
2025-03-08 17:55:04 +00:00
committed by José Rebelo
parent 10412d8eb7
commit 624fb5cf64
9 changed files with 40 additions and 15 deletions
@@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class FitViewerActivity extends AbstractGBActivity implements MenuProvider {
@@ -85,7 +86,7 @@ public class FitViewerActivity extends AbstractGBActivity implements MenuProvide
try {
fitFile = FitFile.parseIncoming(fitPath);
} catch (final IOException e) {
} catch (final IOException | FitParseException e) {
GB.toast("Failed to parse fit file", Toast.LENGTH_LONG, GB.ERROR);
LOG.error("Failed to parse fit file", e);
finish();
@@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitCourse;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitFileId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitWorkout;
@@ -50,6 +51,7 @@ public class GarminFitFileInstallHandler implements InstallHandler {
private byte[] rawBytes;
private FitFile fitFile;
private FileType.FILETYPE fileType;
private FitParseException fitParseException;
public GarminFitFileInstallHandler(final Uri uri, final Context context) {
this.mContext = context;
@@ -99,6 +101,9 @@ public class GarminFitFileInstallHandler implements InstallHandler {
}
fileType = fitFileId.getType();
} catch (final FitParseException e) {
LOG.error("Fit file is corrupted", e);
fitParseException = e;
} catch (final Exception e) {
LOG.error("Failed to read fit file", e);
}
@@ -106,11 +111,18 @@ public class GarminFitFileInstallHandler implements InstallHandler {
@Override
public boolean isValid() {
return fitFile != null && fileType != null;
// If we got a fitParseException, the file is "valid" (a fit file) for this handler, but corrupted
return fitParseException != null || (fitFile != null && fileType != null);
}
@Override
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
if (fitParseException != null) {
installActivity.setInfoText(fitParseException.getLocalizedMessage());
installActivity.setInstallEnabled(false);
return;
}
if (fitFile == null || fileType == null) {
return;
}
@@ -28,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitLap;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
@@ -79,7 +80,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
final FitFile fitFile;
try {
fitFile = FitFile.parseIncoming(file);
} catch (final IOException e) {
} catch (final IOException | FitParseException e) {
LOG.error("Failed to parse fit file", e);
return summary;
}
@@ -18,6 +18,7 @@ import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecordDataFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
@@ -50,12 +51,12 @@ public class FitFile {
}
}
public static FitFile parseIncoming(File file) throws IOException {
public static FitFile parseIncoming(File file) throws IOException, FitParseException {
return parseIncoming(readFileToByteArray(file));
}
//TODO: process file in chunks??
public static FitFile parseIncoming(byte[] fileContents) {
public static FitFile parseIncoming(byte[] fileContents) throws FitParseException {
final GarminByteBufferReader garminByteBufferReader = new GarminByteBufferReader(fileContents);
garminByteBufferReader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
@@ -73,7 +74,7 @@ public class FitFile {
final Integer timeOffset = recordHeader.getTimeOffset();
if (timeOffset != null) {
if (referenceTimestamp == null) {
throw new IllegalArgumentException("Got compressed timestamp without knowing current timestamp");
throw new FitParseException("Got compressed timestamp without knowing current timestamp");
}
if (timeOffset >= (referenceTimestamp & 0x1FL)) {
@@ -104,9 +105,10 @@ public class FitFile {
}
}
garminByteBufferReader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
int fileCrc = garminByteBufferReader.readShort();
if (fileCrc != ChecksumCalculator.computeCrc(fileContents, header.getHeaderSize(), fileContents.length - header.getHeaderSize() - 2)) {
throw new IllegalArgumentException("Wrong CRC for FIT file");
final int fileCrc = garminByteBufferReader.readShort();
final int actualCrc = ChecksumCalculator.computeCrc(fileContents, 0, fileContents.length - 2);
if (fileCrc != actualCrc) {
throw new FitParseException("Wrong CRC for FIT file: got " + actualCrc + " expected " + fileCrc);
}
return new FitFile(header, dataRecords);
}
@@ -60,6 +60,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent;
@@ -118,7 +119,7 @@ public class FitImporter {
/**
* @noinspection StatementWithEmptyBody
*/
public void importFile(final File file) throws IOException {
public void importFile(final File file) throws IOException, FitParseException {
reset();
final FitFile fitFile = FitFile.parseIncoming(file);
@@ -0,0 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception;
public class FitParseException extends Exception {
public FitParseException(final String message) {
super(message);
}
}
@@ -17,6 +17,7 @@ import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
public final class ActivitySummaryUtils {
@@ -61,7 +62,7 @@ public final class ActivitySummaryUtils {
return null;
}
private static File convertFitToGpx(final BaseActivitySummary summary, final File file) throws IOException, ActivityTrackExporter.GPXTrackEmptyException {
private static File convertFitToGpx(final BaseActivitySummary summary, final File file) throws IOException, ActivityTrackExporter.GPXTrackEmptyException, FitParseException {
final FitFile fitFile = FitFile.parseIncoming(file);
final List<ActivityPoint> activityPoints = fitFile.getRecords().stream()
.filter(r -> r instanceof FitRecord)
@@ -15,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDat
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.exception.FitParseException;
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -270,7 +271,7 @@ public class GarminSupportTest extends TestBase {
}
@Test
public void TestFitFileSettings2() {
public void TestFitFileSettings2() throws FitParseException {
byte[] fileContents = GB.hexStringToByteArray("0e101405b90600002e464954b18b40000000000603048c04048601028402" +
"028405028400010000ed2adce7ffffffff01001906ffff02410000310002" +
@@ -356,7 +357,7 @@ public class GarminSupportTest extends TestBase {
}
@Test
public void TestFitFileDevelopersField() {
public void TestFitFileDevelopersField() throws FitParseException {
byte[] fileContents = GB.hexStringToByteArray("0e206806a20000002e464954bed040000100000401028400010002028403048c00000f042329000006a540000100cf0201100d030102000101020305080d1522375990e97962db0040000100ce05000102010102020102031107080a0700000001646f7567686e7574735f6561726e656400646f7567686e7574730060000100140403010204010205048606028401000100008c580000c738b98001008f5a00032c808e400200905c0005a9388a1003d39e");//https://github.com/polyvertex/fitdecode/blob/48b6554d8a3baf33f8b5b9b2fd079fcbe9ac8ce2/tests/files/DeveloperData.fit
String expectedOutput = "[" +
@@ -4,12 +4,11 @@ import org.junit.Ignore;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
public class FitImporterTest {
@Test
@Ignore("helper test for development, remove this while debugging")
public void localTest() throws IOException {
public void localTest() throws Exception {
final FitImporter fitImporter = new FitImporter( null, null);
fitImporter.importFile(new File("/storage/SKIN_TEMP.fit"));
}