fit-file-parser 3.1.3 → 4.0.1

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.
@@ -11,6 +11,29 @@ const CompressedLocalMsgNumMask = 0x60;
11
11
  const CompressedHeaderMask = 0x80;
12
12
  const GarminTimeOffset = 631065600000;
13
13
  let monitoring_timestamp = 0;
14
+ const InvalidFieldData = Symbol('invalid FIT field data');
15
+ const formatTypeMetadata = new Map();
16
+ function requiresBoundedEndianDataView(type, size) {
17
+ switch (type) {
18
+ case 'sint16':
19
+ case 'uint16':
20
+ case 'uint16z':
21
+ return size < 2;
22
+ case 'sint32':
23
+ case 'uint32':
24
+ case 'uint32z':
25
+ case 'float32':
26
+ return size < 4;
27
+ case 'float64':
28
+ return size < 8;
29
+ case 'uint16_array':
30
+ return size % 2 !== 0;
31
+ case 'uint32_array':
32
+ return size % 4 !== 0;
33
+ default:
34
+ return false;
35
+ }
36
+ }
14
37
  function addEndian(littleEndian, bytes) {
15
38
  let result = 0;
16
39
  if (!littleEndian)
@@ -20,7 +43,8 @@ function addEndian(littleEndian, bytes) {
20
43
  }
21
44
  return result;
22
45
  }
