fit-file-parser 3.1.0 → 4.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Change Log
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### FIT decoder performance
6
+
7
+ - Parse `ArrayBuffer` inputs and exact Node.js `Buffer` views directly, avoiding a full copy of the source file.
8
+ - Reuse one parse-local `DataView` instead of allocating temporary views for each supported endian field.
9
+ - Cache standard field metadata and enum/mask lookups on reusable message definitions.
10
+ - Reuse raw field storage for records that share a local message definition.
11
+ - Generate elapsed and timer fields once per record instead of once per decoded field.
12
+ - Skip header and file CRC scans in `force: true` mode, where CRC mismatches are intentionally ignored; strict mode continues to validate both CRCs.
13
+ - Preserve legacy malformed-field zero-padding, field-boundary, developer-field, invalid-value, and offset-buffer behavior.
14
+
15
+ ### Measured benefit
16
+
17
+ - Suunto 93-hour / 7.5 MB FIT decoding: 1.057 s to 0.375 s, a 64.5% reduction.
18
+ - Garmin 110-hour / 28.6 MB FIT decoding: 3.643 s to 1.241 s, a 65.9% reduction.
19
+ - Input-related array-buffer memory is approximately halved by removing the full source copy:
20
+ - Suunto: 15.0 MB to 7.5 MB.
21
+ - Garmin: 57.2 MB to 28.6 MB.
22
+
23
+ There are no intentional parsed-output or public API changes in this release. Output parity was verified across 162 checked-in fixture/mode combinations, 8,320 generated malformed endian-definition cases, and both private long-duration benchmark files.
24
+
3
25
  ## 3.1.0
4
26
 
5
27
  - Add the public `FitEncoder` API for writing FIT headers, definitions, data messages, and CRCs.
package/dist/binary.d.ts CHANGED
@@ -1,8 +1,16 @@
1
1
  import type { FitParserOptions } from './fit-parser.js';
2
+ import type { FieldDefinition } from './fit.js';
2
3
  import type { MesgNum } from './fit_types.js';
3
4
  import { Buffer } from 'buffer';
