fit-file-parser 6.2.0 → 6.2.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 +24 -0
- package/README.md +36 -6
- package/dist/binary.js +5 -5
- package/dist/cjs/binary.js +5 -5
- 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,29 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 6.2.1 - 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add the opt-in `strict` parser option to require complete files and records,
|
|
8
|
+
valid definitions, and no trailing bytes in every parsing API. Checksum
|
|
9
|
+
validation remains controlled by `force`.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Apply the same recovery policy to `parseFilesAsync()`, `parseAsync()`, and
|
|
14
|
+
callback parsing. The all-file API now accepts missing CRC bytes with
|
|
15
|
+
`force: true` when `strict` is disabled, and ignores a non-FIT suffix.
|
|
16
|
+
- Keep existing decoder behavior, recovery, and output when `strict` is not
|
|
17
|
+
requested, including handling of malformed records and definitions.
|
|
18
|
+
- Apply additional definition and record bounds checks only in strict mode.
|
|
19
|
+
|
|
20
|
+
### Compatibility
|
|
21
|
+
|
|
22
|
+
- `force` still defaults to `true`; `strict` defaults to `false`.
|
|
23
|
+
- `strict: true` requires exactly one file for `parseAsync()` and callback
|
|
24
|
+
parsing, or complete concatenated files for `parseFilesAsync()`.
|
|
25
|
+
- Temperature mappings and unit conversions are unchanged.
|
|
26
|
+
|
|
3
27
|
## 6.2.0 - 2026-10-10
|
|
4
28
|
|
|
5
29
|
### 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,24 @@ 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
|
+
`strict: true` adds definition and record bounds checks so CRC bytes cannot be
|
|
154
|
+
decoded as measurements. Full raw-message retention (`includeRawMessages`)
|
|
155
|
+
continues to require complete records as it did before this option was added.
|
|
156
|
+
|
|
157
|
+
Choose options according to the input you accept:
|
|
158
|
+
|
|
159
|
+
| Options | Policy |
|
|
160
|
+
| -------------------------------- | --------------------------------------------------------- |
|
|
161
|
+
| Default | Best effort, including missing or invalid CRCs. |
|
|
162
|
+
| `{ force: false }` | Validate CRCs while keeping existing field recovery. |
|
|
163
|
+
| `{ force: true, strict: true }` | Require complete structure while tolerating invalid CRCs. |
|
|
164
|
+
| `{ force: false, strict: true }` | Require complete structure and valid CRCs. |
|
|
165
|
+
|
|
136
166
|
### Lossless developer fields
|
|
137
167
|
|
|
138
168
|
By default, developer fields retain the existing decoded, name-keyed output.
|
package/dist/binary.js
CHANGED
|
@@ -424,14 +424,14 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
424
424
|
else if ((recordHeader & 64) === 64) {
|
|
425
425
|
// is definition message
|
|
426
426
|
// startIndex + 1 is reserved
|
|
427
|
-
if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
|
|
427
|
+
if ((options.strict || retainsRawMessages(options)) && startIndex + 6 > dataEnd) {
|
|
428
428
|
throw new Error('Invalid FIT definition bounds');
|
|
429
429
|
}
|
|
430
430
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
431
431
|
const lEnd = blob[startIndex + 2] === 0;
|
|
432
432
|
const numberOfFields = blob[startIndex + 5];
|
|
433
433
|
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
434
|
-
if (retainsRawMessages(options)
|
|
434
|
+
if ((options.strict || retainsRawMessages(options))
|
|
435
435
|
&& ((recordHeader & 16) !== 0
|
|
436
436
|
|| blob[startIndex + 1] !== 0
|
|
437
437
|
|| blob[startIndex + 2] > 1
|
|
@@ -444,7 +444,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
444
444
|
: 0;
|
|
445
445
|
const definitionEnd = nativeDefinitionsEnd
|
|
446
446
|
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
447
|
-
if (retainsRawMessages(options) && definitionEnd > dataEnd) {
|
|
447
|
+
if ((options.strict || retainsRawMessages(options)) && definitionEnd > dataEnd) {
|
|
448
448
|
throw new Error('Invalid FIT developer definition bounds');
|
|
449
449
|
}
|
|
450
450
|
const mTypeDef = {
|
|
@@ -530,7 +530,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
530
530
|
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
531
531
|
const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
|
|
532
532
|
const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
|
|
533
|
-
if (retainsRawMessages(options)) {
|
|
533
|
+
if (options.strict || retainsRawMessages(options)) {
|
|
534
534
|
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
535
535
|
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
536
536
|
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
@@ -665,7 +665,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
665
665
|
if (isCompressedTimestamp) {
|
|
666
666
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
667
667
|
if (previousTimestamp === undefined) {
|
|
668
|
-
if (!options.force) {
|
|
668
|
+
if (!options.force || options.strict) {
|
|
669
669
|
throw new Error('Compressed timestamp requires a previous timestamp');
|
|
670
670
|
}
|
|
671
671
|
}
|
package/dist/cjs/binary.js
CHANGED
|
@@ -430,14 +430,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
430
430
|
else if ((recordHeader & 64) === 64) {
|
|
431
431
|
// is definition message
|
|
432
432
|
// startIndex + 1 is reserved
|
|
433
|
-
if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
|
|
433
|
+
if ((options.strict || retainsRawMessages(options)) && startIndex + 6 > dataEnd) {
|
|
434
434
|
throw new Error('Invalid FIT definition bounds');
|
|
435
435
|
}
|
|
436
436
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
437
437
|
const lEnd = blob[startIndex + 2] === 0;
|
|
438
438
|
const numberOfFields = blob[startIndex + 5];
|
|
439
439
|
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
440
|
-
if (retainsRawMessages(options)
|
|
440
|
+
if ((options.strict || retainsRawMessages(options))
|
|
441
441
|
&& ((recordHeader & 16) !== 0
|
|
442
442
|
|| blob[startIndex + 1] !== 0
|
|
443
443
|
|| blob[startIndex + 2] > 1
|
|
@@ -450,7 +450,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
450
450
|
: 0;
|
|
451
451
|
const definitionEnd = nativeDefinitionsEnd
|
|
452
452
|
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
453
|
-
if (retainsRawMessages(options) && definitionEnd > dataEnd) {
|
|
453
|
+
if ((options.strict || retainsRawMessages(options)) && definitionEnd > dataEnd) {
|
|
454
454
|
throw new Error('Invalid FIT developer definition bounds');
|
|
455
455
|
}
|
|
456
456
|
const mTypeDef = {
|
|
@@ -536,7 +536,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
536
536
|
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
537
537
|
const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
|
|
538
538
|
const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
|
|
539
|
-
if (retainsRawMessages(options)) {
|
|
539
|
+
if (options.strict || retainsRawMessages(options)) {
|
|
540
540
|
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
541
541
|
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
542
542
|
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
@@ -671,7 +671,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
671
671
|
if (isCompressedTimestamp) {
|
|
672
672
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
673
673
|
if (previousTimestamp === undefined) {
|
|
674
|
-
if (!options.force) {
|
|
674
|
+
if (!options.force || options.strict) {
|
|
675
675
|
throw new Error('Compressed timestamp requires a previous timestamp');
|
|
676
676
|
}
|
|
677
677
|
}
|
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.1",
|
|
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": {
|