23
- function readData(blob, fDef, startIndex, options) {
46
+ function readData(blob, dataView, fDef, startIndex, options) {
47
+ var _a;
24
48
  if (fDef.type === 'uint8_array') {
25
49
  const array8 = [];
26
50
  for (let i = 0; i < fDef.size; i++) {
@@ -29,39 +53,50 @@ function readData(blob, fDef, startIndex, options) {
29
53
  return array8;
30
54
  }
31
55
  if (fDef.endianAbility) {
32
- const temp = [];
33
- for (let i = 0; i < fDef.size; i++) {
34
- temp.push(blob[startIndex + i]);
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;
35
72
  }
36
- const { buffer } = new Uint8Array(temp);
37
- const dataView = new DataView(buffer);
38
73
  try {
39
74
  switch (fDef.type) {
40
75
  case 'sint16':
41
- return dataView.getInt16(0, fDef.littleEndian);
76
+ return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
42
77
  case 'uint16':
43
78
  case 'uint16z':
44
- return dataView.getUint16(0, fDef.littleEndian);
79
+ return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
45
80
  case 'sint32':
46
- return dataView.getInt32(0, fDef.littleEndian);
81
+ return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
47
82
  case 'uint32':
48
83
  case 'uint32z':
49
- return dataView.getUint32(0, fDef.littleEndian);
84
+ return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
50
85
  case 'float32':
51
- return dataView.getFloat32(0, fDef.littleEndian);
86
+ return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
52
87
  case 'float64':
53
- return dataView.getFloat64(0, fDef.littleEndian);
88
+ return fieldDataView.getFloat64(fieldStartIndex, fDef.littleEndian);
54
89
  case 'uint32_array': {
55
90
  const array32 = [];
56
91
  for (let i = 0; i < fDef.size; i += 4) {
57
- array32.push(dataView.getUint32(i, fDef.littleEndian));
92
+ array32.push(fieldDataView.getUint32(fieldStartIndex + i, fDef.littleEndian));
58
93
  }
59
94
  return array32;
60
95
  }
61
96
  case 'uint16_array': {
62
97
  const array16 = [];
63
98
  for (let i = 0; i < fDef.size; i += 2) {
64
- array16.push(dataView.getUint16(i, fDef.littleEndian));
99
+ array16.push(fieldDataView.getUint16(fieldStartIndex + i, fDef.littleEndian));
65
100
  }
66
101
  return array16;
67
102
  }
@@ -72,6 +107,10 @@ function readData(blob, fDef, startIndex, options) {
72
107
  throw e;
73
108
  }
74
109
  }
110
+ const temp = [];
111
+ for (let i = 0; i < fDef.size; i++) {
112
+ temp.push(blob[startIndex + i]);
113
+ }
75
114
  return addEndian(fDef.littleEndian, temp);
76
115
  }
77
116
  if (fDef.type === 'string') {
@@ -123,30 +162,37 @@ function formatByType(data, type, scale, offset) {
123
162
  return scale ? data / scale + offset : data;
124
163
  default:
125
164
  {
126
- if (!fit_js_1.FIT.types[type]) {
165
+ const typeMap = fit_js_1.FIT.types[type];
166
+ if (!typeMap) {
127
167
  return data;
128
168
  }
129
- // Quick check for a mask
130
- const values = [];
131
- for (const key in fit_js_1.FIT.types[type]) {
132
- if (key in fit_js_1.FIT.types[type]) {
133
- values.push(String(fit_js_1.FIT.types[type][key]));
169
+ let metadata = formatTypeMetadata.get(type);
170
+ if (!metadata) {
171
+ const entries = [];
172
+ let hasMask = false;
173
+ for (const key in typeMap) {
174
+ if (key in typeMap) {
175
+ const value = typeMap[key];
176
+ entries.push([key, value]);
177
+ if (String(value) === 'mask') {
178
+ hasMask = true;
179
+ }
180
+ }
134
181
  }
182
+ metadata = { entries, hasMask, typeMap };
183
+ formatTypeMetadata.set(type, metadata);
135
184
  }
136
- if (!values.includes('mask')) {
137
- const typeMap = fit_js_1.FIT.types[type];
138
- const mapped = typeMap[String(data)];
185
+ if (!metadata.hasMask) {
186
+ const mapped = metadata.typeMap[String(data)];
139
187
  return mapped === undefined ? data : mapped;
140
188
  }
141
189
  const dataItem = {};
142
- for (const key in fit_js_1.FIT.types[type]) {
143
- if (key in fit_js_1.FIT.types[type]) {
144
- if (fit_js_1.FIT.types[type][key] === 'mask') {
145
- dataItem.value = data & Number(key);
146
- }
147
- else {
148
- dataItem[fit_js_1.FIT.types[type][key]] = !!((data & Number(key)) >> 7); // Not sure if we need the >> 7 and casting to boolean but from all the masked props of fields so far this seems to be the case
149
- }
190
+ for (const [key, value] of metadata.entries) {
191
+ if (value === 'mask') {
192
+ dataItem.value = data & Number(key);
193
+ }
194
+ else {
195
+ dataItem[value] = !!((data & Number(key)) >> 7); // Not sure if we need the >> 7 and casting to boolean but from all the masked props of fields so far this seems to be the case
150
196
  }
151
197
  }
152
198
  return dataItem;
@@ -279,8 +325,22 @@ function applyOptions(data, field, options, fields) {
279
325
  return data;
280
326
  }
281
327
  }
282
- function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime) {
283
- var _a, _b;
328
+ function applyGarminProductName(fields) {
329
+ if (fields.product_name !== undefined || fields.manufacturer !== 'garmin') {
330
+ return;
331
+ }
332
+ const product = fields.product;
333
+ if (typeof product !== 'number') {
334
+ return;
335
+ }
336
+ const productName = fit_js_1.FIT.types.garmin_product[product];
337
+ if (typeof productName === 'string') {
338
+ // Keep the raw protocol ID and expose the SDK product name separately.
339
+ fields.product_name = productName;
340
+ }
341
+ }
342
+ function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
343
+ var _a, _b, _c, _d, _e;
284
344
  const recordHeader = blob[startIndex];
285
345
  let localMessageType = recordHeader & 15;
286
346
  if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
@@ -304,12 +364,13 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
304
364
  ]),
305
365
  numberOfFields: numberOfFields + numberOfDeveloperDataFields,
306
366
  fieldDefs: [],
367
+ rawData: [],
307
368
  };
308
369
  const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
309
370
  for (let i = 0; i < numberOfFields; i++) {
310
371
  const fDefIndex = startIndex + 6 + i * 3;
311
372
  const baseType = blob[fDefIndex + 2];
312
- const { field, type } = message.getAttributes(blob[fDefIndex]);
373
+ const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
313
374
  const fDef = {
314
375
  type,
315
376
  fDefNo: blob[fDefIndex],
@@ -319,6 +380,9 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
319
380
  baseTypeNo: baseType,
320
381
  name: field,
321
382
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
383
+ scale,
384
+ offset,
385
+ requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
322
386
  };
323
387
  mTypeDef.fieldDefs.push(fDef);
324
388
  }
@@ -343,6 +407,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
343
407
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
344
408
  scale: devDef.scale || 1,
345
409
  offset: devDef.offset || 0,
410
+ requiresBoundedDataView: requiresBoundedEndianDataView(fit_js_1.FIT.types.fit_base_type[baseType], size),
346
411
  developerDataIndex: devDataIndex,
347
412
  isDeveloperField: true,
348
413
  };
@@ -355,6 +420,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
355
420
  throw e;
356
421
  }
357
422
  }
423
+ mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
358
424
  messageTypes[localMessageType] = mTypeDef;
359
425
  const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
360
426
  const nextIndexWithDeveloperData = nextIndex + 1;
@@ -370,40 +436,58 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
370
436
  let readDataFromIndex = startIndex + 1;
371
437
  const fields = {};
372
438
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
373
- const rawFields = [];
439
+ const rawData = (_a = messageType.rawData) !== null && _a !== void 0 ? _a : (messageType.rawData = Array.from({ length: messageType.fieldDefs.length }, () => InvalidFieldData));
440
+ let validFieldCount = 0;
374
441
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
375
442
  const fDef = messageType.fieldDefs[i];
376
- const data = readData(blob, fDef, readDataFromIndex, options);
443
+ const data = readData(blob, dataView, fDef, readDataFromIndex, options);
377
444
  if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
378
- rawFields.push({ fDef, data });
445
+ rawData[i] = data;
446
+ validFieldCount++;
447
+ }
448
+ else {
449
+ rawData[i] = InvalidFieldData;
379
450
  }
380
451
  readDataFromIndex += fDef.size;
381
452
  messageSize += fDef.size;
382
453
  }
383
- for (const { fDef, data } of rawFields) {
384
- const { field } = fDef.isDeveloperField ? { field: fDef.name } : message.getAttributes(fDef.fDefNo);
454
+ for (let i = 0; i < messageType.fieldDefs.length; i++) {
455
+ const data = rawData[i];
456
+ if (data === InvalidFieldData) {
457
+ continue;
458
+ }
459
+ const fDef = messageType.fieldDefs[i];
460
+ const field = fDef.name;
385
461
  if (field !== 'unknown' && field !== '' && field !== undefined) {
386
462
  fields[field] = data;
387
463
  }
388
464
  }
389
- for (const { fDef, data } of rawFields) {
465
+ for (let i = 0; i < messageType.fieldDefs.length; i++) {
466
+ const data = rawData[i];
467
+ if (data === InvalidFieldData) {
468
+ continue;
469
+ }
470
+ const fDef = messageType.fieldDefs[i];
390
471
  if (fDef.isDeveloperField) {
391
472
  const field = fDef.name;
392
473
  const { type } = fDef;
393
- const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
394
- const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
474
+ const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
475
+ const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
395
476
  fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
396
477
  }
397
478
  else {
398
- const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
479
+ const { name: field, type } = fDef;
480
+ const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
481
+ const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
399
482
  if (field !== 'unknown' && field !== '' && field !== undefined) {
400
483
  fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
401
484
  }
402
485
  }
403
- if (message.name === 'record' && options.elapsedRecordField) {
404
- fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
405
- fields.timer_time = fields.elapsed_time - pausedTime;
406
- }
486
+ }
487
+ applyGarminProductName(fields);
488
+ if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
489
+ fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
490
+ fields.timer_time = fields.elapsed_time - pausedTime;
407
491
  }
408
492
  if (message.name === 'field_description') {
409
493
  developerFields[fields.developer_data_index]
@@ -32,7 +32,10 @@ class FitParser {
32
32
  }
33
33
  parse(content, callback) {
34
34
  var _a;
35
- const blob = new Uint8Array((0, binary_js_1.getArrayBuffer)(content));
35
+ const blob = content instanceof ArrayBuffer
36
+ ? new Uint8Array(content)
37
+ : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
38
+ const dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
36
39
  if (blob.length < 12) {
37
40
  callback('File to small to be a FIT file', undefined);
38
41
  if (!this.options.force) {
@@ -56,27 +59,25 @@ class FitParser {
56
59
  return;
57
60
  }
58
61
  }
59
- if (headerLength === 14) {
62
+ if (headerLength === 14 && !this.options.force) {
60
63
  const crcHeader = blob[12] + (blob[13] << 8);
61
64
  const crcHeaderCalc = (0, binary_js_1.calculateCRC)(blob, 0, 12);
62
65
  if (crcHeader !== crcHeaderCalc) {
63
66
  // callback('Header CRC mismatch', {});
64
67
  // TODO: fix Header CRC check
65
- if (!this.options.force) {
66
- return;
67
- }
68
+ return;
68
69
  }
69
70
  }
70
71
  const protocolVersion = blob[1];
71
72
  const profileVersion = blob[2] + (blob[3] << 8);
72
73
  const dataLength = blob[4] + (blob[5] << 8) + (blob[6] << 16) + (blob[7] << 24);
73
74
  const crcStart = dataLength + headerLength;
74
- const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
75
- const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, headerLength === 12 ? 0 : headerLength, crcStart);
76
- if (crcFile !== crcFileCalc) {
77
- // callback('File CRC mismatch', {});
78
- // TODO: fix File CRC check
79
- if (!this.options.force) {
75
+ if (!this.options.force) {
76
+ const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
77
+ const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, headerLength === 12 ? 0 : headerLength, crcStart);
78
+ if (crcFile !== crcFileCalc) {
79
+ // callback('File CRC mismatch', {});
80
+ // TODO: fix File CRC check
80
81
  return;
81
82
  }
82
83
  }
@@ -120,7 +121,7 @@ class FitParser {
120
121
  let lastStopTimestamp;
121
122
  let pausedTime = 0;
122
123
  while (loopIndex < crcStart) {
123
- const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime);
124
+ const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView);
124
125
  loopIndex = nextIndex;
125
126
  switch (messageType) {
126
127
  case 'lap':
package/dist/cjs/fit.d.ts CHANGED
@@ -8,8 +8,9 @@ export interface FieldDefinition {
8
8
  baseTypeNo: number;
9
9
  name: string;
10
10
  dataType: string;
11
- scale?: number;
11
+ scale?: number | null;
12
12
  offset?: number;
13
+ requiresBoundedDataView?: boolean;
13
14
  developerDataIndex?: number;
14
15
  isDeveloperField?: boolean;
15
16
  }