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 +22 -0
- package/dist/binary.d.ts +9 -1
- package/dist/binary.js +131 -47
- package/dist/cjs/binary.d.ts +9 -1
- package/dist/cjs/binary.js +131 -47
- package/dist/cjs/fit-parser.js +13 -12
- package/dist/cjs/fit.d.ts +2 -1
- package/dist/cjs/fit.js +420 -100
- package/dist/cjs/fit_types.d.ts +1 -1
- package/dist/fit-parser.js +14 -13
- package/dist/fit.d.ts +2 -1
- package/dist/fit.js +420 -100
- package/dist/fit_types.d.ts +1 -1
- package/package.json +4 -1
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:
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
70
|
+
return fieldDataView.getInt16(fieldStartIndex, fDef.littleEndian);
|
|
36
71
|
case 'uint16':
|
|
37
72
|
case 'uint16z':
|
|
38
|
-
return
|
|
73
|
+
return fieldDataView.getUint16(fieldStartIndex, fDef.littleEndian);
|
|
39
74
|
case 'sint32':
|
|
40
|
-
return
|
|
75
|
+
return fieldDataView.getInt32(fieldStartIndex, fDef.littleEndian);
|
|
41
76
|
case 'uint32':
|
|
42
77
|
case 'uint32z':
|
|
43
|
-
return
|
|
78
|
+
return fieldDataView.getUint32(fieldStartIndex, fDef.littleEndian);
|
|
44
79
|
case 'float32':
|
|
45
|
-
return
|
|
80
|
+
return fieldDataView.getFloat32(fieldStartIndex, fDef.littleEndian);
|
|
46
81
|
case 'float64':
|
|
47
|
-
return
|
|
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(
|
|
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(
|
|
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
|
-
|
|
159
|
+
const typeMap = FIT.types[type];
|
|
160
|
+
if (!typeMap) {
|
|
121
161
|
return data;
|
|
122
162
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 (!
|
|
131
|
-
const
|
|
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
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
378
|
-
const
|
|
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 (
|
|
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 = (
|
|
388
|
-
const offset = (
|
|
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
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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]
|
package/dist/cjs/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:
|
|
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;
|