fit-file-parser 3.1.3 → 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;
@@ -273,8 +319,8 @@ function applyOptions(data, field, options, fields) {
273
319
  return data;
274
320
  }
275
321
  }
276
- export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime) {
277
- 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;
278
324
  const recordHeader = blob[startIndex];
279
325
  let localMessageType = recordHeader & 15;
280
326
  if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
@@ -298,12 +344,13 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
298
344
  ]),
299
345
  numberOfFields: numberOfFields + numberOfDeveloperDataFields,
300
346
  fieldDefs: [],
347
+ rawData: [],
301
348
  };
302
349
  const message = getFitMessage(mTypeDef.globalMessageNumber);
303
350
  for (let i = 0; i < numberOfFields; i++) {
304
351
  const fDefIndex = startIndex + 6 + i * 3;
305
352
  const baseType = blob[fDefIndex + 2];
306
- const { field, type } = message.getAttributes(blob[fDefIndex]);
353
+ const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
307
354
  const fDef = {
308
355
  type,
309
356
  fDefNo: blob[fDefIndex],
@@ -313,6 +360,9 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
313
360
  baseTypeNo: baseType,
314
361
  name: field,
315
362
  dataType: getFitMessageBaseType(baseType & 15),
363
+ scale,
364
+ offset,
365
+ requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
316
366
  };
317
367
  mTypeDef.fieldDefs.push(fDef);
318
368
  }
@@ -337,6 +387,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
337
387
  dataType: getFitMessageBaseType(baseType & 15),
338
388
  scale: devDef.scale || 1,
339
389
  offset: devDef.offset || 0,
390
+ requiresBoundedDataView: requiresBoundedEndianDataView(FIT.types.fit_base_type[baseType], size),
340
391
  developerDataIndex: devDataIndex,
341
392
  isDeveloperField: true,
342
393
  };
@@ -349,6 +400,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
349
400
  throw e;
350
401
  }
351
402
  }
403
+ mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
352
404
  messageTypes[localMessageType] = mTypeDef;
353
405
  const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
354
406
  const nextIndexWithDeveloperData = nextIndex + 1;
@@ -364,40 +416,57 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
364
416
  let readDataFromIndex = startIndex + 1;
365
417
  const fields = {};
366
418
  const message = getFitMessage(messageType.globalMessageNumber);
367
- 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;
368
421
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
369
422
  const fDef = messageType.fieldDefs[i];
370
- const data = readData(blob, fDef, readDataFromIndex, options);
423
+ const data = readData(blob, dataView, fDef, readDataFromIndex, options);
371
424
  if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
372
- rawFields.push({ fDef, data });
425
+ rawData[i] = data;
426
+ validFieldCount++;
427
+ }
428
+ else {
429
+ rawData[i] = InvalidFieldData;
373
430
  }
374
431
  readDataFromIndex += fDef.size;
375
432
  messageSize += fDef.size;
376
433
  }
377
- for (const { fDef, data } of rawFields) {
378
- 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;
379
441
  if (field !== 'unknown' && field !== '' && field !== undefined) {
380
442
  fields[field] = data;
381
443
  }
382
444
  }
383
- 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];
384
451
  if (fDef.isDeveloperField) {
385
452
  const field = fDef.name;
386
453
  const { type } = fDef;
387
- const scale = (_a = fDef.scale) !== null && _a !== void 0 ? _a : null;
388
- 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;
389
456
  fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
390
457
  }
391
458
  else {
392
- 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;
393
462
  if (field !== 'unknown' && field !== '' && field !== undefined) {
394
463
  fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
395
464
  }
396
465
  }
397
- if (message.name === 'record' && options.elapsedRecordField) {
398
- fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
399
- fields.timer_time = fields.elapsed_time - pausedTime;
400
- }
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;
401
470
  }
402
471
  if (message.name === 'field_description') {
403
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;
@@ -279,8 +325,8 @@ 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 readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
329
+ var _a, _b, _c, _d, _e;
284
330
  const recordHeader = blob[startIndex];
285
331
  let localMessageType = recordHeader & 15;
286
332
  if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
@@ -304,12 +350,13 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
304
350
  ]),
305
351
  numberOfFields: numberOfFields + numberOfDeveloperDataFields,
306
352
  fieldDefs: [],
353
+ rawData: [],
307
354
  };
308
355
  const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
309
356
  for (let i = 0; i < numberOfFields; i++) {
310
357
  const fDefIndex = startIndex + 6 + i * 3;
311
358
  const baseType = blob[fDefIndex + 2];
312
- const { field, type } = message.getAttributes(blob[fDefIndex]);
359
+ const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
313
360
  const fDef = {
314
361
  type,
315
362
  fDefNo: blob[fDefIndex],
@@ -319,6 +366,9 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
319
366
  baseTypeNo: baseType,
320
367
  name: field,
321
368
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
369
+ scale,
370
+ offset,
371
+ requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
322
372
  };
323
373
  mTypeDef.fieldDefs.push(fDef);
324
374
  }
@@ -343,6 +393,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
343
393
  dataType: (0, messages_js_1.getFitMessageBaseType)(baseType & 15),
344
394
  scale: devDef.scale || 1,
345
395
  offset: devDef.offset || 0,
396
+ requiresBoundedDataView: requiresBoundedEndianDataView(fit_js_1.FIT.types.fit_base_type[baseType], size),
346
397
  developerDataIndex: devDataIndex,
347
398
  isDeveloperField: true,
348
399
  };
@@ -355,6 +406,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
355
406
  throw e;
356
407
  }
357
408
  }
409
+ mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
358
410
  messageTypes[localMessageType] = mTypeDef;
359
411
  const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
360
412
  const nextIndexWithDeveloperData = nextIndex + 1;
@@ -370,40 +422,57 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
370
422
  let readDataFromIndex = startIndex + 1;
371
423
  const fields = {};
372
424
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
373
- 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;
374
427
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
375
428
  const fDef = messageType.fieldDefs[i];
376
- const data = readData(blob, fDef, readDataFromIndex, options);
429
+ const data = readData(blob, dataView, fDef, readDataFromIndex, options);
377
430
  if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
378
- rawFields.push({ fDef, data });
431
+ rawData[i] = data;
432
+ validFieldCount++;
433
+ }
434
+ else {
435
+ rawData[i] = InvalidFieldData;
379
436
  }
380
437
  readDataFromIndex += fDef.size;
381
438
  messageSize += fDef.size;
382
439
  }
383
- for (const { fDef, data } of rawFields) {
384
- 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;
385
447
  if (field !== 'unknown' && field !== '' && field !== undefined) {
386
448
  fields[field] = data;
387
449
  }
388
450
  }
389
- 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];
390
457
  if (fDef.isDeveloperField) {
391
458
  const field = fDef.name;
392
459
  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;
460
+ const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
461
+ const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
395
462
  fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
396
463
  }
397
464
  else {
398
- 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;
399
468
  if (field !== 'unknown' && field !== '' && field !== undefined) {
400
469
  fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
401
470
  }
402
471
  }
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
- }
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;
407
476
  }
408
477
  if (message.name === 'field_description') {
409
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
  }
@@ -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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fit-file-parser",
3
3
  "type": "module",
4
- "version": "3.1.3",
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": {