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.
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,22 @@ 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
+ function applyGarminProductName(fields) {
323
+ if (fields.product_name !== undefined || fields.manufacturer !== 'garmin') {
324
+ return;
325
+ }
326
+ const product = fields.product;
327
+ if (typeof product !== 'number') {
328
+ return;
329
+ }
330
+ const productName = FIT.types.garmin_product[product];
331
+ if (typeof productName === 'string') {
332
+ // Keep the raw protocol ID and expose the SDK product name separately.
333
+ fields.product_name = productName;
334
+ }
335
+ }
336
+ export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)) {
337
+ var _a, _b, _c, _d, _e;
278
338
  const recordHeader = blob[startIndex];
279
339
  let localMessageType = recordHeader & 15;
280
340
  if ((recordHeader & CompressedHeaderMask) === CompressedHeaderMask) {
@@ -298,12 +358,13 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
298
358
  ]),
299
359
  numberOfFields: numberOfFields + numberOfDeveloperDataFields,
300
360
  fieldDefs: [],
361
+ rawData: [],
301
362
  };
302
363
  const message = getFitMessage(mTypeDef.globalMessageNumber);
303
364
  for (let i = 0; i < numberOfFields; i++) {
304
365
  const fDefIndex = startIndex + 6 + i * 3;
305
366
  const baseType = blob[fDefIndex + 2];
306
- const { field, type } = message.getAttributes(blob[fDefIndex]);
367
+ const { field, type, scale, offset } = message.getAttributes(blob[fDefIndex]);
307
368
  const fDef = {
308
369
  type,
309
370
  fDefNo: blob[fDefIndex],
@@ -313,6 +374,9 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
313
374
  baseTypeNo: baseType,
314
375
  name: field,
315
376
  dataType: getFitMessageBaseType(baseType & 15),
377
+ scale,
378
+ offset,
379
+ requiresBoundedDataView: requiresBoundedEndianDataView(type, blob[fDefIndex + 1]),
316
380
  };
317
381
  mTypeDef.fieldDefs.push(fDef);
318
382
  }
@@ -337,6 +401,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
337
401
  dataType: getFitMessageBaseType(baseType & 15),
338
402
  scale: devDef.scale || 1,
339
403
  offset: devDef.offset || 0,
404
+ requiresBoundedDataView: requiresBoundedEndianDataView(FIT.types.fit_base_type[baseType], size),
340
405
  developerDataIndex: devDataIndex,
341
406
  isDeveloperField: true,
342
407
  };
@@ -349,6 +414,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
349
414
  throw e;
350
415
  }
351
416
  }
417
+ mTypeDef.rawData = Array.from({ length: mTypeDef.fieldDefs.length }, () => InvalidFieldData);
352
418
  messageTypes[localMessageType] = mTypeDef;
353
419
  const nextIndex = startIndex + 6 + mTypeDef.numberOfFields * 3;
354
420
  const nextIndexWithDeveloperData = nextIndex + 1;
@@ -364,40 +430,58 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
364
430
  let readDataFromIndex = startIndex + 1;
365
431
  const fields = {};
366
432
  const message = getFitMessage(messageType.globalMessageNumber);
367
- const rawFields = [];
433
+ const rawData = (_a = messageType.rawData) !== null && _a !== void 0 ? _a : (messageType.rawData = Array.from({ length: messageType.fieldDefs.length }, () => InvalidFieldData));
434
+ let validFieldCount = 0;
368
435
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
369
436
  const fDef = messageType.fieldDefs[i];
370
- const data = readData(blob, fDef, readDataFromIndex, options);
437
+ const data = readData(blob, dataView, fDef, readDataFromIndex, options);
371
438
  if (!isInvalidValue(data, fDef.type) && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
372
- rawFields.push({ fDef, data });
439
+ rawData[i] = data;
440
+ validFieldCount++;
441
+ }
442
+ else {
443
+ rawData[i] = InvalidFieldData;
373
444
  }
374
445
  readDataFromIndex += fDef.size;
375
446
  messageSize += fDef.size;
376
447
  }
377
- for (const { fDef, data } of rawFields) {
378
- const { field } = fDef.isDeveloperField ? { field: fDef.name } : message.getAttributes(fDef.fDefNo);
448
+ for (let i = 0; i < messageType.fieldDefs.length; i++) {
449
+ const data = rawData[i];
450
+ if (data === InvalidFieldData) {
451
+ continue;
452
+ }
453
+ const fDef = messageType.fieldDefs[i];
454
+ const field = fDef.name;
379
455
  if (field !== 'unknown' && field !== '' && field !== undefined) {
380
456
  fields[field] = data;
381
457
  }
382
458
  }
383
- for (const { fDef, data } of rawFields) {
459
+ for (let i = 0; i < messageType.fieldDefs.length; i++) {
460
+ const data = rawData[i];
461
+ if (data === InvalidFieldData) {
462
+ continue;
463
+ }
464
+ const fDef = messageType.fieldDefs[i];
384
465
  if (fDef.isDeveloperField) {
385
466
  const field = fDef.name;
386
467
  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;
468
+ const scale = (_b = fDef.scale) !== null && _b !== void 0 ? _b : null;
469
+ const offset = (_c = fDef.offset) !== null && _c !== void 0 ? _c : 0;
389
470
  fields[fDef.name] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
390
471
  }
391
472
  else {
392
- const { field, type, scale, offset } = message.getAttributes(fDef.fDefNo);
473
+ const { name: field, type } = fDef;
474
+ const scale = (_d = fDef.scale) !== null && _d !== void 0 ? _d : null;
475
+ const offset = (_e = fDef.offset) !== null && _e !== void 0 ? _e : 0;
393
476
  if (field !== 'unknown' && field !== '' && field !== undefined) {
394
477
  fields[field] = applyOptions(formatByType(data, type, scale, offset), field, options, fields);
395
478
  }
396
479
  }
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
- }
480
+ }
481
+ applyGarminProductName(fields);
482
+ if (validFieldCount > 0 && message.name === 'record' && options.elapsedRecordField) {
483
+ fields.elapsed_time = (fields.timestamp - (startDate || 0)) / 1000;
484
+ fields.timer_time = fields.elapsed_time - pausedTime;
401
485
  }
402
486
  if (message.name === 'field_description') {
403
487
  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;