fit-file-parser 4.0.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,30 +9,47 @@ const fit_js_1 = require("./fit.js");
9
9
  const messages_js_1 = require("./messages.js");
10
10
  const CompressedLocalMsgNumMask = 0x60;
11
11
  const CompressedHeaderMask = 0x80;
12
+ const CompressedTimestampMask = 0x1F;
12
13
  const GarminTimeOffset = 631065600000;
13
- let monitoring_timestamp = 0;
14
14
  const InvalidFieldData = Symbol('invalid FIT field data');
15
15
  const formatTypeMetadata = new Map();
16
- function requiresBoundedEndianDataView(type, size) {
16
+ const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
17
+ function baseTypeSize(type) {
17
18
  switch (type) {
19
+ case 'enum':
20
+ case 'sint8':
21
+ case 'uint8':
22
+ case 'uint8z':
23
+ case 'byte':
24
+ return 1;
18
25
  case 'sint16':
19
26
  case 'uint16':
20
27
  case 'uint16z':
21
- return size < 2;
28
+ return 2;
22
29
  case 'sint32':
23
30
  case 'uint32':
24
31
  case 'uint32z':
25
32
  case 'float32':
26
- return size < 4;
33
+ return 4;
27
34
  case 'float64':
28
- return size < 8;
29
- case 'uint16_array':
30
- return size % 2 !== 0;
31
- case 'uint32_array':
32
- return size % 4 !== 0;
35
+ return 8;
33
36
  default:
34
- return false;
37
+ return undefined;
38
+ }
39
+ }
40
+ function requiresBoundedEndianDataView(type, size) {
41
+ const elementSize = baseTypeSize(type);
42
+ return elementSize !== undefined && size % elementSize !== 0;
43
+ }
44
+ function areProfileBaseTypesCompatible(profileType, wireType) {
45
+ if (profileType === undefined || profileType === wireType) {
46
+ return true;
35
47
  }
48
+ // FIT producers commonly interchange enum, uint8, and byte definitions.
49
+ // They have the same width and value representation; keeping the semantic
50
+ // profile type is safe while still rejecting width and sign conflicts.
51
+ return uint8CompatibleTypes.has(String(profileType))
52
+ && uint8CompatibleTypes.has(String(wireType));
36
53
  }
37
54
  function addEndian(littleEndian, bytes) {
38
55
  let result = 0;
@@ -43,77 +60,10 @@ function addEndian(littleEndian, bytes) {
43
60
  }
44
61
  return result;
45
62
  }
46
- function readData(blob, dataView, fDef, startIndex, options) {
47
- var _a;
48
- if (fDef.type === 'uint8_array') {
49
- const array8 = [];
50
- for (let i = 0; i < fDef.size; i++) {
51
- array8.push(blob[startIndex + i]);
52
- }
53
- return array8;
54
- }
55
- if (fDef.endianAbility) {
56
- const requiresBoundedDataView = (_a = fDef.requiresBoundedDataView) !== null && _a !== void 0 ? _a : (fDef.requiresBoundedDataView = requiresBoundedEndianDataView(fDef.type, fDef.size));
57
- let fieldDataView = dataView;
58
- let fieldStartIndex = startIndex;
59
- if (startIndex < 0
60
- || startIndex + fDef.size > blob.length
61
- || startIndex + fDef.size > dataView.byteLength) {
62
- const paddedField = new Uint8Array(fDef.size);
63
- for (let index = 0; index < fDef.size; index++) {
64
- paddedField[index] = blob[startIndex + index];
65
- }
66
- fieldDataView = new DataView(paddedField.buffer);
67
- fieldStartIndex = 0;
68
- }
69
- else if (requiresBoundedDataView) {
70
- fieldDataView = new DataView(dataView.buffer, dataView.byteOffset + startIndex, fDef.size);
71
- fieldStartIndex = 0;
72
- }
73
- try {
74
- switch (fDef.type) {
75
- case 'sint16':
76
- return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
77
- case 'uint16':
78
- case 'uint16z':
79
- return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
80
- case 'sint32':
81
- return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
82
- case 'uint32':
83
- case 'uint32z':
84
- return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
85
- case 'float32':
86
- return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
87
- case 'float64':
88
- return fieldDataView.getFloat64(fieldStartIndex, fDef.littleEndian);
89
- case 'uint32_array': {
90
- const array32 = [];
91
- for (let i = 0; i < fDef.size; i += 4) {
92
- array32.push(fieldDataView.getUint32(fieldStartIndex + i, fDef.littleEndian));
93
- }
94
- return array32;
95
- }
96
- case 'uint16_array': {
97
- const array16 = [];
98
- for (let i = 0; i < fDef.size; i += 2) {
99
- array16.push(fieldDataView.getUint16(fieldStartIndex + i, fDef.littleEndian));
100
- }
101
- return array16;
102
- }
103
- }
104
- }
105
- catch (e) {
106
- if (!options.force) {
107
- throw e;
108
- }
109
- }
110
- const temp = [];
111
- for (let i = 0; i < fDef.size; i++) {
112
- temp.push(blob[startIndex + i]);
113
- }
114
- return addEndian(fDef.littleEndian, temp);
115
- }
116
- if (fDef.type === 'string') {
63
+ function readData(blob, dataView, fDef, startIndex) {
64
+ var _a, _b;
65
+ const rawType = (_b = (_a = fDef.rawType) !== null && _a !== void 0 ? _a : fit_js_1.FIT.types.fit_base_type[fDef.baseTypeNo]) !== null && _b !== void 0 ? _b : fDef.type;
66
+ if (rawType === 'string') {
117
67
  const temp = [];
118
68
  for (let i = 0; i < fDef.size; i++) {
119
69
  if (blob[startIndex + i]) {
@@ -122,18 +72,57 @@ function readData(blob, dataView, fDef, startIndex, options) {
122
72
  }
123
73
  return buffer_1.Buffer.from(temp).toString('utf-8');
124
74
  }
125
- if (fDef.type === 'byte_array') {
126
- const temp = [];
127
- for (let i = 0; i < fDef.size; i++) {
128
- temp.push(blob[startIndex + i]);
129
- }
130
- return temp;
75
+ const elementSize = baseTypeSize(rawType);
76
+ if (elementSize === undefined) {
77
+ return InvalidFieldData;
78
+ }
79
+ if (fDef.size < elementSize
80
+ || fDef.size % elementSize !== 0
81
+ || startIndex < 0
82
+ || startIndex + fDef.size > blob.length
83
+ || startIndex + fDef.size > dataView.byteLength) {
84
+ return InvalidFieldData;
131
85
  }
132
- if (fDef.type === 'sint8') {
133
- const val = blob[startIndex];
134
- return val > 127 ? val - 256 : val;
86
+ const isArray = fDef.array === true
87
+ || String(fDef.type).endsWith('_array')
88
+ || fDef.type === 'exercise_category_array'
89
+ || (fDef.isDeveloperField === true && fDef.size > elementSize);
90
+ const elementCount = isArray ? fDef.size / elementSize : 1;
91
+ const values = [];
92
+ for (let elementIndex = 0; elementIndex < elementCount; elementIndex++) {
93
+ const offset = startIndex + elementIndex * elementSize;
94
+ switch (rawType) {
95
+ case 'sint8': {
96
+ const value = blob[offset];
97
+ values.push(value > 127 ? value - 256 : value);
98
+ break;
99
+ }
100
+ case 'sint16':
101
+ values.push(dataView.getInt16(offset, fDef.littleEndian));
102
+ break;
103
+ case 'uint16':
104
+ case 'uint16z':
105
+ values.push(dataView.getUint16(offset, fDef.littleEndian));
106
+ break;
107
+ case 'sint32':
108
+ values.push(dataView.getInt32(offset, fDef.littleEndian));
109
+ break;
110
+ case 'uint32':
111
+ case 'uint32z':
112
+ values.push(dataView.getUint32(offset, fDef.littleEndian));
113
+ break;
114
+ case 'float32':
115
+ values.push(dataView.getFloat32(offset, fDef.littleEndian));
116
+ break;
117
+ case 'float64':
118
+ values.push(dataView.getFloat64(offset, fDef.littleEndian));
119
+ break;
120
+ default:
121
+ values.push(blob[offset]);
122
+ break;
123
+ }
135
124
  }
136
- return blob[startIndex];
125
+ return isArray ? values : values[0];
137
126
  }
138
127
  function formatByType(data, type, scale, offset) {
139
128
  switch (type) {
@@ -160,6 +149,16 @@ function formatByType(data, type, scale, offset) {
160
149
  });
161
150
  }
162
151
  return scale ? data / scale + offset : data;
152
+ case 'exercise_category_array':
153
+ if (Array.isArray(data)) {
154
+ return data.map((dataItem) => {
155
+ if (isInvalidValue(dataItem, 'uint16')) {
156
+ return null;
157
+ }
158
+ return formatByType(dataItem, 'exercise_category', scale, offset);
159
+ });
160
+ }
161
+ return formatByType(data, 'exercise_category', scale, offset);
163
162
  default:
164
163
  {
165
164
  const typeMap = fit_js_1.FIT.types[type];
@@ -218,10 +217,9 @@ function isInvalidValue(data, type) {
218
217
  case 'string':
219
218
  return data === 0x00;
220
219
  case 'float32':
221
- return data === 0xFFFFFFFF;
220
+ return Number.isNaN(data);
222
221
  case 'float64':
223
- // eslint-disable-next-line no-loss-of-precision
224
- return data === 0xFFFFFFFFFFFFFFFF;
222
+ return Number.isNaN(data);
225
223
  case 'uint8z':
226
224
  return data === 0x00;
227
225
  case 'uint16z':
@@ -249,6 +247,27 @@ function isInvalidBaseTypeValue(data, baseTypeNo) {
249
247
  const baseType = fit_js_1.FIT.types.fit_base_type[baseTypeNo];
250
248
  return typeof baseType === 'string' ? isInvalidValue(data, baseType) : false;
251
249
  }
250
+ function formatFieldValue(data, fDef, options, fields) {
251
+ var _a, _b, _c;
252
+ const field = fDef.name;
253
+ const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
254
+ const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
255
+ if (Array.isArray(data)
256
+ && !String(fDef.type).endsWith('_array')
257
+ && fDef.type !== 'exercise_category_array') {
258
+ const rawType = (_c = fDef.rawType) !== null && _c !== void 0 ? _c : fit_js_1.FIT.types.fit_base_type[fDef.baseTypeNo];
259
+ return data.map((item) => {
260
+ if (isInvalidValue(item, rawType)) {
261
+ return null;
262
+ }
263
+ return applyOptions(formatByType(item, fDef.type, scale, offset), field, options, fields);
264
+ });
265
+ }
266
+ return applyOptions(formatByType(data, fDef.type, scale, offset), field, options, fields);
267
+ }
268
+ function isOutputFieldName(field) {
269
+ return field !== 'unknown' && field !== '' && field !== undefined;
270
+ }
252
271
  function convertTo(data, unitsList, unitName) {
253
272
  const options = fit_js_1.FIT.options[unitsList];
254
273
  const unit = options[unitName];
@@ -307,8 +326,10 @@ function applyOptions(data, field, options, fields) {
307
326
  return convertTo(data, 'lengthUnits', options.lengthUnit);
308
327
  case 'temperature':
309
328
  case 'min_temperature':
329
+ case 'temperature_min':
310
330
  case 'avg_temperature':
311
331
  case 'max_temperature':
332
+ case 'temperature_max':
312
333
  return convertTo(data, 'temperatureUnits', options.temperatureUnit);
313
334
  case 'pressure':
314
335
  case 'start_pressure':
@@ -340,7 +361,7 @@ function applyGarminProductName(fields) {
340
361
  }
341
362
  }
342
363
  function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, developerFields, options) {
343
- var _a, _b, _c, _d;
364
+ var _a, _b, _c, _d, _e, _f;
344
365
  const description = (_a = developerFields[developerFieldDef.developerDataIndex]) === null || _a === void 0 ? void 0 : _a[developerFieldDef.fieldDefinitionNumber];
345
366
  if (!description) {
346
367
  developerFieldDef.resolvedFieldDef = undefined;
@@ -351,9 +372,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
351
372
  && developerFieldDef.resolvedFieldDef) {
352
373
  return developerFieldDef.resolvedFieldDef;
353
374
  }
354
- const baseType = description.fit_base_type_id;
375
+ const describedBaseType = description.fit_base_type_id;
376
+ const baseType = typeof describedBaseType === 'number'
377
+ ? describedBaseType
378
+ : Number((_b = Object.entries(fit_js_1.FIT.types.fit_base_type).find(([, typeName]) => typeName === describedBaseType)) === null || _b === void 0 ? void 0 : _b[0]);
355
379
  const type = fit_js_1.FIT.types.fit_base_type[baseType];
356
- if (typeof baseType !== 'number' || type === undefined) {
380
+ if (!Number.isInteger(baseType) || type === undefined) {
357
381
  developerFieldDef.resolvedFieldDef = undefined;
358
382
  developerFieldDef.resolvedFrom = undefined;
359
383
  if (options.force) {
@@ -363,15 +387,18 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
363
387
  }
364
388
  const resolvedFieldDef = {
365
389
  type,
390
+ rawType: type,
366
391
  fDefNo: developerFieldDef.fieldDefinitionNumber,
367
392
  size: developerFieldDef.size,
393
+ array: type !== 'string'
394
+ && developerFieldDef.size > ((_c = baseTypeSize(type)) !== null && _c !== void 0 ? _c : developerFieldDef.size),
368
395
  endianAbility: (baseType & 128) === 128,
369
396
  littleEndian,
370
397
  baseTypeNo: baseType,
371
- name: (_b = description.field_name) !== null && _b !== void 0 ? _b : '',
398
+ name: (_d = description.field_name) !== null && _d !== void 0 ? _d : '',
372
399
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
373
- scale: (_c = description.scale) !== null && _c !== void 0 ? _c : 1,
374
- offset: (_d = description.offset) !== null && _d !== void 0 ? _d : 0,
400
+ scale: (_e = description.scale) !== null && _e !== void 0 ? _e : 1,
401
+ offset: (_f = description.offset) !== null && _f !== void 0 ? _f : 0,
375
402
  requiresBoundedDataView: requiresBoundedEndianDataView(type, developerFieldDef.size),
376
403
  developerDataIndex: developerFieldDef.developerDataIndex,
377
404
  isDeveloperField: true,
@@ -380,11 +407,12 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
380
407
  developerFieldDef.resolvedFrom = description;
381
408
  return resolvedFieldDef;
382
409
  }
383
- function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
384
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
410
+ function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
411
+ var _a, _b, _c;
385
412
  const recordHeader = blob[startIndex];
386
413
  let localMessageType = recordHeader & 15;
387
- if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
414
+ const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
415
+ if (isCompressedTimestamp) {
388
416
  // compressed timestamp
389
417
  localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
390
418
  }
@@ -412,19 +440,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
412
440
  for (let i = 0; i < numberOfFields; i++) {
413
441
  const fDefIndex = startIndex + 6 + i * 3;
414
442
  const baseType = blob[fDefIndex + 2];
415
- const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
443
+ const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
444
+ const { field, type, baseType: profileBaseType, array, scale, offset, aliases, } = message.getAttributes(blob[fDefIndex]);
445
+ const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
416
446
  const fDef = {
417
- type,
447
+ type: profileCompatible ? type : wireType,
448
+ rawType: wireType,
418
449
  fDefNo: blob[fDefIndex],
419
450
  size: blob[fDefIndex + 1],
451
+ array: profileCompatible
452
+ ? array === true || String(type).endsWith('_array')
453
+ : false,
420
454
  endianAbility: (baseType & 128) === 128,
421
455
  littleEndian: lEnd,
422
456
  baseTypeNo: baseType,
423
- name: field,
457
+ name: profileCompatible ? field : '',
424
458
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
425
- scale,
426
- offset,
427
- requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
459
+ scale: profileCompatible ? scale : null,
460
+ offset: profileCompatible ? offset : 0,
461
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
462
+ aliases: profileCompatible ? aliases : undefined,
428
463
  };
429
464
  mTypeDef.fieldDefs.push(fDef);
430
465
  }
@@ -446,8 +481,6 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
446
481
  };
447
482
  }
448
483
  const messageType = messageTypes[localMessageType] || messageTypes[0];
449
- // TODO: handle compressed header ((recordHeader & 128) == 128)
450
- // uncompressed header
451
484
  let messageSize = 0;
452
485
  let readDataFromIndex = startIndex + 1;
453
486
  const fields = {};
@@ -458,10 +491,15 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
458
491
  let validFieldCount = 0;
459
492
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
460
493
  const fDef = messageType.fieldDefs[i];
461
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
462
- if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
494
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
495
+ if (data !== InvalidFieldData
496
+ && !isInvalidValue(data, fDef.type)
497
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
463
498
  rawData[i] = data;
464
499
  validFieldCount++;
500
+ if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
501
+ decoderState.lastTimestamp = data;
502
+ }
465
503
  }
466
504
  else {
467
505
  rawData[i] = InvalidFieldData;
@@ -474,8 +512,10 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
474
512
  const rawDataIndex = messageType.fieldDefs.length + i;
475
513
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
476
514
  if (fDef) {
477
- const data = readData(blob, dataView, fDef, readDataFromIndex, options);
478
- if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
515
+ const data = readData(blob, dataView, fDef, readDataFromIndex);
516
+ if (data !== InvalidFieldData
517
+ && !isInvalidValue(data, fDef.type)
518
+ && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
479
519
  rawData[rawDataIndex] = data;
480
520
  validFieldCount++;
481
521
  }
@@ -495,9 +535,15 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
495
535
  continue;
496
536
  }
497
537
  const fDef = messageType.fieldDefs[i];
498
- const field = fDef.name;
499
- if (field !== 'unknown' && field !== '' && field !== undefined) {
500
- fields[field] = data;
538
+ if (isOutputFieldName(fDef.name)) {
539
+ fields[fDef.name] = data;
540
+ }
541
+ if (fDef.aliases) {
542
+ for (const alias of fDef.aliases) {
543
+ if (isOutputFieldName(alias.field)) {
544
+ fields[alias.field] = data;
545
+ }
546
+ }
501
547
  }
502
548
  }
503
549
  for (let i = 0; i < developerFieldDefs.length; i++) {
@@ -517,19 +563,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
517
563
  continue;
518
564
  }
519
565
  const fDef = messageType.fieldDefs[i];
520
- if (fDef.isDeveloperField) {
521
- const field = fDef.name;
522
- const { type } = fDef;
523
- const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
524
- const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
525
- fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
566
+ if (isOutputFieldName(fDef.name)) {
567
+ fields[fDef.name] = formatFieldValue(data, fDef, options, fields);
526
568
  }
527
- else {
528
- const { name: field, type } = fDef;
529
- const scale = (_f = fDef.scale) !== null && _f !== void 0 ? _f : null;
530
- const offset = (_g = fDef.offset) !== null && _g !== void 0 ? _g : 0;
531
- if (field !== 'unknown' && field !== '' && field !== undefined) {
532
- fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
569
+ if (fDef.aliases) {
570
+ for (const alias of fDef.aliases) {
571
+ if (isOutputFieldName(alias.field)) {
572
+ fields[alias.field] = formatFieldValue(data, Object.assign(Object.assign(Object.assign({}, fDef), alias), { name: alias.field, aliases: undefined }), options, fields);
573
+ }
533
574
  }
534
575
  }
535
576
  }
@@ -541,7 +582,24 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
541
582
  }
542
583
  const field = fDef.name;
543
584
  if (field !== 'unknown' && field !== '') {
544
- fields[field] = applyOptions(formatByType(data, fDef.type, (_h = fDef.scale) !== null && _h !== void 0 ? _h : null, (_j = fDef.offset) !== null && _j !== void 0 ? _j : 0), field, options, fields);
585
+ fields[field] = formatFieldValue(data, fDef, options, fields);
586
+ }
587
+ }
588
+ if (isCompressedTimestamp) {
589
+ const previousTimestamp = decoderState.lastTimestamp;
590
+ if (previousTimestamp === undefined) {
591
+ if (!options.force) {
592
+ throw new Error('Compressed timestamp requires a previous timestamp');
593
+ }
594
+ }
595
+ else {
596
+ const timeOffset = recordHeader & CompressedTimestampMask;
597
+ const previousOffset = previousTimestamp & CompressedTimestampMask;
598
+ const rollover = timeOffset < previousOffset ? 0x20 : 0;
599
+ const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
600
+ decoderState.lastTimestamp = timestamp;
601
+ fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
602
+ validFieldCount++;
545
603
  }
546
604
  }
547
605
  applyGarminProductName(fields);
@@ -555,17 +613,20 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
555
613
  developerFields[fields.developer_data_index][fields.field_definition_number] = fields;
556
614
  }
557
615
  if (message.name === 'monitoring') {
558
- // TODO weirdly uses global variables?
559
- // we need to keep the raw timestamp value so we can calculate subsequent timestamp16 fields
560
- if (fields.timestamp) {
561
- monitoring_timestamp = fields.timestamp;
562
- fields.timestamp = new Date(fields.timestamp * 1000 + GarminTimeOffset);
616
+ const timestampFieldIndex = messageType.fieldDefs.findIndex(fieldDefinition => fieldDefinition.fDefNo === 253);
617
+ const rawTimestamp = timestampFieldIndex >= 0
618
+ ? rawData[timestampFieldIndex]
619
+ : InvalidFieldData;
620
+ if (rawTimestamp !== InvalidFieldData && typeof rawTimestamp === 'number') {
621
+ decoderState.monitoringTimestamp = rawTimestamp;
622
+ fields.timestamp = new Date(rawTimestamp * 1000 + GarminTimeOffset);
563
623
  }
564
- if (fields.timestamp16 && !fields.timestamp) {
565
- monitoring_timestamp
566
- += (fields.timestamp16 - (monitoring_timestamp & 0xFFFF)) & 0xFFFF;
567
- // fields.timestamp = monitoring_timestamp;
568
- fields.timestamp = new Date(monitoring_timestamp * 1000 + GarminTimeOffset);
624
+ else if (fields.timestamp16
625
+ && decoderState.monitoringTimestamp !== undefined
626
+ && !fields.timestamp) {
627
+ decoderState.monitoringTimestamp
628
+ += (fields.timestamp16 - (decoderState.monitoringTimestamp & 0xFFFF)) & 0xFFFF;
629
+ fields.timestamp = new Date(decoderState.monitoringTimestamp * 1000 + GarminTimeOffset);
569
630
  }
570
631
  }
571
632
  return {
@@ -31,23 +31,23 @@ class FitParser {
31
31
  });
32
32
  }
33
33
  parse(content, callback) {
34
- var _a;
34
+ var _a, _b;
35
35
  const blob = content instanceof ArrayBuffer
36
36
  ? new Uint8Array(content)
37
37
  : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
38
38
  const dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
39
39
  if (blob.length < 12) {
40
40
  callback('File to small to be a FIT file', undefined);
41
- if (!this.options.force) {
42
- return;
43
- }
41
+ return;
44
42
  }
45
43
  const headerLength = blob[0];
46
44
  if (headerLength !== 14 && headerLength !== 12) {
47
45
  callback('Incorrect header size', undefined);
48
- if (!this.options.force) {
49
- return;
50
- }
46
+ return;
47
+ }
48
+ if (blob.length < headerLength) {
49
+ callback('File to small to be a FIT file', undefined);
50
+ return;
51
51
  }
52
52
  let fileTypeString = '';
53
53
  for (let i = 8; i < 12; i++) {
@@ -55,29 +55,33 @@ class FitParser {
55
55
  }
56
56
  if (fileTypeString !== '.FIT') {
57
57
  callback('Missing \'.FIT\' in header', undefined);
58
- if (!this.options.force) {
59
- return;
60
- }
58
+ return;
61
59
  }
62
60
  if (headerLength === 14 && !this.options.force) {
63
61
  const crcHeader = blob[12] + (blob[13] << 8);
64
62
  const crcHeaderCalc = (0, binary_js_1.calculateCRC)(blob, 0, 12);
65
- if (crcHeader !== crcHeaderCalc) {
66
- // callback('Header CRC mismatch', {});
67
- // TODO: fix Header CRC check
63
+ if (crcHeader !== 0 && crcHeader !== crcHeaderCalc) {
64
+ callback('Header CRC mismatch', undefined);
68
65
  return;
69
66
  }
70
67
  }
71
68
  const protocolVersion = blob[1];
72
69
  const profileVersion = blob[2] + (blob[3] << 8);
73
- const dataLength = blob[4] + (blob[5] << 8) + (blob[6] << 16) + (blob[7] << 24);
70
+ const dataLength = dataView.getUint32(4, true);
74
71
  const crcStart = dataLength + headerLength;
72
+ if (crcStart > blob.length) {
73
+ callback('File data exceeds input length', undefined);
74
+ return;
75
+ }
76
+ if (!this.options.force && crcStart + 2 > blob.length) {
77
+ callback('File CRC missing', undefined);
78
+ return;
79
+ }
75
80
  if (!this.options.force) {
76
81
  const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
77
- const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, headerLength === 12 ? 0 : headerLength, crcStart);
82
+ const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, 0, crcStart);
78
83
  if (crcFile !== crcFileCalc) {
79
- // callback('File CRC mismatch', {});
80
- // TODO: fix File CRC check
84
+ callback('File CRC mismatch', undefined);
81
85
  return;
82
86
  }
83
87
  }
@@ -85,6 +89,7 @@ class FitParser {
85
89
  profileVersion,
86
90
  protocolVersion,
87
91
  };
92
+ const messages = {};
88
93
  let sessions = [];
89
94
  let laps = [];
90
95
  const records = [];
@@ -107,6 +112,7 @@ class FitParser {
107
112
  const tank_updates = [];
108
113
  const tank_summaries = [];
109
114
  const jumps = [];
115
+ const sets = [];
110
116
  const splits = [];
111
117
  const split_summaries = [];
112
118
  const time_in_zone = [];
@@ -115,14 +121,20 @@ class FitParser {
115
121
  let loopIndex = headerLength;
116
122
  const messageTypes = [];
117
123
  const developerFields = [];
124
+ const decoderState = {};
118
125
  const isModeCascade = this.options.mode === 'cascade';
119
126
  const isCascadeNeeded = isModeCascade || this.options.mode === 'both';
120
127
  let startDate;
121
128
  let lastStopTimestamp;
122
129
  let pausedTime = 0;
123
130
  while (loopIndex < crcStart) {
124
- const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView);
131
+ const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState);
125
132
  loopIndex = nextIndex;
133
+ if (messageType !== ''
134
+ && messageType !== 'definition'
135
+ && message !== undefined) {
136
+ ((_a = messages[messageType]) !== null && _a !== void 0 ? _a : (messages[messageType] = [])).push(message);
137
+ }
126
138
  switch (messageType) {
127
139
  case 'lap':
128
140
  laps.push(message);
@@ -210,6 +222,9 @@ class FitParser {
210
222
  case 'jump':
211
223
  jumps.push(message);
212
224
  break;
225
+ case 'set':
226
+ sets.push(message);
227
+ break;
213
228
  case 'split':
214
229
  splits.push(message);
215
230
  break;
@@ -250,15 +265,17 @@ class FitParser {
250
265
  fitObj.time_in_zone = time_in_zone;
251
266
  fitObj.activity_metrics = activity_metrics;
252
267
  fitObj.user_metrics = user_metrics;
268
+ fitObj.messages = messages;
253
269
  if (isCascadeNeeded) {
254
270
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
255
271
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
256
272
  sessions = (0, helper_js_1.mapDataIntoSession)(sessions, laps);
257
- fitObj.activity = Object.assign(Object.assign({}, ((_a = fitObj.activity) !== null && _a !== void 0 ? _a : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
273
+ fitObj.activity = Object.assign(Object.assign({}, ((_b = fitObj.activity) !== null && _b !== void 0 ? _b : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
258
274
  sessions,
259
275
  events,
260
276
  hrv,
261
277
  device_infos, developer_data_ids: applications, field_descriptions: fieldDescriptions, sports,
278
+ sets,
262
279
  splits,
263
280
  split_summaries });
264
281
  }
@@ -272,6 +289,7 @@ class FitParser {
272
289
  fitObj.developer_data_ids = applications;
273
290
  fitObj.field_descriptions = fieldDescriptions;
274
291
  fitObj.hrv = hrv;
292
+ fitObj.sets = sets;
275
293
  }
276
294
  callback(undefined, fitObj);
277
295
  }