5
+ export interface MessageTypeDefinition {
6
+ littleEndian: boolean;
7
+ globalMessageNumber: number;
8
+ numberOfFields: number;
9
+ fieldDefs: FieldDefinition[];
10
+ rawData?: any[];
11
+ }
4
12
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
5
- export declare function readRecord(blob: Uint8Array, messageTypes: any[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number): {
13
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView): {
6
14
  messageType: MesgNum | '';
7
15
  nextIndex: number;
8
16
  message?: any;
package/dist/binary.js CHANGED
@@ -5,6 +5,29 @@ const CompressedLocalMsgNumMask = 0x60;
5
5
  const CompressedHeaderMask = 0x80;
6
6
  const GarminTimeOffset = 631065600000;
7
7
  let monitoring_timestamp = 0;
8
+ const InvalidFieldData = Symbol('invalid FIT field data');
9
+ const formatTypeMetadata = new Map();
10
+ function requiresBoundedEndianDataView(type, size) {
11
+ switch (type) {
12
+ case 'sint16':
13
+ case 'uint16':
14
+ case 'uint16z':
15
+ return size < 2;
16
+ case 'sint32':
17
+ case 'uint32':
18
+ case 'uint32z':
19
+ case 'float32':
20
+ return size < 4;
21
+ case 'float64':
22
+ return size < 8;
23
+ case 'uint16_array':
24
+ return size % 2 !== 0;
25
+ case 'uint32_array':
26
+ return size % 4 !== 0;
27
+ default:
28
+ return false;
29
+ }
30
+ }
8
31
  export function addEndian(littleEndian, bytes) {
9
32
  let result = 0;
10
33
  if (!littleEndian)
@@ -14,7 +37,8 @@ export function addEndian(littleEndian, bytes) {
14
37
  }
15
38
  return result;
16
39
  }
17
- function readData(blob, fDef, startIndex, options) {
40
+ function readData(blob, dataView, fDef, startIndex, options) {
41
+ var _a;
18
42
  if (fDef.type === 'uint8_array') {
19
43
  const array8 = [];
20
44
  for (let i = 0; i < fDef.size; i++) {
@@ -23,39 +47,50 @@ function readData(blob, fDef, startIndex, options) {
23
47
  return array8;
24
48
  }
25
49
  if (fDef.endianAbility) {
26
- const temp = [];
27
- for (let i = 0; i < fDef.size; i++) {
28
- temp.push(blob[startIndex + i]);
50
+ const requiresBoundedDataView = (_a = fDef.requiresBoundedDataView) !== null && _a !== void 0 ? _a : (fDef.requiresBoundedDataView = requiresBoundedEndianDataView(fDef.type, fDef.size));
51
+ let fieldDataView = dataView;
52
+ let fieldStartIndex = startIndex;
53
+ if (startIndex < 0
54
+ || startIndex + fDef.size > blob.length
55
+ || startIndex + fDef.size > dataView.byteLength) {
56
+ const paddedField = new Uint8Array(fDef.size);
57
+ for (let index = 0; index < fDef.size; index++) {
58
+ paddedField[index] = blob[startIndex + index];
59
+ }
60
+ fieldDataView = new DataView(paddedField.buffer);
61
+ fieldStartIndex = 0;
62
+ }
63
+ else if (requiresBoundedDataView) {
64
+ fieldDataView = new DataView(dataView.buffer, dataView.byteOffset + startIndex, fDef.size);
65
+ fieldStartIndex = 0;
29
66
  }
30
- const { buffer } = new Uint8Array(temp);
31
- const dataView = new DataView(buffer);
32
67
  try {
33
68
  switch (fDef.type) {
34
69
  case 'sint16':
35
- return dataView.getInt16(0, fDef.littleEndian);
70
+ return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
36
71
  case 'uint16':
37
72
  case 'uint16z':
38
- return dataView.getUint16(0, fDef.littleEndian);
73
+ return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
39
74
  case 'sint32':
40
- return dataView.getInt32(0, fDef.littleEndian);
75
+ return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
41
76
  case 'uint32':
42
77
  case 'uint32z':
43
- return dataView.getUint32(0, fDef.littleEndian);
78
+ return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
44
79
  case 'float32':
45
- return dataView.getFloat32(0, fDef.littleEndian);
80
+ return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
46
81
  case 'float64':
47
- return dataView.getFloat64(0, fDef.littleEndian);
82
+ return fieldDataView.getFloat64(fieldStartIndex, fDef.littleEndian);
48
83
  case 'uint32_array': {
49
84
  const array32 = [];
50
85
  for (let i = 0; i < fDef.size; i += 4) {
51
- array32.push(dataView.getUint32(i, fDef.littleEndian));
86
+ array32.push(fieldDataView.getUint32(fieldStartIndex + i, fDef.littleEndian));
52
87
  }
53
88
  return array32;
54
89
  }
55
90
  case 'uint16_array': {
56
91
  const array16 = [];
57
92
  for (let i = 0; i < fDef.size; i += 2) {
58
- array16.push(dataView.getUint16(i, fDef.littleEndian));
93
+ array16.push(fieldDataView.getUint16(fieldStartIndex + i, fDef.littleEndian));
59
94
  }
60
95
  return array16;
61
96
  }
@@ -66,6 +101,10 @@ function readData(blob, fDef, startIndex, options) {
66
101
  throw e;
67
102
  }
68
103
  }
104
+ const temp = [];
105
+ for (let i = 0; i < fDef.size; i++) {
106
+ temp.push(blob[startIndex + i]);
107
+ }
69
108
  return addEndian(fDef.littleEndian, temp);
70
109
  }
71
110
  if (fDef.type === 'string') {
@@ -117,30 +156,37 @@ function formatByType(data, type, scale, offset) {
117
156
  return scale ? data / scale + offset : data;
118
157
  default:
119
158
  {
120
- if (!FIT.types[type]) {
159
+ const typeMap = FIT.types[type];
160
+ if (!typeMap) {
121
161
  return data;
122
162
  }
123
- // Quick check for a mask
124
- const values = [];
125
- for (const key in FIT.types[type]) {
126
- if (key in FIT.types[type]) {
127
- values.push(String(FIT.types[type][key]));
163
+ let metadata = formatTypeMetadata.get(type);
164
+ if (!metadata) {
165
+ const entries = [];
166
+ let hasMask = false;
167
+ for (const key in typeMap) {
168
+ if (key in typeMap) {
169
+ const value = typeMap[key];
170
+ entries.push([key, value]);
171
+ if (String(value) === 'mask') {
172
+ hasMask = true;
173
+ }
174
+ }
128
175
  }
176
+ metadata = { entries, hasMask, typeMap };
177
+ formatTypeMetadata.set(type, metadata);
129
178
  }
130
- if (!values.includes('mask')) {
131
- const typeMap = FIT.types[type];
132
- const mapped = typeMap[String(data)];
179
+ if (!metadata.hasMask) {
180
+ const mapped = metadata.typeMap[String(data)];
133
181
  return mapped === undefined ? data : mapped;
134
182
  }
135
183
  const dataItem = {};
136
- for (const key in FIT.types[type]) {
137
- if (key in FIT.types[type]) {
138
- if (FIT.types[type][key] === 'mask') {
139
- dataItem.value = data & Number(key);
140
- }
141
- else {
142
- dataItem[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
143
- }
184
+ for (const [key, value] of metadata.entries) {
185
+ if (value === 'mask') {
186
+ dataItem.value = data & Number(key);
187
+ }
188
+ else {
189
+ 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
144
190
  }
145
191
  }
146
192
  return dataItem;
@@ -254,6 +300,7 @@ function applyOptions(data, field, options, fields) {
254
300
  case 'gps_accuracy':
255
301
  return convertTo(data, 'lengthUnits', options.lengthUnit);
256
302
  case 'temperature':
303
+ case 'min_temperature':
257
304
  case 'avg_temperature':
258
305
  case 'max_temperature':
259
306
  return convertTo(data, 'temperatureUnits', options.temperatureUnit);
@@ -272,8 +319,8 @@ function applyOptions(data, field, options, fields) {
272
319
  return data;
273
320
  }
274
321
  }
275
- export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime) {
276
- var _a, _b;
322
+ export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
323
+ var _a, _b, _c, _d, _e;
277
324
  const recordHeader = blob[startIndex];
278
325
  let localMessageType = recordHeader & 15;
279
326
  if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
@@ -297,12 +344,13 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
297
344
  ]),
298
345
  numberOfFields: numberOfFields + numberOfDeveloperDataFields,
299
346
  fieldDefs: [],
347
+ rawData: [],
300
348
  };
301
349
  const message = getFitMessage(mTypeDef.globalMessageNumber);
302
350
  for (let i = 0; i < numberOfFields; i++) {
303
351
  const fDefIndex = startIndex + 6 + i * 3;
304
352
  const baseType = blob[fDefIndex + 2];
305
- const { field, type } = message.getAttributes(blob[fDefIndex]);
353
+ const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
306
354
  const fDef = {
307
355
  type,
308
356
  fDefNo: blob[fDefIndex],
@@ -312,6 +360,9 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
312
360
  baseTypeNo: baseType,
313
361
  name: field,
314
362
  dataType: getFitMessageBaseType(baseType & 15),
363
+ scale,
364
+ offset,
365
+ requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
315
366
  };
316
367
  mTypeDef.fieldDefs.push(fDef);
317
368
  }
@@ -336,6 +387,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
336
387
  dataType: getFitMessageBaseType(baseType & 15),
337
388
  scale: devDef.scale || 1,
338
389
  offset: devDef.offset || 0,
390
+ requiresBoundedDataView: requiresBoundedEndianDataView(FIT.types.fit_base_type[baseType], size),
339
391
  developerDataIndex: devDataIndex,
340
392
  isDeveloperField: true,
341
393
  };
@@ -348,6 +400,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
348
400
  throw e;
349
401
  }
350
402
  }
403
+ mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
351
404
  messageTypes[localMessageType] = mTypeDef;
352
405
  const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
353
406
  const nextIndexWithDeveloperData = nextIndex + 1;
@@ -363,40 +416,57 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
363
416
  let readDataFromIndex = startIndex + 1;
364
417
  const fields = {};
365
418
  const message = getFitMessage(messageType.globalMessageNumber);
366
- const rawFields = [];
419
+ const rawData = (_a = messageType.rawData) !== null && _a !== void 0 ? _a : (messageType.rawData = Array.from({ length: messageType.fieldDefs.length }, () => InvalidFieldData));
420
+ let validFieldCount = 0;
367
421
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
368
422
  const fDef = messageType.fieldDefs[i];
369
- const data = readData(blob, fDef, readDataFromIndex, options);
423
+ const data = readData(blob, dataView, fDef, readDataFromIndex, options);
370
424
  if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
371
- rawFields.push({ fDef, data });
425
+ rawData[i] = data;
426
+ validFieldCount++;
427
+ }
428
+ else {
429
+ rawData[i] = InvalidFieldData;
372
430
  }
373
431
  readDataFromIndex += fDef.size;
374
432
  messageSize += fDef.size;
375
433
  }
376
- for (const { fDef, data } of rawFields) {
377
- const { field } = fDef.isDeveloperField ? { field: fDef.name } : message.getAttributes(fDef.fDefNo);
434
+ for (let i = 0; i < messageType.fieldDefs.length; i++) {
435
+ const data = rawData[i];
436
+ if (data === InvalidFieldData) {
437
+ continue;
438
+ }
439
+ const fDef = messageType.fieldDefs[i];
440
+ const field = fDef.name;
378
441
  if (field !== 'unknown' && field !== '' && field !== undefined) {
379
442
  fields[field] = data;
380
443
  }
381
444
  }
382
- for (const { fDef, data } of rawFields) {
445
+ for (let i = 0; i < messageType.fieldDefs.length; i++) {
446
+ const data = rawData[i];
447
+ if (data === InvalidFieldData) {
448
+ continue;
449
+ }
450
+ const fDef = messageType.fieldDefs[i];
383
451
  if (fDef.isDeveloperField) {
384
452
  const field = fDef.name;
385
453
  const { type } = fDef;
386
- const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
387
- const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
454
+ const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
455
+ const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
388
456
  fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
389
457
  }
390
458
  else {
391
- const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
459
+ const { name: field, type } = fDef;
460
+ const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
461
+ const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
392
462
  if (field !== 'unknown' && field !== '' && field !== undefined) {
393
463
  fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
394
464
  }
395
465
  }
396
- if (message.name === 'record' && options.elapsedRecordField) {
397
- fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
398
- fields.timer_time = fields.elapsed_time - pausedTime;
399
- }
466
+ }
467
+ if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
468
+ fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
469
+ fields.timer_time = fields.elapsed_time - pausedTime;
400
470
  }
401
471
  if (message.name === 'field_description') {
402
472
  developerFields[fields.developer_data_index]
@@ -1,8 +1,16 @@
1
1
  import type { FitParserOptions } from './fit-parser.js';
2
+ import type { FieldDefinition } from './fit.js';
2
3
  import type { MesgNum } from './fit_types.js';
3
4
  import { Buffer } from 'buffer';
5
+ export interface MessageTypeDefinition {
6
+ littleEndian: boolean;
7
+ globalMessageNumber: number;
8
+ numberOfFields: number;
9
+ fieldDefs: FieldDefinition[];
10
+ rawData?: any[];
11
+ }
4
12
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
5
- export declare function readRecord(blob: Uint8Array, messageTypes: any[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number): {
13
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView): {
6
14
  messageType: MesgNum | '';
7
15
  nextIndex: number;
8
16
  message?: any;
@@ -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;
@@ -260,6 +306,7 @@ function applyOptions(data, field, options, fields) {
260
306
  case 'gps_accuracy':
261
307
  return convertTo(data, 'lengthUnits', options.lengthUnit);
262
308
  case 'temperature':
309
+ case 'min_temperature':
263
310
  case 'avg_temperature':
264
311
  case 'max_temperature':
265
312
  return convertTo(data, 'temperatureUnits', options.temperatureUnit);
@@ -278,8 +325,8 @@ function applyOptions(data, field, options, fields) {
278
325
  return data;
279
326
  }
280
327
  }
281
- function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime) {
282
- var _a, _b;
328
+ function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
329
+ var _a, _b, _c, _d, _e;
283
330
  const recordHeader = blob[startIndex];
284
331
  let localMessageType = recordHeader & 15;
285
332
  if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
@@ -303,12 +350,13 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
303
350
  ]),
304
351
  numberOfFields: numberOfFields + numberOfDeveloperDataFields,
305
352
  fieldDefs: [],
353
+ rawData: [],
306
354
  };
307
355
  const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
308
356
  for (let i = 0; i < numberOfFields; i++) {
309
357
  const fDefIndex = startIndex + 6 + i * 3;
310
358
  const baseType = blob[fDefIndex + 2];
311
- const { field, type } = message.getAttributes(blob[fDefIndex]);
359
+ const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
312
360
  const fDef = {
313
361
  type,
314
362
  fDefNo: blob[fDefIndex],
@@ -318,6 +366,9 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
318
366
  baseTypeNo: baseType,
319
367
  name: field,
320
368
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
369
+ scale,
370
+ offset,
371
+ requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
321
372
  };
322
373
  mTypeDef.fieldDefs.push(fDef);
323
374
  }
@@ -342,6 +393,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
342
393
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
343
394
  scale: devDef.scale || 1,
344
395
  offset: devDef.offset || 0,
396
+ requiresBoundedDataView: requiresBoundedEndianDataView(fit_js_1.FIT.types.fit_base_type[baseType], size),
345
397
  developerDataIndex: devDataIndex,
346
398
  isDeveloperField: true,
347
399
  };
@@ -354,6 +406,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
354
406
  throw e;
355
407
  }
356
408
  }
409
+ mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
357
410
  messageTypes[localMessageType] = mTypeDef;
358
411
  const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
359
412
  const nextIndexWithDeveloperData = nextIndex + 1;
@@ -369,40 +422,57 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
369
422
  let readDataFromIndex = startIndex + 1;
370
423
  const fields = {};
371
424
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
372
- const rawFields = [];
425
+ const rawData = (_a = messageType.rawData) !== null && _a !== void 0 ? _a : (messageType.rawData = Array.from({ length: messageType.fieldDefs.length }, () => InvalidFieldData));
426
+ let validFieldCount = 0;
373
427
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
374
428
  const fDef = messageType.fieldDefs[i];
375
- const data = readData(blob, fDef, readDataFromIndex, options);
429
+ const data = readData(blob, dataView, fDef, readDataFromIndex, options);
376
430
  if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
377
- rawFields.push({ fDef, data });
431
+ rawData[i] = data;
432
+ validFieldCount++;
433
+ }
434
+ else {
435
+ rawData[i] = InvalidFieldData;
378
436
  }
379
437
  readDataFromIndex += fDef.size;
380
438
  messageSize += fDef.size;
381
439
  }
382
- for (const { fDef, data } of rawFields) {
383
- const { field } = fDef.isDeveloperField ? { field: fDef.name } : message.getAttributes(fDef.fDefNo);
440
+ for (let i = 0; i < messageType.fieldDefs.length; i++) {
441
+ const data = rawData[i];
442
+ if (data === InvalidFieldData) {
443
+ continue;
444
+ }
445
+ const fDef = messageType.fieldDefs[i];
446
+ const field = fDef.name;
384
447
  if (field !== 'unknown' && field !== '' && field !== undefined) {
385
448
  fields[field] = data;
386
449
  }
387
450
  }
388
- for (const { fDef, data } of rawFields) {
451
+ for (let i = 0; i < messageType.fieldDefs.length; i++) {
452
+ const data = rawData[i];
453
+ if (data === InvalidFieldData) {
454
+ continue;
455
+ }
456
+ const fDef = messageType.fieldDefs[i];
389
457
  if (fDef.isDeveloperField) {
390
458
  const field = fDef.name;
391
459
  const { type } = fDef;
392
- const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
393
- const offset = (_b = fDef.offset) !== null && _b !== void 0 ? _b : 0;
460
+ const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
461
+ const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
394
462
  fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
395
463
  }
396
464
  else {
397
- const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
465
+ const { name: field, type } = fDef;
466
+ const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
467
+ const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
398
468
  if (field !== 'unknown' && field !== '' && field !== undefined) {
399
469
  fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
400
470
  }
401
471
  }
402
- if (message.name === 'record' && options.elapsedRecordField) {
403
- fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
404
- fields.timer_time = fields.elapsed_time - pausedTime;
405
- }
472
+ }
473
+ if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
474
+ fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
475
+ fields.timer_time = fields.elapsed_time - pausedTime;
406
476
  }
407
477
  if (message.name === 'field_description') {
408
478
  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
  }
package/dist/cjs/fit.js CHANGED
@@ -2554,6 +2554,13 @@ exports.FIT = {
2554
2554
  },
2555
2555
  20: {
2556
2556
  name: 'record',
2557
+ 90: {
2558
+ field: 'garmin_performance_condition',
2559
+ type: 'sint8',
2560
+ scale: null,
2561
+ offset: 0,
2562
+ units: '',
2563
+ },
2557
2564
  253: {
2558
2565
  field: 'timestamp',
2559
2566
  type: 'date_time',
@@ -642,6 +642,7 @@ export interface ParsedRecord {
642
642
  vertical_ratio?: number;
643
643
  stance_time_balance?: number;
644
644
  step_length?: number;
645
+ garmin_performance_condition?: number;
645
646
  absolute_pressure?: number;
646
647
  depth?: number;
647
648
  next_stop_depth?: number;
@@ -729,6 +730,7 @@ export interface ParsedCourse {
729
730
  sport?: Sport;
730
731
  name?: string;
731
732
  capabilities?: CourseCapabilities;
733
+ sub_sport?: SubSport;
732
734
  }
733
735
  export interface ParsedCoursePoint {
734
736
  timestamp: string;
@@ -1,4 +1,4 @@
1
- import { calculateCRC, getArrayBuffer, readRecord } from './binary.js';
1
+ import { calculateCRC, readRecord } from './binary.js';
2
2
  import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export default class FitParser {
@@ -27,7 +27,10 @@ export default class FitParser {
27
27
  }
28
28
  parse(content, callback) {
29
29
  var _a;
30
- const blob = new Uint8Array(getArrayBuffer(content));
30
+ const blob = content instanceof ArrayBuffer
31
+ ? new Uint8Array(content)
32
+ : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
33
+ const dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
31
34
  if (blob.length < 12) {
32
35
  callback('File to small to be a FIT file', undefined);
33
36
  if (!this.options.force) {
@@ -51,27 +54,25 @@ export default class FitParser {
51
54
  return;
52
55
  }
53
56
  }
54
- if (headerLength === 14) {
57
+ if (headerLength === 14 && !this.options.force) {
55
58
  const crcHeader = blob[12] + (blob[13] << 8);
56
59
  const crcHeaderCalc = calculateCRC(blob, 0, 12);
57
60
  if (crcHeader !== crcHeaderCalc) {
58
61
  // callback('Header CRC mismatch', {});
59
62
  // TODO: fix Header CRC check
60
- if (!this.options.force) {
61
- return;
62
- }
63
+ return;
63
64
  }
64
65
  }
65
66
  const protocolVersion = blob[1];
66
67
  const profileVersion = blob[2] + (blob[3] << 8);
67
68
  const dataLength = blob[4] + (blob[5] << 8) + (blob[6] << 16) + (blob[7] << 24);
68
69
  const crcStart = dataLength + headerLength;
69
- const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
70
- const crcFileCalc = calculateCRC(blob, headerLength === 12 ? 0 : headerLength, crcStart);
71
- if (crcFile !== crcFileCalc) {
72
- // callback('File CRC mismatch', {});
73
- // TODO: fix File CRC check
74
- if (!this.options.force) {
70
+ if (!this.options.force) {
71
+ const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
72
+ const crcFileCalc = calculateCRC(blob, headerLength === 12 ? 0 : headerLength, crcStart);
73
+ if (crcFile !== crcFileCalc) {
74
+ // callback('File CRC mismatch', {});
75
+ // TODO: fix File CRC check
75
76
  return;
76
77
  }
77
78
  }
@@ -115,7 +116,7 @@ export default class FitParser {
115
116
  let lastStopTimestamp;
116
117
  let pausedTime = 0;
117
118
  while (loopIndex < crcStart) {
118
- const { nextIndex, messageType, message } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime);
119
+ const { nextIndex, messageType, message } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView);
119
120
  loopIndex = nextIndex;
120
121
  switch (messageType) {
121
122
  case 'lap':
package/dist/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
  }
package/dist/fit.js CHANGED
@@ -2551,6 +2551,13 @@ export const FIT = {
2551
2551
  },
2552
2552
  20: {
2553
2553
  name: 'record',
2554
+ 90: {
2555
+ field: 'garmin_performance_condition',
2556
+ type: 'sint8',
2557
+ scale: null,
2558
+ offset: 0,
2559
+ units: '',
2560
+ },
2554
2561
  253: {
2555
2562
  field: 'timestamp',
2556
2563
  type: 'date_time',
@@ -642,6 +642,7 @@ export interface ParsedRecord {
642
642
  vertical_ratio?: number;
643
643
  stance_time_balance?: number;
644
644
  step_length?: number;
645
+ garmin_performance_condition?: number;
645
646
  absolute_pressure?: number;
646
647
  depth?: number;
647
648
  next_stop_depth?: number;
@@ -729,6 +730,7 @@ export interface ParsedCourse {
729
730
  sport?: Sport;
730
731
  name?: string;
731
732
  capabilities?: CourseCapabilities;
733
+ sub_sport?: SubSport;
732
734
  }
733
735
  export interface ParsedCoursePoint {
734
736
  timestamp: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fit-file-parser",
3
3
  "type": "module",
4
- "version": "3.1.0",
4
+ "version": "4.0.0",
5
5
  "private": false,
6
6
  "description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
7
7
  "author": {
@@ -32,13 +32,6 @@
32
32
  "garmin",
33
33
  "parse"
34
34
  ],
35
- "files": [
36
- "dist/",
37
- "README.md",
38
- "LICENSE",
39
- "CHANGELOG.md",
40
- "CONTRIBUTORS.md"
41
- ],
42
35
  "exports": {
43
36
  ".": {
44
37
  "types": "./dist/fit-parser.d.ts",
@@ -47,6 +40,13 @@
47
40
  }
48
41
  },
49
42
  "main": "dist/cjs/fit-parser.js",
43
+ "files": [
44
+ "CHANGELOG.md",
45
+ "CONTRIBUTORS.md",
46
+ "LICENSE",
47
+ "README.md",
48
+ "dist/"
49
+ ],
50
50
  "maintainers": [
51
51
  {
52
52
  "email": "jimmykane9@gmail.com",
@@ -66,7 +66,8 @@
66
66
  "lint": "eslint .",
67
67
  "fmt": "eslint --fix .",
68
68
  "type-check": "tsc --noEmit",
69
- "build": "tsc --noCheck && tsc --project tsconfig.cjs.json --noCheck && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json"
69
+ "build": "tsc --noCheck && tsc --project tsconfig.cjs.json --noCheck && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
70
+ "prepack": "npm run build"
70
71
  },
71
72
  "test": "vitest",
72
73
  "dependencies": {