fit-file-parser 6.2.1 → 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 CHANGED
@@ -1,5 +1,24 @@
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
+
3
22
  ## 6.2.1 - 2026-10-10
4
23
 
5
24
  ### Added
package/README.md CHANGED
@@ -150,6 +150,13 @@ malformed records and definitions. `parseAsync()` and callback callers keep
150
150
  their current defaults, recovery, and output. `parseFilesAsync()` uses that
151
151
  same decoder independently for each file.
152
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
+
153
160
  `strict: true` adds definition and record bounds checks so CRC bytes cannot be
154
161
  decoded as measurements. Full raw-message retention (`includeRawMessages`)
155
162
  continues to require complete records as it did before this option was added.
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
- if ((options.strict || retainsRawMessages(options)) && startIndex + 6 > dataEnd) {
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 ((options.strict || retainsRawMessages(options))
435
- && ((recordHeader & 16) !== 0
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 ((options.strict || retainsRawMessages(options)) && definitionEnd > dataEnd) {
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
- if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
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 (options.strict || retainsRawMessages(options)) {
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) {
@@ -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;
@@ -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
- if ((options.strict || retainsRawMessages(options)) && startIndex + 6 > dataEnd) {
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 ((options.strict || retainsRawMessages(options))
441
- && ((recordHeader & 16) !== 0
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 ((options.strict || retainsRawMessages(options)) && definitionEnd > dataEnd) {
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
- if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
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 (options.strict || retainsRawMessages(options)) {
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) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fit-file-parser",
3
3
  "type": "module",
4
- "version": "6.2.1",
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": {