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 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. Empty input, incomplete files (including
81
- missing file CRC bytes), and trailing non-FIT bytes reject the entire call,
82
- even with `force: true`. Records and definitions extending beyond the declared
83
- data section are also rejected. CRC validation follows the `force` option for every
84
- file. Failures reject with an `Error`. The existing `parseAsync()` and callback
85
- APIs continue to parse the first FIT file only.
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
  }
@@ -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
  }
@@ -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 every complete FIT file in an input, preserving file order and
33
- * independent decoder state. Rejects incomplete files and trailing bytes;
34
- * each file uses this parser's options, including its CRC policy.
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[]>;
@@ -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: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
46
- includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
47
- includeUnmappedMessages: (_c = options.includeUnmappedMessages) !== null && _c !== void 0 ? _c : false,
48
- rawMessagesOnly: (_d = options.rawMessagesOnly) !== null && _d !== void 0 ? _d : false,
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 every complete FIT file in an input, preserving file order and
65
- * independent decoder state. Rejects incomplete files and trailing bytes;
66
- * each file uses this parser's options, including its CRC policy.
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
- if (crcStart > blob.length) {
98
- throw new Error('File data exceeds input length');
99
- }
100
- if (crcStart + 2 > blob.length) {
101
- throw new Error('File CRC missing');
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
- }, true);
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, validateRecordBounds = false) {
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
- // Complete-file parsing also validates bounds without requiring raw
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
  }
@@ -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 every complete FIT file in an input, preserving file order and
33
- * independent decoder state. Rejects incomplete files and trailing bytes;
34
- * each file uses this parser's options, including its CRC policy.
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[]>;
@@ -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: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
27
- includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
28
- includeUnmappedMessages: (_c = options.includeUnmappedMessages) !== null && _c !== void 0 ? _c : false,
29
- rawMessagesOnly: (_d = options.rawMessagesOnly) !== null && _d !== void 0 ? _d : false,
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 every complete FIT file in an input, preserving file order and
46
- * independent decoder state. Rejects incomplete files and trailing bytes;
47
- * each file uses this parser's options, including its CRC policy.
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
- if (crcStart > blob.length) {
79
- throw new Error('File data exceeds input length');
80
- }
81
- if (crcStart + 2 > blob.length) {
82
- throw new Error('File CRC missing');
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
- }, true);
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, validateRecordBounds = false) {
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
- // Complete-file parsing also validates bounds without requiring raw
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.0",
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": {