fit-file-parser 6.2.0 → 6.2.2
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 +43 -0
- package/README.md +43 -6
- package/dist/binary.d.ts +2 -0
- package/dist/binary.js +24 -7
- package/dist/cjs/binary.d.ts +2 -0
- package/dist/cjs/binary.js +24 -7
- package/dist/cjs/fit-parser.d.ts +6 -3
- package/dist/cjs/fit-parser.js +43 -21
- package/dist/fit-parser.d.ts +6 -3
- package/dist/fit-parser.js +43 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 6.2.2 - 2026-10-11
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Restore ordinary decoding of existing Bryton and Magene FIT files whose
|
|
8
|
+
normal record headers set reserved bit 4. Their activity data was readable
|
|
9
|
+
before 5.2.0 and is recovered without modifying the original bytes.
|
|
10
|
+
- Require complete definitions and native/developer payloads for messages
|
|
11
|
+
using reserved-header compatibility, including ordinary data records that
|
|
12
|
+
reference a definition with the reserved bit set.
|
|
13
|
+
|
|
14
|
+
### Compatibility
|
|
15
|
+
|
|
16
|
+
- Strict parsing, full raw-message retention, and the standalone raw reader
|
|
17
|
+
continue rejecting reserved header bits. Bit 5 in data headers remains
|
|
18
|
+
invalid in all modes. Checksum behavior still follows `force`.
|
|
19
|
+
- No new options or parsed output fields. Existing successful imports need no
|
|
20
|
+
migration; retry previously rejected originals with the updated parser.
|
|
21
|
+
|
|
22
|
+
## 6.2.1 - 2026-10-10
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Add the opt-in `strict` parser option to require complete files and records,
|
|
27
|
+
valid definitions, and no trailing bytes in every parsing API. Checksum
|
|
28
|
+
validation remains controlled by `force`.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- Apply the same recovery policy to `parseFilesAsync()`, `parseAsync()`, and
|
|
33
|
+
callback parsing. The all-file API now accepts missing CRC bytes with
|
|
34
|
+
`force: true` when `strict` is disabled, and ignores a non-FIT suffix.
|
|
35
|
+
- Keep existing decoder behavior, recovery, and output when `strict` is not
|
|
36
|
+
requested, including handling of malformed records and definitions.
|
|
37
|
+
- Apply additional definition and record bounds checks only in strict mode.
|
|
38
|
+
|
|
39
|
+
### Compatibility
|
|
40
|
+
|
|
41
|
+
- `force` still defaults to `true`; `strict` defaults to `false`.
|
|
42
|
+
- `strict: true` requires exactly one file for `parseAsync()` and callback
|
|
43
|
+
parsing, or complete concatenated files for `parseFilesAsync()`.
|
|
44
|
+
- Temperature mappings and unit conversions are unchanged.
|
|
45
|
+
|
|
3
46
|
## 6.2.0 - 2026-10-10
|
|
4
47
|
|
|
5
48
|
### Added
|
package/README.md
CHANGED
|
@@ -77,12 +77,23 @@ for (const file of files) {
|
|
|
77
77
|
|
|
78
78
|
Each result has independent message definitions, developer descriptions,
|
|
79
79
|
timestamps, and activity collections. Results are never merged. A single FIT
|
|
80
|
-
file returns a one-element array.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
file returns a one-element array. All parsing APIs share the same `force` and
|
|
81
|
+
`strict` options. By default, complete fields in a partial final record can be
|
|
82
|
+
recovered, and a non-FIT suffix after parsed files is ignored. With `force: true`,
|
|
83
|
+
missing CRC bytes are also accepted, including when the next FIT header starts
|
|
84
|
+
at the declared data boundary or after one CRC byte. The parser never scans
|
|
85
|
+
record payloads for guessed file boundaries.
|
|
86
|
+
|
|
87
|
+
Use `{ force: false, strict: true }` to require complete files and records,
|
|
88
|
+
valid checksums, and no trailing bytes. `{ force: true, strict: true }` enforces
|
|
89
|
+
the same structure while tolerating known checksum failures. Any file that
|
|
90
|
+
cannot be parsed rejects the whole call with an `Error`. Empty input, invalid
|
|
91
|
+
headers, and declared data beyond the available input
|
|
92
|
+
remain errors in every mode.
|
|
93
|
+
|
|
94
|
+
The existing `parseAsync()` and callback APIs return the first FIT file only.
|
|
95
|
+
With `strict: true`, they require exactly one file; use `parseFilesAsync()` for
|
|
96
|
+
strict validation of concatenated input.
|
|
86
97
|
|
|
87
98
|
### Callback API
|
|
88
99
|
|
|
@@ -120,6 +131,7 @@ All options are optional.
|
|
|
120
131
|
| --------------------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
121
132
|
| `mode` | `list`, `cascade`, `both` | `list` | Controls whether primary activity collections are returned as root lists, nested data, or both. |
|
|
122
133
|
| `force` | `true`, `false` | `true` | Skips header and file CRC validation and enables supported best-effort field recovery. Structural header and data bounds are always validated. |
|
|
134
|
+
| `strict` | `true`, `false` | `false` | Requires CRC bytes, complete records, valid definitions, and no trailing bytes. CRC validation remains controlled by `force`. |
|
|
123
135
|
| `speedUnit` | `m/s`, `km/h`, `mph` | `m/s` | Converts speed-related fields. |
|
|
124
136
|
| `lengthUnit` | `m`, `km`, `mi` | `m` | Converts distance, altitude, and other length-related fields. |
|
|
125
137
|
| `temperatureUnit` | `celsius`, `°C`, `kelvin`, `fahrenheit` | `celsius` | Converts temperature fields. `°C` remains available as a legacy alias. |
|
|
@@ -133,6 +145,31 @@ All options are optional.
|
|
|
133
145
|
too short, have an invalid header size or signature, or declare data beyond the
|
|
134
146
|
available bytes are rejected in both modes.
|
|
135
147
|
|
|
148
|
+
`strict: false` keeps the existing decoder behavior, including its handling of
|
|
149
|
+
malformed records and definitions. `parseAsync()` and callback callers keep
|
|
150
|
+
their current defaults, recovery, and output. `parseFilesAsync()` uses that
|
|
151
|
+
same decoder independently for each file.
|
|
152
|
+
|
|
153
|
+
Ordinary decoding tolerates the reserved bit 4 in normal record headers used
|
|
154
|
+
by some existing device files. Messages using this compatibility behavior must
|
|
155
|
+
have complete definitions and payloads inside the declared data section. Bit 5
|
|
156
|
+
in data headers remains invalid. Strict mode, full raw-message retention, and
|
|
157
|
+
the standalone raw reader continue rejecting reserved header bits. Original
|
|
158
|
+
bytes and the caller's checksum policy are unchanged.
|
|
159
|
+
|
|
160
|
+
`strict: true` adds definition and record bounds checks so CRC bytes cannot be
|
|
161
|
+
decoded as measurements. Full raw-message retention (`includeRawMessages`)
|
|
162
|
+
continues to require complete records as it did before this option was added.
|
|
163
|
+
|
|
164
|
+
Choose options according to the input you accept:
|
|
165
|
+
|
|
166
|
+
| Options | Policy |
|
|
167
|
+
| -------------------------------- | --------------------------------------------------------- |
|
|
168
|
+
| Default | Best effort, including missing or invalid CRCs. |
|
|
169
|
+
| `{ force: false }` | Validate CRCs while keeping existing field recovery. |
|
|
170
|
+
| `{ force: true, strict: true }` | Require complete structure while tolerating invalid CRCs. |
|
|
171
|
+
| `{ force: false, strict: true }` | Require complete structure and valid CRCs. |
|
|
172
|
+
|
|
136
173
|
### Lossless developer fields
|
|
137
174
|
|
|
138
175
|
By default, developer fields retain the existing decoded, name-keyed output.
|
package/dist/binary.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface MessageTypeDefinition {
|
|
|
8
8
|
fieldDefs: FieldDefinition[];
|
|
9
9
|
developerFieldDefs?: DeveloperFieldDefinition[];
|
|
10
10
|
rawData?: any[];
|
|
11
|
+
completeDefinition?: boolean;
|
|
12
|
+
reservedHeaderBit?: boolean;
|
|
11
13
|
}
|
|
12
14
|
export interface DeveloperFieldDefinition {
|
|
13
15
|
developerDataIndex: number;
|
package/dist/binary.js
CHANGED
|
@@ -417,6 +417,8 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
417
417
|
const recordHeader = blob[startIndex];
|
|
418
418
|
let localMessageType = recordHeader & 15;
|
|
419
419
|
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
420
|
+
const reservedHeaderBit = !isCompressedTimestamp && (recordHeader & 0x10) !== 0;
|
|
421
|
+
const strictHeaders = options.strict || retainsRawMessages(options);
|
|
420
422
|
if (isCompressedTimestamp) {
|
|
421
423
|
// compressed timestamp
|
|
422
424
|
localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
|
|
@@ -424,15 +426,16 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
424
426
|
else if ((recordHeader & 64) === 64) {
|
|
425
427
|
// is definition message
|
|
426
428
|
// startIndex + 1 is reserved
|
|
427
|
-
|
|
429
|
+
const requireCompleteDefinition = strictHeaders || reservedHeaderBit;
|
|
430
|
+
if (requireCompleteDefinition && startIndex + 6 > dataEnd) {
|
|
428
431
|
throw new Error('Invalid FIT definition bounds');
|
|
429
432
|
}
|
|
430
433
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
431
434
|
const lEnd = blob[startIndex + 2] === 0;
|
|
432
435
|
const numberOfFields = blob[startIndex + 5];
|
|
433
436
|
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
434
|
-
if (
|
|
435
|
-
&& ((
|
|
437
|
+
if (requireCompleteDefinition
|
|
438
|
+
&& ((strictHeaders && reservedHeaderBit)
|
|
436
439
|
|| blob[startIndex + 1] !== 0
|
|
437
440
|
|| blob[startIndex + 2] > 1
|
|
438
441
|
|| nativeDefinitionsEnd > dataEnd
|
|
@@ -444,7 +447,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
444
447
|
: 0;
|
|
445
448
|
const definitionEnd = nativeDefinitionsEnd
|
|
446
449
|
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
447
|
-
if (
|
|
450
|
+
if (requireCompleteDefinition && definitionEnd > dataEnd) {
|
|
448
451
|
throw new Error('Invalid FIT developer definition bounds');
|
|
449
452
|
}
|
|
450
453
|
const mTypeDef = {
|
|
@@ -457,6 +460,13 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
457
460
|
fieldDefs: [],
|
|
458
461
|
developerFieldDefs: [],
|
|
459
462
|
rawData: [],
|
|
463
|
+
// A later data header may use the reserved bit with an ordinary
|
|
464
|
+
// definition. Do not extend legacy partial-record recovery to that case.
|
|
465
|
+
completeDefinition: startIndex + 6 <= dataEnd
|
|
466
|
+
&& blob[startIndex + 1] === 0
|
|
467
|
+
&& blob[startIndex + 2] <= 1
|
|
468
|
+
&& definitionEnd <= dataEnd,
|
|
469
|
+
reservedHeaderBit,
|
|
460
470
|
};
|
|
461
471
|
const message = getFitMessage(mTypeDef.globalMessageNumber);
|
|
462
472
|
for (let i = 0; i < numberOfFields; i++) {
|
|
@@ -509,13 +519,20 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
509
519
|
nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
|
|
510
520
|
};
|
|
511
521
|
}
|
|
512
|
-
|
|
522
|
+
// Some existing producer files set bit 4 without changing the record layout.
|
|
523
|
+
// Recover only that bit in ordinary decoding; bit 5 and strict/raw parsing
|
|
524
|
+
// retain their previous rejection policy. Never rewrite the source bytes.
|
|
525
|
+
if (!isCompressedTimestamp && ((recordHeader & 0x20) !== 0 || (strictHeaders && reservedHeaderBit))) {
|
|
513
526
|
throw new Error('Invalid FIT data record header');
|
|
514
527
|
}
|
|
515
528
|
const messageType = messageTypes[localMessageType];
|
|
516
529
|
if (!messageType) {
|
|
517
530
|
throw new Error('FIT data record has no local definition');
|
|
518
531
|
}
|
|
532
|
+
const reservedHeaderCompatibility = reservedHeaderBit || messageType.reservedHeaderBit;
|
|
533
|
+
if (reservedHeaderCompatibility && !messageType.completeDefinition) {
|
|
534
|
+
throw new Error('Invalid FIT definition for reserved header compatibility');
|
|
535
|
+
}
|
|
519
536
|
let messageSize = 0;
|
|
520
537
|
let readDataFromIndex = startIndex + 1;
|
|
521
538
|
const fields = {};
|
|
@@ -530,7 +547,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
530
547
|
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
531
548
|
const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
|
|
532
549
|
const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
|
|
533
|
-
if (
|
|
550
|
+
if (strictHeaders || reservedHeaderCompatibility) {
|
|
534
551
|
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
535
552
|
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
536
553
|
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
@@ -665,7 +682,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
665
682
|
if (isCompressedTimestamp) {
|
|
666
683
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
667
684
|
if (previousTimestamp === undefined) {
|
|
668
|
-
if (!options.force) {
|
|
685
|
+
if (!options.force || options.strict) {
|
|
669
686
|
throw new Error('Compressed timestamp requires a previous timestamp');
|
|
670
687
|
}
|
|
671
688
|
}
|
package/dist/cjs/binary.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface MessageTypeDefinition {
|
|
|
8
8
|
fieldDefs: FieldDefinition[];
|
|
9
9
|
developerFieldDefs?: DeveloperFieldDefinition[];
|
|
10
10
|
rawData?: any[];
|
|
11
|
+
completeDefinition?: boolean;
|
|
12
|
+
reservedHeaderBit?: boolean;
|
|
11
13
|
}
|
|
12
14
|
export interface DeveloperFieldDefinition {
|
|
13
15
|
developerDataIndex: number;
|
package/dist/cjs/binary.js
CHANGED
|
@@ -423,6 +423,8 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
423
423
|
const recordHeader = blob[startIndex];
|
|
424
424
|
let localMessageType = recordHeader & 15;
|
|
425
425
|
const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
|
|
426
|
+
const reservedHeaderBit = !isCompressedTimestamp && (recordHeader & 0x10) !== 0;
|
|
427
|
+
const strictHeaders = options.strict || retainsRawMessages(options);
|
|
426
428
|
if (isCompressedTimestamp) {
|
|
427
429
|
// compressed timestamp
|
|
428
430
|
localMessageType = (recordHeader & CompressedLocalMsgNumMask) >> 5;
|
|
@@ -430,15 +432,16 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
430
432
|
else if ((recordHeader & 64) === 64) {
|
|
431
433
|
// is definition message
|
|
432
434
|
// startIndex + 1 is reserved
|
|
433
|
-
|
|
435
|
+
const requireCompleteDefinition = strictHeaders || reservedHeaderBit;
|
|
436
|
+
if (requireCompleteDefinition && startIndex + 6 > dataEnd) {
|
|
434
437
|
throw new Error('Invalid FIT definition bounds');
|
|
435
438
|
}
|
|
436
439
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
437
440
|
const lEnd = blob[startIndex + 2] === 0;
|
|
438
441
|
const numberOfFields = blob[startIndex + 5];
|
|
439
442
|
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
440
|
-
if (
|
|
441
|
-
&& ((
|
|
443
|
+
if (requireCompleteDefinition
|
|
444
|
+
&& ((strictHeaders && reservedHeaderBit)
|
|
442
445
|
|| blob[startIndex + 1] !== 0
|
|
443
446
|
|| blob[startIndex + 2] > 1
|
|
444
447
|
|| nativeDefinitionsEnd > dataEnd
|
|
@@ -450,7 +453,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
450
453
|
: 0;
|
|
451
454
|
const definitionEnd = nativeDefinitionsEnd
|
|
452
455
|
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
453
|
-
if (
|
|
456
|
+
if (requireCompleteDefinition && definitionEnd > dataEnd) {
|
|
454
457
|
throw new Error('Invalid FIT developer definition bounds');
|
|
455
458
|
}
|
|
456
459
|
const mTypeDef = {
|
|
@@ -463,6 +466,13 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
463
466
|
fieldDefs: [],
|
|
464
467
|
developerFieldDefs: [],
|
|
465
468
|
rawData: [],
|
|
469
|
+
// A later data header may use the reserved bit with an ordinary
|
|
470
|
+
// definition. Do not extend legacy partial-record recovery to that case.
|
|
471
|
+
completeDefinition: startIndex + 6 <= dataEnd
|
|
472
|
+
&& blob[startIndex + 1] === 0
|
|
473
|
+
&& blob[startIndex + 2] <= 1
|
|
474
|
+
&& definitionEnd <= dataEnd,
|
|
475
|
+
reservedHeaderBit,
|
|
466
476
|
};
|
|
467
477
|
const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
|
|
468
478
|
for (let i = 0; i < numberOfFields; i++) {
|
|
@@ -515,13 +525,20 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
515
525
|
nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
|
|
516
526
|
};
|
|
517
527
|
}
|
|
518
|
-
|
|
528
|
+
// Some existing producer files set bit 4 without changing the record layout.
|
|
529
|
+
// Recover only that bit in ordinary decoding; bit 5 and strict/raw parsing
|
|
530
|
+
// retain their previous rejection policy. Never rewrite the source bytes.
|
|
531
|
+
if (!isCompressedTimestamp && ((recordHeader & 0x20) !== 0 || (strictHeaders && reservedHeaderBit))) {
|
|
519
532
|
throw new Error('Invalid FIT data record header');
|
|
520
533
|
}
|
|
521
534
|
const messageType = messageTypes[localMessageType];
|
|
522
535
|
if (!messageType) {
|
|
523
536
|
throw new Error('FIT data record has no local definition');
|
|
524
537
|
}
|
|
538
|
+
const reservedHeaderCompatibility = reservedHeaderBit || messageType.reservedHeaderBit;
|
|
539
|
+
if (reservedHeaderCompatibility && !messageType.completeDefinition) {
|
|
540
|
+
throw new Error('Invalid FIT definition for reserved header compatibility');
|
|
541
|
+
}
|
|
525
542
|
let messageSize = 0;
|
|
526
543
|
let readDataFromIndex = startIndex + 1;
|
|
527
544
|
const fields = {};
|
|
@@ -536,7 +553,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
536
553
|
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
537
554
|
const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
|
|
538
555
|
const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
|
|
539
|
-
if (
|
|
556
|
+
if (strictHeaders || reservedHeaderCompatibility) {
|
|
540
557
|
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
541
558
|
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
542
559
|
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
@@ -671,7 +688,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
671
688
|
if (isCompressedTimestamp) {
|
|
672
689
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
673
690
|
if (previousTimestamp === undefined) {
|
|
674
|
-
if (!options.force) {
|
|
691
|
+
if (!options.force || options.strict) {
|
|
675
692
|
throw new Error('Compressed timestamp requires a previous timestamp');
|
|
676
693
|
}
|
|
677
694
|
}
|
package/dist/cjs/fit-parser.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProduc
|
|
|
7
7
|
export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
|
|
8
8
|
export type { FitMessageReaderErrorCode, FitMessageReaderIssue, FitMessageReaderIssueCode, FitMessageReaderOptions, FitMessageReaderResult, FitRawDeveloperField, FitRawField, FitRawMessage, } from './raw-message-reader.js';
|
|
9
9
|
export interface FitParserOptions {
|
|
10
|
+
/** Skips CRC validation and enables supported best-effort recovery. Defaults to true. */
|
|
10
11
|
force?: boolean;
|
|
12
|
+
/** Requires complete files and records, and rejects trailing bytes. Defaults to false. */
|
|
13
|
+
strict?: boolean;
|
|
11
14
|
speedUnit?: string;
|
|
12
15
|
lengthUnit?: string;
|
|
13
16
|
temperatureUnit?: string;
|
|
@@ -29,9 +32,9 @@ export default class FitParser {
|
|
|
29
32
|
constructor(options?: FitParserOptions);
|
|
30
33
|
parseAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit>;
|
|
31
34
|
/**
|
|
32
|
-
* Parses
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* Parses FIT files in input order with independent decoder state. Uses the
|
|
36
|
+
* same force and strict policies as parseAsync; strict mode rejects trailing
|
|
37
|
+
* bytes, while the default ignores a non-FIT suffix after parsed files.
|
|
35
38
|
* Rejects with an Error when any file cannot be parsed.
|
|
36
39
|
*/
|
|
37
40
|
parseFilesAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit[]>;
|
package/dist/cjs/fit-parser.js
CHANGED
|
@@ -31,21 +31,31 @@ Object.defineProperty(exports, "getFitBaseTypeId", { enumerable: true, get: func
|
|
|
31
31
|
Object.defineProperty(exports, "readFitMessages", { enumerable: true, get: function () { return raw_message_reader_js_1.readFitMessages; } });
|
|
32
32
|
Object.defineProperty(exports, "readFitStringField", { enumerable: true, get: function () { return raw_message_reader_js_1.readFitStringField; } });
|
|
33
33
|
Object.defineProperty(exports, "readFitUnsignedField", { enumerable: true, get: function () { return raw_message_reader_js_1.readFitUnsignedField; } });
|
|
34
|
+
function hasFitHeader(blob, start) {
|
|
35
|
+
const headerLength = blob[start];
|
|
36
|
+
return blob.length - start >= headerLength
|
|
37
|
+
&& (headerLength === 12 || headerLength === 14)
|
|
38
|
+
&& blob[start + 8] === 0x2E
|
|
39
|
+
&& blob[start + 9] === 0x46
|
|
40
|
+
&& blob[start + 10] === 0x49
|
|
41
|
+
&& blob[start + 11] === 0x54;
|
|
42
|
+
}
|
|
34
43
|
class FitParser {
|
|
35
44
|
constructor(options = {}) {
|
|
36
|
-
var _a, _b, _c, _d;
|
|
45
|
+
var _a, _b, _c, _d, _e;
|
|
37
46
|
this.options = {
|
|
38
47
|
force: options.force != null ? options.force : true,
|
|
48
|
+
strict: (_a = options.strict) !== null && _a !== void 0 ? _a : false,
|
|
39
49
|
speedUnit: options.speedUnit || 'm/s',
|
|
40
50
|
lengthUnit: options.lengthUnit || 'm',
|
|
41
51
|
temperatureUnit: options.temperatureUnit || 'celsius',
|
|
42
52
|
elapsedRecordField: options.elapsedRecordField || false,
|
|
43
53
|
pressureUnit: options.pressureUnit || 'bar',
|
|
44
54
|
mode: options.mode || 'list',
|
|
45
|
-
includeRawDeveloperFields: (
|
|
46
|
-
includeRawMessages: (
|
|
47
|
-
includeUnmappedMessages: (
|
|
48
|
-
rawMessagesOnly: (
|
|
55
|
+
includeRawDeveloperFields: (_b = options.includeRawDeveloperFields) !== null && _b !== void 0 ? _b : false,
|
|
56
|
+
includeRawMessages: (_c = options.includeRawMessages) !== null && _c !== void 0 ? _c : false,
|
|
57
|
+
includeUnmappedMessages: (_d = options.includeUnmappedMessages) !== null && _d !== void 0 ? _d : false,
|
|
58
|
+
rawMessagesOnly: (_e = options.rawMessagesOnly) !== null && _e !== void 0 ? _e : false,
|
|
49
59
|
};
|
|
50
60
|
}
|
|
51
61
|
parseAsync(content) {
|
|
@@ -61,9 +71,9 @@ class FitParser {
|
|
|
61
71
|
});
|
|
62
72
|
}
|
|
63
73
|
/**
|
|
64
|
-
* Parses
|
|
65
|
-
*
|
|
66
|
-
*
|
|
74
|
+
* Parses FIT files in input order with independent decoder state. Uses the
|
|
75
|
+
* same force and strict policies as parseAsync; strict mode rejects trailing
|
|
76
|
+
* bytes, while the default ignores a non-FIT suffix after parsed files.
|
|
67
77
|
* Rejects with an Error when any file cannot be parsed.
|
|
68
78
|
*/
|
|
69
79
|
parseFilesAsync(content) {
|
|
@@ -75,10 +85,13 @@ class FitParser {
|
|
|
75
85
|
let start = 0;
|
|
76
86
|
do {
|
|
77
87
|
const remaining = blob.length - start;
|
|
88
|
+
const headerLength = blob[start];
|
|
89
|
+
if (results.length > 0 && !this.options.strict && !hasFitHeader(blob, start)) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
78
92
|
if (remaining < 12) {
|
|
79
93
|
throw new Error('File to small to be a FIT file');
|
|
80
94
|
}
|
|
81
|
-
const headerLength = blob[start];
|
|
82
95
|
if (headerLength !== 12 && headerLength !== 14) {
|
|
83
96
|
throw new Error('Incorrect header size');
|
|
84
97
|
}
|
|
@@ -94,13 +107,20 @@ class FitParser {
|
|
|
94
107
|
const dataLength = new DataView(blob.buffer, blob.byteOffset + start, remaining)
|
|
95
108
|
.getUint32(4, true);
|
|
96
109
|
const crcStart = start + headerLength + dataLength;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
// File completeness and CRC checks belong to the shared parser below,
|
|
111
|
+
// preserving the same validation policy and error order in every API.
|
|
112
|
+
let end = Math.min(crcStart + 2, blob.length);
|
|
113
|
+
// Missing CRC bytes can also precede the next file. Check only the
|
|
114
|
+
// declared boundary and its one-byte CRC variant; never scan activity
|
|
115
|
+
// payloads for a guessed file boundary.
|
|
116
|
+
if (this.options.force && !this.options.strict) {
|
|
117
|
+
if (hasFitHeader(blob, crcStart)) {
|
|
118
|
+
end = crcStart;
|
|
119
|
+
}
|
|
120
|
+
else if (hasFitHeader(blob, crcStart + 1)) {
|
|
121
|
+
end = crcStart + 1;
|
|
122
|
+
}
|
|
102
123
|
}
|
|
103
|
-
const end = crcStart + 2;
|
|
104
124
|
try {
|
|
105
125
|
results.push(yield new Promise((resolve, reject) => {
|
|
106
126
|
this.parseFile(blob.slice(start, end).buffer, (error, data) => {
|
|
@@ -110,7 +130,7 @@ class FitParser {
|
|
|
110
130
|
else if (data) {
|
|
111
131
|
resolve(data);
|
|
112
132
|
}
|
|
113
|
-
}
|
|
133
|
+
});
|
|
114
134
|
}));
|
|
115
135
|
}
|
|
116
136
|
catch (error) {
|
|
@@ -124,7 +144,7 @@ class FitParser {
|
|
|
124
144
|
parse(content, callback) {
|
|
125
145
|
this.parseFile(content, callback);
|
|
126
146
|
}
|
|
127
|
-
parseFile(content, callback
|
|
147
|
+
parseFile(content, callback) {
|
|
128
148
|
var _a, _b, _c;
|
|
129
149
|
const blob = content instanceof ArrayBuffer
|
|
130
150
|
? new Uint8Array(content)
|
|
@@ -167,10 +187,14 @@ class FitParser {
|
|
|
167
187
|
callback('File data exceeds input length', undefined);
|
|
168
188
|
return;
|
|
169
189
|
}
|
|
170
|
-
if (!this.options.force && crcStart + 2 > blob.length) {
|
|
190
|
+
if ((!this.options.force || this.options.strict) && crcStart + 2 > blob.length) {
|
|
171
191
|
callback('File CRC missing', undefined);
|
|
172
192
|
return;
|
|
173
193
|
}
|
|
194
|
+
if (this.options.strict && crcStart + 2 < blob.length) {
|
|
195
|
+
callback('Unexpected bytes after FIT file', undefined);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
174
198
|
if (!this.options.force) {
|
|
175
199
|
const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
|
|
176
200
|
const crcFileCalc = (0, binary_js_1.calculateCRC)(blob, 0, crcStart);
|
|
@@ -234,9 +258,7 @@ class FitParser {
|
|
|
234
258
|
let pausedTime = 0;
|
|
235
259
|
while (loopIndex < crcStart) {
|
|
236
260
|
const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, unmappedFields: recordUnmappedFields, unmappedDeveloperFields: recordUnmappedDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
|
|
237
|
-
|
|
238
|
-
// retention. The single-file API keeps its existing recovery behavior.
|
|
239
|
-
if (validateRecordBounds
|
|
261
|
+
if (this.options.strict
|
|
240
262
|
&& (!Number.isInteger(nextIndex) || nextIndex <= loopIndex || nextIndex > crcStart)) {
|
|
241
263
|
throw new Error('Invalid FIT record bounds');
|
|
242
264
|
}
|
package/dist/fit-parser.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProduc
|
|
|
7
7
|
export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
|
|
8
8
|
export type { FitMessageReaderErrorCode, FitMessageReaderIssue, FitMessageReaderIssueCode, FitMessageReaderOptions, FitMessageReaderResult, FitRawDeveloperField, FitRawField, FitRawMessage, } from './raw-message-reader.js';
|
|
9
9
|
export interface FitParserOptions {
|
|
10
|
+
/** Skips CRC validation and enables supported best-effort recovery. Defaults to true. */
|
|
10
11
|
force?: boolean;
|
|
12
|
+
/** Requires complete files and records, and rejects trailing bytes. Defaults to false. */
|
|
13
|
+
strict?: boolean;
|
|
11
14
|
speedUnit?: string;
|
|
12
15
|
lengthUnit?: string;
|
|
13
16
|
temperatureUnit?: string;
|
|
@@ -29,9 +32,9 @@ export default class FitParser {
|
|
|
29
32
|
constructor(options?: FitParserOptions);
|
|
30
33
|
parseAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit>;
|
|
31
34
|
/**
|
|
32
|
-
* Parses
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* Parses FIT files in input order with independent decoder state. Uses the
|
|
36
|
+
* same force and strict policies as parseAsync; strict mode rejects trailing
|
|
37
|
+
* bytes, while the default ignores a non-FIT suffix after parsed files.
|
|
35
38
|
* Rejects with an Error when any file cannot be parsed.
|
|
36
39
|
*/
|
|
37
40
|
parseFilesAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit[]>;
|
package/dist/fit-parser.js
CHANGED
|
@@ -12,21 +12,31 @@ import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
|
|
|
12
12
|
export { FitBaseType, FitEncoder } from './fit-encoder.js';
|
|
13
13
|
export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProductName, getFitManufacturerName, getFitSportId, getFitSportName, getFitSubSportId, getFitSubSportName, } from './profile-lookup.js';
|
|
14
14
|
export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
|
|
15
|
+
function hasFitHeader(blob, start) {
|
|
16
|
+
const headerLength = blob[start];
|
|
17
|
+
return blob.length - start >= headerLength
|
|
18
|
+
&& (headerLength === 12 || headerLength === 14)
|
|
19
|
+
&& blob[start + 8] === 0x2E
|
|
20
|
+
&& blob[start + 9] === 0x46
|
|
21
|
+
&& blob[start + 10] === 0x49
|
|
22
|
+
&& blob[start + 11] === 0x54;
|
|
23
|
+
}
|
|
15
24
|
export default class FitParser {
|
|
16
25
|
constructor(options = {}) {
|
|
17
|
-
var _a, _b, _c, _d;
|
|
26
|
+
var _a, _b, _c, _d, _e;
|
|
18
27
|
this.options = {
|
|
19
28
|
force: options.force != null ? options.force : true,
|
|
29
|
+
strict: (_a = options.strict) !== null && _a !== void 0 ? _a : false,
|
|
20
30
|
speedUnit: options.speedUnit || 'm/s',
|
|
21
31
|
lengthUnit: options.lengthUnit || 'm',
|
|
22
32
|
temperatureUnit: options.temperatureUnit || 'celsius',
|
|
23
33
|
elapsedRecordField: options.elapsedRecordField || false,
|
|
24
34
|
pressureUnit: options.pressureUnit || 'bar',
|
|
25
35
|
mode: options.mode || 'list',
|
|
26
|
-
includeRawDeveloperFields: (
|
|
27
|
-
includeRawMessages: (
|
|
28
|
-
includeUnmappedMessages: (
|
|
29
|
-
rawMessagesOnly: (
|
|
36
|
+
includeRawDeveloperFields: (_b = options.includeRawDeveloperFields) !== null && _b !== void 0 ? _b : false,
|
|
37
|
+
includeRawMessages: (_c = options.includeRawMessages) !== null && _c !== void 0 ? _c : false,
|
|
38
|
+
includeUnmappedMessages: (_d = options.includeUnmappedMessages) !== null && _d !== void 0 ? _d : false,
|
|
39
|
+
rawMessagesOnly: (_e = options.rawMessagesOnly) !== null && _e !== void 0 ? _e : false,
|
|
30
40
|
};
|
|
31
41
|
}
|
|
32
42
|
parseAsync(content) {
|
|
@@ -42,9 +52,9 @@ export default class FitParser {
|
|
|
42
52
|
});
|
|
43
53
|
}
|
|
44
54
|
/**
|
|
45
|
-
* Parses
|
|
46
|
-
*
|
|
47
|
-
*
|
|
55
|
+
* Parses FIT files in input order with independent decoder state. Uses the
|
|
56
|
+
* same force and strict policies as parseAsync; strict mode rejects trailing
|
|
57
|
+
* bytes, while the default ignores a non-FIT suffix after parsed files.
|
|
48
58
|
* Rejects with an Error when any file cannot be parsed.
|
|
49
59
|
*/
|
|
50
60
|
parseFilesAsync(content) {
|
|
@@ -56,10 +66,13 @@ export default class FitParser {
|
|
|
56
66
|
let start = 0;
|
|
57
67
|
do {
|
|
58
68
|
const remaining = blob.length - start;
|
|
69
|
+
const headerLength = blob[start];
|
|
70
|
+
if (results.length > 0 && !this.options.strict && !hasFitHeader(blob, start)) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
59
73
|
if (remaining < 12) {
|
|
60
74
|
throw new Error('File to small to be a FIT file');
|
|
61
75
|
}
|
|
62
|
-
const headerLength = blob[start];
|
|
63
76
|
if (headerLength !== 12 && headerLength !== 14) {
|
|
64
77
|
throw new Error('Incorrect header size');
|
|
65
78
|
}
|
|
@@ -75,13 +88,20 @@ export default class FitParser {
|
|
|
75
88
|
const dataLength = new DataView(blob.buffer, blob.byteOffset + start, remaining)
|
|
76
89
|
.getUint32(4, true);
|
|
77
90
|
const crcStart = start + headerLength + dataLength;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
91
|
+
// File completeness and CRC checks belong to the shared parser below,
|
|
92
|
+
// preserving the same validation policy and error order in every API.
|
|
93
|
+
let end = Math.min(crcStart + 2, blob.length);
|
|
94
|
+
// Missing CRC bytes can also precede the next file. Check only the
|
|
95
|
+
// declared boundary and its one-byte CRC variant; never scan activity
|
|
96
|
+
// payloads for a guessed file boundary.
|
|
97
|
+
if (this.options.force && !this.options.strict) {
|
|
98
|
+
if (hasFitHeader(blob, crcStart)) {
|
|
99
|
+
end = crcStart;
|
|
100
|
+
}
|
|
101
|
+
else if (hasFitHeader(blob, crcStart + 1)) {
|
|
102
|
+
end = crcStart + 1;
|
|
103
|
+
}
|
|
83
104
|
}
|
|
84
|
-
const end = crcStart + 2;
|
|
85
105
|
try {
|
|
86
106
|
results.push(yield new Promise((resolve, reject) => {
|
|
87
107
|
this.parseFile(blob.slice(start, end).buffer, (error, data) => {
|
|
@@ -91,7 +111,7 @@ export default class FitParser {
|
|
|
91
111
|
else if (data) {
|
|
92
112
|
resolve(data);
|
|
93
113
|
}
|
|
94
|
-
}
|
|
114
|
+
});
|
|
95
115
|
}));
|
|
96
116
|
}
|
|
97
117
|
catch (error) {
|
|
@@ -105,7 +125,7 @@ export default class FitParser {
|
|
|
105
125
|
parse(content, callback) {
|
|
106
126
|
this.parseFile(content, callback);
|
|
107
127
|
}
|
|
108
|
-
parseFile(content, callback
|
|
128
|
+
parseFile(content, callback) {
|
|
109
129
|
var _a, _b, _c;
|
|
110
130
|
const blob = content instanceof ArrayBuffer
|
|
111
131
|
? new Uint8Array(content)
|
|
@@ -148,10 +168,14 @@ export default class FitParser {
|
|
|
148
168
|
callback('File data exceeds input length', undefined);
|
|
149
169
|
return;
|
|
150
170
|
}
|
|
151
|
-
if (!this.options.force && crcStart + 2 > blob.length) {
|
|
171
|
+
if ((!this.options.force || this.options.strict) && crcStart + 2 > blob.length) {
|
|
152
172
|
callback('File CRC missing', undefined);
|
|
153
173
|
return;
|
|
154
174
|
}
|
|
175
|
+
if (this.options.strict && crcStart + 2 < blob.length) {
|
|
176
|
+
callback('Unexpected bytes after FIT file', undefined);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
155
179
|
if (!this.options.force) {
|
|
156
180
|
const crcFile = blob[crcStart] + (blob[crcStart + 1] << 8);
|
|
157
181
|
const crcFileCalc = calculateCRC(blob, 0, crcStart);
|
|
@@ -215,9 +239,7 @@ export default class FitParser {
|
|
|
215
239
|
let pausedTime = 0;
|
|
216
240
|
while (loopIndex < crcStart) {
|
|
217
241
|
const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, unmappedFields: recordUnmappedFields, unmappedDeveloperFields: recordUnmappedDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
|
|
218
|
-
|
|
219
|
-
// retention. The single-file API keeps its existing recovery behavior.
|
|
220
|
-
if (validateRecordBounds
|
|
242
|
+
if (this.options.strict
|
|
221
243
|
&& (!Number.isInteger(nextIndex) || nextIndex <= loopIndex || nextIndex > crcStart)) {
|
|
222
244
|
throw new Error('Invalid FIT record bounds');
|
|
223
245
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fit-file-parser",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "6.2.
|
|
4
|
+
"version": "6.2.2",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "JavaScript and TypeScript library for parsing and encoding FIT files, with developer-field support, raw message access, and ESM/CommonJS builds.",
|
|
7
7
|
"author": {
|