fit-file-parser 5.0.2 → 5.2.0

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
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### Added
6
+
7
+ - Add opt-in, message-filterable `raw_messages` output with exact native and
8
+ developer-field bytes, wire base types, endianness, message occurrence, and
9
+ reconstructed compressed timestamps. Existing decoded output remains
10
+ unchanged when the option is disabled.
11
+ - Add `rawMessagesOnly` for bounded metadata consumers that do not need decoded
12
+ activity collections alongside retained messages.
13
+
14
+ ### Fixed
15
+
16
+ - Decode compressed timestamp records without consuming bytes for the omitted
17
+ timestamp field.
18
+
19
+ ## 5.1.0
20
+
21
+ ### Added
22
+
23
+ - Add opt-in, message-filterable `raw_developer_fields` output with exact
24
+ developer-field bytes, global-message occurrence, developer-data index, and
25
+ field-definition number. Existing decoded output remains unchanged when the
26
+ option is disabled.
27
+
5
28
  ## 5.0.2
6
29
 
7
30
  ### Fixed
package/README.md CHANGED
@@ -135,20 +135,98 @@ callback API receives as its first argument.
135
135
 
136
136
  All options are optional.
137
137
 
138
- | Option | Values | Default | Behavior |
139
- | -------------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
140
- | `mode` | `list`, `cascade`, `both` | `list` | Controls whether primary activity collections are returned as root lists, nested data, or both. |
141
- | `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. |
142
- | `speedUnit` | `m/s`, `km/h`, `mph` | `m/s` | Converts speed-related fields. |
143
- | `lengthUnit` | `m`, `km`, `mi` | `m` | Converts distance, altitude, and other length-related fields. |
144
- | `temperatureUnit` | `celsius`, `°C`, `kelvin`, `fahrenheit` | `celsius` | Converts temperature fields. `°C` remains available as a legacy alias. |
145
- | `pressureUnit` | `bar`, `cbar`, `psi` | `bar` | Converts pressure and tank-pressure fields. |
146
- | `elapsedRecordField` | `true`, `false` | `false` | Adds `elapsed_time` and `timer_time`, in seconds, to records. |
138
+ | Option | Values | Default | Behavior |
139
+ | --------------------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
140
+ | `mode` | `list`, `cascade`, `both` | `list` | Controls whether primary activity collections are returned as root lists, nested data, or both. |
141
+ | `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. |
142
+ | `speedUnit` | `m/s`, `km/h`, `mph` | `m/s` | Converts speed-related fields. |
143
+ | `lengthUnit` | `m`, `km`, `mi` | `m` | Converts distance, altitude, and other length-related fields. |
144
+ | `temperatureUnit` | `celsius`, `°C`, `kelvin`, `fahrenheit` | `celsius` | Converts temperature fields. `°C` remains available as a legacy alias. |
145
+ | `pressureUnit` | `bar`, `cbar`, `psi` | `bar` | Converts pressure and tank-pressure fields. |
146
+ | `elapsedRecordField` | `true`, `false` | `false` | Adds `elapsed_time` and `timer_time`, in seconds, to records. |
147
+ | `includeRawDeveloperFields` | `true`, `false`, message-number array | `false` | Adds a lossless, message-associated view of developer-field bytes at `raw_developer_fields`. |
148
+ | `includeRawMessages` | `true`, `false`, message-number array | `false` | Adds selected FIT messages with exact native/developer bytes and wire metadata at `raw_messages`. |
149
+ | `rawMessagesOnly` | `true`, `false` | `false` | Omits decoded activity collections when a consumer needs only the opt-in raw-message representation. |
147
150
 
148
151
  `force: true` does not make arbitrary bytes a valid FIT file. Inputs that are
149
152
  too short, have an invalid header size or signature, or declare data beyond the
150
153
  available bytes are rejected in both modes.
151
154
 
155
+ ### Lossless developer fields
156
+
157
+ By default, developer fields retain the existing decoded, name-keyed output.
158
+ Set `includeRawDeveloperFields: true` when a consumer also needs exact field
159
+ bytes or must distinguish same-named fields from different developer-data
160
+ indexes:
161
+
162
+ ```javascript
163
+ const data = await new FitParser({
164
+ force: false,
165
+ includeRawDeveloperFields: [18],
166
+ }).parseAsync(content)
167
+
168
+ for (const field of data.raw_developer_fields ?? []) {
169
+ console.log({
170
+ globalMessageNumber: field.global_message_number,
171
+ messageIndex: field.message_index,
172
+ developerDataIndex: field.developer_data_index,
173
+ fieldDefinitionNumber: field.field_definition_number,
174
+ rawBytes: field.raw_value,
175
+ })
176
+ }
177
+ ```
178
+
179
+ `message_index` is the zero-based occurrence of that global message number in
180
+ file order. `raw_value` is a defensive plain-number copy of the exact FIT field
181
+ bytes, including interior NUL bytes and invalid sentinels. Join
182
+ `developer_data_index` and `field_definition_number` to the existing
183
+ `developer_data_ids` and `field_descriptions` collections to interpret an
184
+ application and field. The parser deliberately does not infer application- or
185
+ provider-specific semantics. Pass `true` to retain developer fields from every
186
+ global message, or an array such as `[18]` to bound collection to specific FIT
187
+ message numbers.
188
+
189
+ ### Lossless selected messages
190
+
191
+ Set `includeRawMessages` when a consumer needs native FIT numeric codes rather
192
+ than the parser's intentionally formatted enum names, or needs native and
193
+ developer fields grouped exactly by message occurrence:
194
+
195
+ ```javascript
196
+ const data = await new FitParser({
197
+ force: false,
198
+ includeRawMessages: [18, 26, 72, 206, 207],
199
+ rawMessagesOnly: true,
200
+ }).parseAsync(content)
201
+
202
+ for (const message of data.raw_messages ?? []) {
203
+ console.log({
204
+ globalMessageNumber: message.global_message_number,
205
+ messageIndex: message.message_index,
206
+ littleEndian: message.little_endian,
207
+ fields: message.fields,
208
+ developerFields: message.developer_fields,
209
+ })
210
+ }
211
+ ```
212
+
213
+ Each native field includes its field-definition number, wire base-type byte,
214
+ and a defensive plain-number copy of its exact bytes. Developer fields retain
215
+ their developer-data index, field-definition number, and exact bytes. Empty
216
+ selected messages are retained with empty field arrays. For a compressed
217
+ timestamp record, the timestamp is not present on the wire and therefore is
218
+ not invented in `fields`; its reconstructed native FIT timestamp is exposed as
219
+ `compressed_timestamp` instead.
220
+
221
+ The parser does not apply profile enum formatting, scaling, invalid-sentinel
222
+ filtering, or provider semantics to this opt-in representation. Its normal
223
+ decoded output remains unchanged. Pass `true` to retain every message or a
224
+ global-message-number array to keep memory use bounded. `raw_messages` and
225
+ `raw_developer_fields` are independent opt-ins; enabling `raw_messages` does
226
+ not add the flattened `raw_developer_fields` property. Set `rawMessagesOnly`
227
+ when only this representation is needed; the parser still validates and walks
228
+ the complete FIT data section but does not retain decoded activity collections.
229
+
152
230
  ## Output modes
153
231
 
154
232
  The mode controls where sessions, laps, records, and related activity
package/dist/binary.d.ts CHANGED
@@ -20,11 +20,26 @@ export interface DecoderState {
20
20
  lastTimestamp?: number;
21
21
  monitoringTimestamp?: number;
22
22
  }
23
+ export interface RawDeveloperFieldValue {
24
+ developerDataIndex: number;
25
+ fieldDefinitionNumber: number;
26
+ rawValue: number[];
27
+ }
28
+ export interface RawFieldValue {
29
+ fieldDefinitionNumber: number;
30
+ baseType: number;
31
+ rawValue: number[];
32
+ }
23
33
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
24
- export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
34
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState, dataEnd?: number): {
25
35
  messageType: MessageName | 'definition' | '';
26
36
  nextIndex: number;
27
37
  message?: any;
38
+ globalMessageNumber?: number;
39
+ littleEndian?: boolean;
40
+ compressedTimestamp?: number;
41
+ rawFields?: RawFieldValue[];
42
+ rawDeveloperFields?: RawDeveloperFieldValue[];
28
43
  };
29
44
  export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
30
45
  export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;
package/dist/binary.js CHANGED
@@ -8,6 +8,37 @@ const GarminTimeOffset = 631065600000;
8
8
  const InvalidFieldData = Symbol('invalid FIT field data');
9
9
  const formatTypeMetadata = new Map();
10
10
  const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
11
+ const fitBaseTypeWidths = new Map([
12
+ [0, 1],
13
+ [1, 1],
14
+ [2, 1],
15
+ [3, 2],
16
+ [4, 2],
17
+ [5, 4],
18
+ [6, 4],
19
+ [7, 1],
20
+ [8, 4],
21
+ [9, 8],
22
+ [10, 1],
23
+ [11, 2],
24
+ [12, 4],
25
+ [13, 1],
26
+ [14, 8],
27
+ [15, 8],
28
+ [16, 8],
29
+ ]);
30
+ function retainsRawMessages(options) {
31
+ return options.includeRawMessages === true
32
+ || Array.isArray(options.includeRawMessages);
33
+ }
34
+ function isValidRawFieldDefinition(size, baseType) {
35
+ if (size <= 0 || (baseType & 0x60) !== 0) {
36
+ return false;
37
+ }
38
+ const typeId = baseType & 0x1F;
39
+ const width = fitBaseTypeWidths.get(typeId);
40
+ return width !== undefined && (typeId === 7 || size % width === 0);
41
+ }
11
42
  function baseTypeSize(type) {
12
43
  switch (type) {
13
44
  case 'enum':
@@ -368,7 +399,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
368
399
  if (!Number.isInteger(baseType) || type === undefined) {
369
400
  developerFieldDef.resolvedFieldDef = undefined;
370
401
  developerFieldDef.resolvedFrom = undefined;
371
- if (options.force) {
402
+ if (options.force || retainsRawMessages(options)) {
372
403
  return undefined;
373
404
  }
374
405
  throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
@@ -399,8 +430,11 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
399
430
  developerFieldDef.resolvedFrom = description;
400
431
  return resolvedFieldDef;
401
432
  }
402
- export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
433
+ export function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
403
434
  var _a, _b, _c;
435
+ if (startIndex < 0 || startIndex >= dataEnd) {
436
+ throw new Error('Invalid FIT record bounds');
437
+ }
404
438
  const recordHeader = blob[startIndex];
405
439
  let localMessageType = recordHeader & 15;
406
440
  const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
@@ -411,12 +445,29 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
411
445
  else if ((recordHeader & 64) === 64) {
412
446
  // is definition message
413
447
  // startIndex + 1 is reserved
448
+ if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
449
+ throw new Error('Invalid FIT definition bounds');
450
+ }
414
451
  const hasDeveloperData = (recordHeader & 32) === 32;
415
452
  const lEnd = blob[startIndex + 2] === 0;
416
453
  const numberOfFields = blob[startIndex + 5];
454
+ const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
455
+ if (retainsRawMessages(options)
456
+ && ((recordHeader & 16) !== 0
457
+ || blob[startIndex + 1] !== 0
458
+ || blob[startIndex + 2] > 1
459
+ || nativeDefinitionsEnd > dataEnd
460
+ || (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
461
+ throw new Error('Invalid FIT definition');
462
+ }
417
463
  const numberOfDeveloperDataFields = hasDeveloperData
418
464
  ? blob[startIndex + 5 + numberOfFields * 3 + 1]
419
465
  : 0;
466
+ const definitionEnd = nativeDefinitionsEnd
467
+ + (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
468
+ if (retainsRawMessages(options) && definitionEnd > dataEnd) {
469
+ throw new Error('Invalid FIT developer definition bounds');
470
+ }
420
471
  const mTypeDef = {
421
472
  littleEndian: lEnd,
422
473
  globalMessageNumber: addEndian(lEnd, [
@@ -429,17 +480,26 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
429
480
  rawData: [],
430
481
  };
431
482
  const message = getFitMessage(mTypeDef.globalMessageNumber);
483
+ const nativeFieldNumbers = new Set();
432
484
  for (let i = 0; i < numberOfFields; i++) {
433
485
  const fDefIndex = startIndex + 6 + i * 3;
434
486
  const baseType = blob[fDefIndex + 2];
487
+ const fieldNumber = blob[fDefIndex];
488
+ const fieldSize = blob[fDefIndex + 1];
489
+ if (retainsRawMessages(options)
490
+ && (nativeFieldNumbers.has(fieldNumber)
491
+ || !isValidRawFieldDefinition(fieldSize, baseType))) {
492
+ throw new Error('Invalid FIT native field definition');
493
+ }
494
+ nativeFieldNumbers.add(fieldNumber);
435
495
  const wireType = FIT.types.fit_base_type[baseType];
436
496
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
437
497
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
438
498
  const fDef = {
439
499
  type: profileCompatible ? type : wireType,
440
500
  rawType: wireType,
441
- fDefNo: blob[fDefIndex],
442
- size: blob[fDefIndex + 1],
501
+ fDefNo: fieldNumber,
502
+ size: fieldSize,
443
503
  array: profileCompatible
444
504
  ? array === true || String(type).endsWith('_array')
445
505
  : false,
@@ -451,12 +511,19 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
451
511
  scale: profileCompatible ? scale : null,
452
512
  offset: profileCompatible ? offset : 0,
453
513
  units: profileCompatible ? units : '',
454
- requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
514
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
455
515
  };
456
516
  mTypeDef.fieldDefs.push(fDef);
457
517
  }
518
+ const developerFieldNumbers = new Set();
458
519
  for (let i = 0; i < numberOfDeveloperDataFields; i++) {
459
520
  const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
521
+ const developerFieldKey = `${blob[fDefIndex + 2]}:${blob[fDefIndex]}`;
522
+ if (retainsRawMessages(options)
523
+ && (blob[fDefIndex + 1] === 0 || developerFieldNumbers.has(developerFieldKey))) {
524
+ throw new Error('Invalid FIT developer field definition');
525
+ }
526
+ developerFieldNumbers.add(developerFieldKey);
460
527
  (_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
461
528
  fieldDefinitionNumber: blob[fDefIndex],
462
529
  size: blob[fDefIndex + 1],
@@ -472,29 +539,73 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
472
539
  nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
473
540
  };
474
541
  }
475
- const messageType = messageTypes[localMessageType] || messageTypes[0];
542
+ if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
543
+ throw new Error('Invalid FIT data record header');
544
+ }
545
+ const messageType = messageTypes[localMessageType];
546
+ if (!messageType) {
547
+ throw new Error('FIT data record has no local definition');
548
+ }
549
+ if (isCompressedTimestamp && retainsRawMessages(options)) {
550
+ const timestampField = messageType.fieldDefs[0];
551
+ if (!timestampField
552
+ || timestampField.fDefNo !== 253
553
+ || timestampField.size !== 4
554
+ || (timestampField.baseTypeNo & 0x1F) !== 6) {
555
+ throw new Error('Invalid FIT compressed timestamp definition');
556
+ }
557
+ }
476
558
  let messageSize = 0;
477
559
  let readDataFromIndex = startIndex + 1;
478
560
  const fields = {};
479
561
  const message = getFitMessage(messageType.globalMessageNumber);
480
562
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
481
563
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
564
+ const includeRawMessage = options.includeRawMessages === true
565
+ || (Array.isArray(options.includeRawMessages)
566
+ && options.includeRawMessages.includes(messageType.globalMessageNumber));
567
+ const includeRawDeveloperFields = options.includeRawDeveloperFields === true
568
+ || (Array.isArray(options.includeRawDeveloperFields)
569
+ && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
570
+ const rawFields = includeRawMessage ? [] : undefined;
571
+ const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
572
+ if (retainsRawMessages(options)) {
573
+ const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
574
+ const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
575
+ if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
576
+ throw new Error('Invalid FIT data record bounds');
577
+ }
578
+ }
482
579
  const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
483
580
  let validFieldCount = 0;
484
581
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
485
582
  const fDef = messageType.fieldDefs[i];
583
+ if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
584
+ rawData[i] = InvalidFieldData;
585
+ continue;
586
+ }
587
+ if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
588
+ rawFields.push({
589
+ fieldDefinitionNumber: fDef.fDefNo,
590
+ baseType: fDef.baseTypeNo,
591
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
592
+ });
593
+ }
486
594
  const data = readData(blob, dataView, fDef, readDataFromIndex);
487
595
  if (data !== InvalidFieldData
488
596
  && !isInvalidValue(data, fDef.type)
489
597
  && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
490
598
  rawData[i] = data;
491
599
  validFieldCount++;
492
- if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
493
- decoderState.lastTimestamp = data;
600
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
601
+ decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
494
602
  }
495
603
  }
496
604
  else {
497
605
  rawData[i] = InvalidFieldData;
606
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
607
+ decoderState.lastTimestamp = undefined;
608
+ }
498
609
  }
499
610
  readDataFromIndex += fDef.size;
500
611
  messageSize += fDef.size;
@@ -502,6 +613,14 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
502
613
  for (let i = 0; i < developerFieldDefs.length; i++) {
503
614
  const developerFieldDef = developerFieldDefs[i];
504
615
  const rawDataIndex = messageType.fieldDefs.length + i;
616
+ if (rawDeveloperFields
617
+ && readDataFromIndex + developerFieldDef.size <= dataEnd) {
618
+ rawDeveloperFields.push({
619
+ developerDataIndex: developerFieldDef.developerDataIndex,
620
+ fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
621
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
622
+ });
623
+ }
505
624
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
506
625
  if (fDef) {
507
626
  const data = readData(blob, dataView, fDef, readDataFromIndex);
@@ -563,6 +682,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
563
682
  fields[field] = formatFieldValue(data, fDef, options, fields);
564
683
  }
565
684
  }
685
+ let compressedTimestamp;
566
686
  if (isCompressedTimestamp) {
567
687
  const previousTimestamp = decoderState.lastTimestamp;
568
688
  if (previousTimestamp === undefined) {
@@ -574,8 +694,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
574
694
  const timeOffset = recordHeader & CompressedTimestampMask;
575
695
  const previousOffset = previousTimestamp & CompressedTimestampMask;
576
696
  const rollover = timeOffset < previousOffset ? 0x20 : 0;
577
- const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
697
+ const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
698
+ if (timestamp >= 0xFFFFFFFF) {
699
+ throw new Error('Compressed timestamp exceeds FIT uint32 range');
700
+ }
578
701
  decoderState.lastTimestamp = timestamp;
702
+ compressedTimestamp = timestamp;
579
703
  fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
580
704
  validFieldCount++;
581
705
  }
@@ -607,9 +731,14 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
607
731
  }
608
732
  }
609
733
  return {
734
+ globalMessageNumber: messageType.globalMessageNumber,
735
+ littleEndian: messageType.littleEndian,
736
+ compressedTimestamp,
610
737
  messageType: message.name,
611
738
  nextIndex: startIndex + messageSize + 1,
612
739
  message: fields,
740
+ rawFields,
741
+ rawDeveloperFields,
613
742
  };
614
743
  }
615
744
  export function getArrayBuffer(buffer) {
@@ -20,11 +20,26 @@ export interface DecoderState {
20
20
  lastTimestamp?: number;
21
21
  monitoringTimestamp?: number;
22
22
  }
23
+ export interface RawDeveloperFieldValue {
24
+ developerDataIndex: number;
25
+ fieldDefinitionNumber: number;
26
+ rawValue: number[];
27
+ }
28
+ export interface RawFieldValue {
29
+ fieldDefinitionNumber: number;
30
+ baseType: number;
31
+ rawValue: number[];
32
+ }
23
33
  export declare function addEndian(littleEndian: boolean, bytes: number[]): number;
24
- export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState): {
34
+ export declare function readRecord(blob: Uint8Array, messageTypes: MessageTypeDefinition[], developerFields: any[], startIndex: number, options: FitParserOptions, startDate: number | undefined, pausedTime: number, dataView?: DataView, decoderState?: DecoderState, dataEnd?: number): {
25
35
  messageType: MessageName | 'definition' | '';
26
36
  nextIndex: number;
27
37
  message?: any;
38
+ globalMessageNumber?: number;
39
+ littleEndian?: boolean;
40
+ compressedTimestamp?: number;
41
+ rawFields?: RawFieldValue[];
42
+ rawDeveloperFields?: RawDeveloperFieldValue[];
28
43
  };
29
44
  export declare function getArrayBuffer(buffer: ArrayBuffer | Buffer): ArrayBuffer;
30
45
  export declare function calculateCRC(blob: Uint8Array, start: number, end: number): number;
@@ -14,6 +14,37 @@ const GarminTimeOffset = 631065600000;
14
14
  const InvalidFieldData = Symbol('invalid FIT field data');
15
15
  const formatTypeMetadata = new Map();
16
16
  const uint8CompatibleTypes = new Set(['enum', 'uint8', 'byte']);
17
+ const fitBaseTypeWidths = new Map([
18
+ [0, 1],
19
+ [1, 1],
20
+ [2, 1],
21
+ [3, 2],
22
+ [4, 2],
23
+ [5, 4],
24
+ [6, 4],
25
+ [7, 1],
26
+ [8, 4],
27
+ [9, 8],
28
+ [10, 1],
29
+ [11, 2],
30
+ [12, 4],
31
+ [13, 1],
32
+ [14, 8],
33
+ [15, 8],
34
+ [16, 8],
35
+ ]);
36
+ function retainsRawMessages(options) {
37
+ return options.includeRawMessages === true
38
+ || Array.isArray(options.includeRawMessages);
39
+ }
40
+ function isValidRawFieldDefinition(size, baseType) {
41
+ if (size <= 0 || (baseType & 0x60) !== 0) {
42
+ return false;
43
+ }
44
+ const typeId = baseType & 0x1F;
45
+ const width = fitBaseTypeWidths.get(typeId);
46
+ return width !== undefined && (typeId === 7 || size % width === 0);
47
+ }
17
48
  function baseTypeSize(type) {
18
49
  switch (type) {
19
50
  case 'enum':
@@ -374,7 +405,7 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
374
405
  if (!Number.isInteger(baseType) || type === undefined) {
375
406
  developerFieldDef.resolvedFieldDef = undefined;
376
407
  developerFieldDef.resolvedFrom = undefined;
377
- if (options.force) {
408
+ if (options.force || retainsRawMessages(options)) {
378
409
  return undefined;
379
410
  }
380
411
  throw new Error(`Unsupported base type for developer data index ${developerFieldDef.developerDataIndex}, field ${developerFieldDef.fieldDefinitionNumber}`);
@@ -405,8 +436,11 @@ function resolveDeveloperFieldDefinition(developerFieldDef, littleEndian, develo
405
436
  developerFieldDef.resolvedFrom = description;
406
437
  return resolvedFieldDef;
407
438
  }
408
- function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}) {
439
+ function readRecord(blob, messageTypes, developerFields, startIndex, options, startDate, pausedTime, dataView = new DataView(blob.buffer, blob.byteOffset, blob.byteLength), decoderState = {}, dataEnd = dataView.byteLength) {
409
440
  var _a, _b, _c;
441
+ if (startIndex < 0 || startIndex >= dataEnd) {
442
+ throw new Error('Invalid FIT record bounds');
443
+ }
410
444
  const recordHeader = blob[startIndex];
411
445
  let localMessageType = recordHeader & 15;
412
446
  const isCompressedTimestamp = (recordHeader & CompressedHeaderMask) === CompressedHeaderMask;
@@ -417,12 +451,29 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
417
451
  else if ((recordHeader & 64) === 64) {
418
452
  // is definition message
419
453
  // startIndex + 1 is reserved
454
+ if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
455
+ throw new Error('Invalid FIT definition bounds');
456
+ }
420
457
  const hasDeveloperData = (recordHeader & 32) === 32;
421
458
  const lEnd = blob[startIndex + 2] === 0;
422
459
  const numberOfFields = blob[startIndex + 5];
460
+ const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
461
+ if (retainsRawMessages(options)
462
+ && ((recordHeader & 16) !== 0
463
+ || blob[startIndex + 1] !== 0
464
+ || blob[startIndex + 2] > 1
465
+ || nativeDefinitionsEnd > dataEnd
466
+ || (hasDeveloperData && nativeDefinitionsEnd + 1 > dataEnd))) {
467
+ throw new Error('Invalid FIT definition');
468
+ }
423
469
  const numberOfDeveloperDataFields = hasDeveloperData
424
470
  ? blob[startIndex + 5 + numberOfFields * 3 + 1]
425
471
  : 0;
472
+ const definitionEnd = nativeDefinitionsEnd
473
+ + (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
474
+ if (retainsRawMessages(options) && definitionEnd > dataEnd) {
475
+ throw new Error('Invalid FIT developer definition bounds');
476
+ }
426
477
  const mTypeDef = {
427
478
  littleEndian: lEnd,
428
479
  globalMessageNumber: addEndian(lEnd, [
@@ -435,17 +486,26 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
435
486
  rawData: [],
436
487
  };
437
488
  const message = (0, messages_js_1.getFitMessage)(mTypeDef.globalMessageNumber);
489
+ const nativeFieldNumbers = new Set();
438
490
  for (let i = 0; i < numberOfFields; i++) {
439
491
  const fDefIndex = startIndex + 6 + i * 3;
440
492
  const baseType = blob[fDefIndex + 2];
493
+ const fieldNumber = blob[fDefIndex];
494
+ const fieldSize = blob[fDefIndex + 1];
495
+ if (retainsRawMessages(options)
496
+ && (nativeFieldNumbers.has(fieldNumber)
497
+ || !isValidRawFieldDefinition(fieldSize, baseType))) {
498
+ throw new Error('Invalid FIT native field definition');
499
+ }
500
+ nativeFieldNumbers.add(fieldNumber);
441
501
  const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
442
502
  const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
443
503
  const profileCompatible = areProfileBaseTypesCompatible(profileBaseType, wireType);
444
504
  const fDef = {
445
505
  type: profileCompatible ? type : wireType,
446
506
  rawType: wireType,
447
- fDefNo: blob[fDefIndex],
448
- size: blob[fDefIndex + 1],
507
+ fDefNo: fieldNumber,
508
+ size: fieldSize,
449
509
  array: profileCompatible
450
510
  ? array === true || String(type).endsWith('_array')
451
511
  : false,
@@ -457,12 +517,19 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
457
517
  scale: profileCompatible ? scale : null,
458
518
  offset: profileCompatible ? offset : 0,
459
519
  units: profileCompatible ? units : '',
460
- requiresBoundedDataView: requiresBoundedEndianDataView(wireType, blob[fDefIndex + 1]),
520
+ requiresBoundedDataView: requiresBoundedEndianDataView(wireType, fieldSize),
461
521
  };
462
522
  mTypeDef.fieldDefs.push(fDef);
463
523
  }
524
+ const developerFieldNumbers = new Set();
464
525
  for (let i = 0; i < numberOfDeveloperDataFields; i++) {
465
526
  const fDefIndex = startIndex + 6 + numberOfFields * 3 + 1 + i * 3;
527
+ const developerFieldKey = `${blob[fDefIndex + 2]}:${blob[fDefIndex]}`;
528
+ if (retainsRawMessages(options)
529
+ && (blob[fDefIndex + 1] === 0 || developerFieldNumbers.has(developerFieldKey))) {
530
+ throw new Error('Invalid FIT developer field definition');
531
+ }
532
+ developerFieldNumbers.add(developerFieldKey);
466
533
  (_a = mTypeDef.developerFieldDefs) === null || _a === void 0 ? void 0 : _a.push({
467
534
  fieldDefinitionNumber: blob[fDefIndex],
468
535
  size: blob[fDefIndex + 1],
@@ -478,29 +545,73 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
478
545
  nextIndex: hasDeveloperData ? nextIndexWithDeveloperData : nextIndex,
479
546
  };
480
547
  }
481
- const messageType = messageTypes[localMessageType] || messageTypes[0];
548
+ if (!isCompressedTimestamp && (recordHeader & 0x30) !== 0) {
549
+ throw new Error('Invalid FIT data record header');
550
+ }
551
+ const messageType = messageTypes[localMessageType];
552
+ if (!messageType) {
553
+ throw new Error('FIT data record has no local definition');
554
+ }
555
+ if (isCompressedTimestamp && retainsRawMessages(options)) {
556
+ const timestampField = messageType.fieldDefs[0];
557
+ if (!timestampField
558
+ || timestampField.fDefNo !== 253
559
+ || timestampField.size !== 4
560
+ || (timestampField.baseTypeNo & 0x1F) !== 6) {
561
+ throw new Error('Invalid FIT compressed timestamp definition');
562
+ }
563
+ }
482
564
  let messageSize = 0;
483
565
  let readDataFromIndex = startIndex + 1;
484
566
  const fields = {};
485
567
  const message = (0, messages_js_1.getFitMessage)(messageType.globalMessageNumber);
486
568
  const developerFieldDefs = (_b = messageType.developerFieldDefs) !== null && _b !== void 0 ? _b : [];
487
569
  const totalFieldCount = messageType.fieldDefs.length + developerFieldDefs.length;
570
+ const includeRawMessage = options.includeRawMessages === true
571
+ || (Array.isArray(options.includeRawMessages)
572
+ && options.includeRawMessages.includes(messageType.globalMessageNumber));
573
+ const includeRawDeveloperFields = options.includeRawDeveloperFields === true
574
+ || (Array.isArray(options.includeRawDeveloperFields)
575
+ && options.includeRawDeveloperFields.includes(messageType.globalMessageNumber));
576
+ const rawFields = includeRawMessage ? [] : undefined;
577
+ const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
578
+ if (retainsRawMessages(options)) {
579
+ const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
580
+ const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
581
+ if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
582
+ throw new Error('Invalid FIT data record bounds');
583
+ }
584
+ }
488
585
  const rawData = (_c = messageType.rawData) !== null && _c !== void 0 ? _c : (messageType.rawData = Array.from({ length: totalFieldCount }, () => InvalidFieldData));
489
586
  let validFieldCount = 0;
490
587
  for (let i = 0; i < messageType.fieldDefs.length; i++) {
491
588
  const fDef = messageType.fieldDefs[i];
589
+ if (isCompressedTimestamp && i === 0 && fDef.fDefNo === 253) {
590
+ rawData[i] = InvalidFieldData;
591
+ continue;
592
+ }
593
+ if (rawFields && readDataFromIndex + fDef.size <= dataEnd) {
594
+ rawFields.push({
595
+ fieldDefinitionNumber: fDef.fDefNo,
596
+ baseType: fDef.baseTypeNo,
597
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
598
+ });
599
+ }
492
600
  const data = readData(blob, dataView, fDef, readDataFromIndex);
493
601
  if (data !== InvalidFieldData
494
602
  && !isInvalidValue(data, fDef.type)
495
603
  && !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
496
604
  rawData[i] = data;
497
605
  validFieldCount++;
498
- if (!isCompressedTimestamp && fDef.fDefNo === 253 && typeof data === 'number') {
499
- decoderState.lastTimestamp = data;
606
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
607
+ decoderState.lastTimestamp = typeof data === 'number' ? data : undefined;
500
608
  }
501
609
  }
502
610
  else {
503
611
  rawData[i] = InvalidFieldData;
612
+ if (!isCompressedTimestamp && fDef.fDefNo === 253) {
613
+ decoderState.lastTimestamp = undefined;
614
+ }
504
615
  }
505
616
  readDataFromIndex += fDef.size;
506
617
  messageSize += fDef.size;
@@ -508,6 +619,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
508
619
  for (let i = 0; i < developerFieldDefs.length; i++) {
509
620
  const developerFieldDef = developerFieldDefs[i];
510
621
  const rawDataIndex = messageType.fieldDefs.length + i;
622
+ if (rawDeveloperFields
623
+ && readDataFromIndex + developerFieldDef.size <= dataEnd) {
624
+ rawDeveloperFields.push({
625
+ developerDataIndex: developerFieldDef.developerDataIndex,
626
+ fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
627
+ rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
628
+ });
629
+ }
511
630
  const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
512
631
  if (fDef) {
513
632
  const data = readData(blob, dataView, fDef, readDataFromIndex);
@@ -569,6 +688,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
569
688
  fields[field] = formatFieldValue(data, fDef, options, fields);
570
689
  }
571
690
  }
691
+ let compressedTimestamp;
572
692
  if (isCompressedTimestamp) {
573
693
  const previousTimestamp = decoderState.lastTimestamp;
574
694
  if (previousTimestamp === undefined) {
@@ -580,8 +700,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
580
700
  const timeOffset = recordHeader & CompressedTimestampMask;
581
701
  const previousOffset = previousTimestamp & CompressedTimestampMask;
582
702
  const rollover = timeOffset < previousOffset ? 0x20 : 0;
583
- const timestamp = (previousTimestamp & ~CompressedTimestampMask) + timeOffset + rollover;
703
+ const timestamp = previousTimestamp - previousOffset + timeOffset + rollover;
704
+ if (timestamp >= 0xFFFFFFFF) {
705
+ throw new Error('Compressed timestamp exceeds FIT uint32 range');
706
+ }
584
707
  decoderState.lastTimestamp = timestamp;
708
+ compressedTimestamp = timestamp;
585
709
  fields.timestamp = new Date(timestamp * 1000 + GarminTimeOffset);
586
710
  validFieldCount++;
587
711
  }
@@ -613,9 +737,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
613
737
  }
614
738
  }
615
739
  return {
740
+ globalMessageNumber: messageType.globalMessageNumber,
741
+ littleEndian: messageType.littleEndian,
742
+ compressedTimestamp,
616
743
  messageType: message.name,
617
744
  nextIndex: startIndex + messageSize + 1,
618
745
  message: fields,
746
+ rawFields,
747
+ rawDeveloperFields,
619
748
  };
620
749
  }
621
750
  function getArrayBuffer(buffer) {
@@ -2,6 +2,7 @@ import type { Buffer } from 'buffer';
2
2
  import type { ParsedFit } from './fit_types.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
5
+ export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
5
6
  export interface FitParserOptions {
6
7
  force?: boolean;
7
8
  speedUnit?: string;
@@ -10,6 +11,12 @@ export interface FitParserOptions {
10
11
  elapsedRecordField?: boolean;
11
12
  pressureUnit?: string;
12
13
  mode?: 'list' | 'cascade' | 'both';
14
+ /** Retains exact developer-field bytes for all or selected global message numbers. */
15
+ includeRawDeveloperFields?: boolean | readonly number[];
16
+ /** Retains exact native and developer fields for all or selected global message numbers. */
17
+ includeRawMessages?: boolean | readonly number[];
18
+ /** Returns only parser metadata and retained raw messages instead of decoded activity collections. */
19
+ rawMessagesOnly?: boolean;
13
20
  }
14
21
  type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
15
22
  export default class FitParser {
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "FitBaseType", { enumerable: true, get: function
8
8
  Object.defineProperty(exports, "FitEncoder", { enumerable: true, get: function () { return fit_encoder_js_1.FitEncoder; } });
9
9
  class FitParser {
10
10
  constructor(options = {}) {
11
+ var _a, _b, _c;
11
12
  this.options = {
12
13
  force: options.force != null ? options.force : true,
13
14
  speedUnit: options.speedUnit || 'm/s',
@@ -16,6 +17,9 @@ class FitParser {
16
17
  elapsedRecordField: options.elapsedRecordField || false,
17
18
  pressureUnit: options.pressureUnit || 'bar',
18
19
  mode: options.mode || 'list',
20
+ includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
21
+ includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
22
+ rawMessagesOnly: (_c = options.rawMessagesOnly) !== null && _c !== void 0 ? _c : false,
19
23
  };
20
24
  }
21
25
  parseAsync(content) {
@@ -31,7 +35,7 @@ class FitParser {
31
35
  });
32
36
  }
33
37
  parse(content, callback) {
34
- var _a, _b;
38
+ var _a, _b, _c;
35
39
  const blob = content instanceof ArrayBuffer
36
40
  ? new Uint8Array(content)
37
41
  : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
@@ -118,6 +122,15 @@ class FitParser {
118
122
  const time_in_zone = [];
119
123
  const activity_metrics = [];
120
124
  const user_metrics = [];
125
+ const rawDeveloperFields = this.options.includeRawDeveloperFields === true
126
+ || Array.isArray(this.options.includeRawDeveloperFields)
127
+ ? []
128
+ : undefined;
129
+ const rawMessages = this.options.includeRawMessages === true
130
+ || Array.isArray(this.options.includeRawMessages)
131
+ ? []
132
+ : undefined;
133
+ const messageCountsByGlobalNumber = new Map();
121
134
  let loopIndex = headerLength;
122
135
  const messageTypes = [];
123
136
  const developerFields = [];
@@ -128,12 +141,41 @@ class FitParser {
128
141
  let lastStopTimestamp;
129
142
  let pausedTime = 0;
130
143
  while (loopIndex < crcStart) {
131
- const { nextIndex, messageType, message } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState);
144
+ const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
132
145
  loopIndex = nextIndex;
146
+ if (globalMessageNumber !== undefined) {
147
+ const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
148
+ messageCountsByGlobalNumber.set(globalMessageNumber, messageIndex + 1);
149
+ if (recordRawFields && littleEndian !== undefined) {
150
+ rawMessages === null || rawMessages === void 0 ? void 0 : rawMessages.push(Object.assign(Object.assign({ global_message_number: globalMessageNumber, message_index: messageIndex, little_endian: littleEndian }, (compressedTimestamp === undefined
151
+ ? {}
152
+ : { compressed_timestamp: compressedTimestamp })), { fields: recordRawFields.map(field => ({
153
+ field_definition_number: field.fieldDefinitionNumber,
154
+ base_type: field.baseType,
155
+ raw_value: field.rawValue,
156
+ })), developer_fields: (recordRawDeveloperFields !== null && recordRawDeveloperFields !== void 0 ? recordRawDeveloperFields : []).map(field => ({
157
+ developer_data_index: field.developerDataIndex,
158
+ field_definition_number: field.fieldDefinitionNumber,
159
+ raw_value: field.rawValue,
160
+ })) }));
161
+ }
162
+ recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
163
+ rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
164
+ global_message_number: globalMessageNumber,
165
+ message_index: messageIndex,
166
+ developer_data_index: field.developerDataIndex,
167
+ field_definition_number: field.fieldDefinitionNumber,
168
+ raw_value: field.rawValue,
169
+ });
170
+ });
171
+ }
172
+ if (this.options.rawMessagesOnly) {
173
+ continue;
174
+ }
133
175
  if (messageType !== ''
134
176
  && messageType !== 'definition'
135
177
  && message !== undefined) {
136
- ((_a = messages[messageType]) !== null && _a !== void 0 ? _a : (messages[messageType] = [])).push(message);
178
+ ((_b = messages[messageType]) !== null && _b !== void 0 ? _b : (messages[messageType] = [])).push(message);
137
179
  }
138
180
  switch (messageType) {
139
181
  case 'lap':
@@ -249,6 +291,16 @@ class FitParser {
249
291
  break;
250
292
  }
251
293
  }
294
+ if (this.options.rawMessagesOnly) {
295
+ if (rawDeveloperFields) {
296
+ fitObj.raw_developer_fields = rawDeveloperFields;
297
+ }
298
+ if (rawMessages) {
299
+ fitObj.raw_messages = rawMessages;
300
+ }
301
+ callback(undefined, fitObj);
302
+ return;
303
+ }
252
304
  fitObj.hr_zone = hr_zone;
253
305
  fitObj.power_zone = power_zone;
254
306
  fitObj.dive_gases = dive_gases;
@@ -268,11 +320,17 @@ class FitParser {
268
320
  fitObj.activity_metrics = activity_metrics;
269
321
  fitObj.user_metrics = user_metrics;
270
322
  fitObj.messages = messages;
323
+ if (rawDeveloperFields) {
324
+ fitObj.raw_developer_fields = rawDeveloperFields;
325
+ }
326
+ if (rawMessages) {
327
+ fitObj.raw_messages = rawMessages;
328
+ }
271
329
  if (isCascadeNeeded) {
272
330
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'records', records);
273
331
  laps = (0, helper_js_1.mapDataIntoLap)(laps, 'lengths', lengths);
274
332
  sessions = (0, helper_js_1.mapDataIntoSession)(sessions, laps);
275
- fitObj.activity = Object.assign(Object.assign({}, ((_b = fitObj.activity) !== null && _b !== void 0 ? _b : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
333
+ fitObj.activity = Object.assign(Object.assign({}, ((_c = fitObj.activity) !== null && _c !== void 0 ? _c : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
276
334
  sessions,
277
335
  events,
278
336
  hrv,
@@ -9,6 +9,31 @@ export type Unit<T extends string> = Record<T, {
9
9
  multiplier: number;
10
10
  offset: number;
11
11
  }>;
12
+ export interface ParsedRawDeveloperField {
13
+ global_message_number: number;
14
+ message_index: number;
15
+ developer_data_index: number;
16
+ field_definition_number: number;
17
+ raw_value: number[];
18
+ }
19
+ export interface ParsedRawFitField {
20
+ field_definition_number: number;
21
+ base_type: number;
22
+ raw_value: number[];
23
+ }
24
+ export interface ParsedRawFitMessageDeveloperField {
25
+ developer_data_index: number;
26
+ field_definition_number: number;
27
+ raw_value: number[];
28
+ }
29
+ export interface ParsedRawFitMessage {
30
+ global_message_number: number;
31
+ message_index: number;
32
+ little_endian: boolean;
33
+ compressed_timestamp?: number;
34
+ fields: ParsedRawFitField[];
35
+ developer_fields: ParsedRawFitMessageDeveloperField[];
36
+ }
12
37
  export type Activity = 'manual' | 'auto_multi_sport' | number;
13
38
  export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
14
39
  export type ActivityLevel = 'low' | 'medium' | 'high' | number;
@@ -2070,6 +2095,8 @@ export interface ParsedFit {
2070
2095
  protocolVersion?: number;
2071
2096
  profileVersion?: number;
2072
2097
  messages?: ParsedMessages;
2098
+ raw_developer_fields?: ParsedRawDeveloperField[];
2099
+ raw_messages?: ParsedRawFitMessage[];
2073
2100
  file_creator: ParsedFileCreator;
2074
2101
  device_settings: ParsedDeviceSettings;
2075
2102
  dive_summary?: ParsedDiveSummary;
@@ -175,6 +175,31 @@ function generateUtilities() {
175
175
  ]),
176
176
  ]));
177
177
  nodes.push(unitType);
178
+ nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawDeveloperField', undefined, undefined, [
179
+ generateProperty('global_message_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
180
+ generateProperty('message_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
181
+ generateProperty('developer_data_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
182
+ generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
183
+ generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
184
+ ]));
185
+ nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitField', undefined, undefined, [
186
+ generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
187
+ generateProperty('base_type', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
188
+ generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
189
+ ]));
190
+ nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessageDeveloperField', undefined, undefined, [
191
+ generateProperty('developer_data_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
192
+ generateProperty('field_definition_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
193
+ generateArrayProperty('raw_value', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
194
+ ]));
195
+ nodes.push(typescript_1.default.factory.createInterfaceDeclaration([typescript_1.default.factory.createModifier(typescript_1.default.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessage', undefined, undefined, [
196
+ generateProperty('global_message_number', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
197
+ generateProperty('message_index', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword), false),
198
+ generateProperty('little_endian', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.BooleanKeyword), false),
199
+ generateProperty('compressed_timestamp', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
200
+ generateArrayProperty('fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitField'), false),
201
+ generateArrayProperty('developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessageDeveloperField'), false),
202
+ ]));
178
203
  return nodes;
179
204
  }
180
205
  function generateAdditionalFields(msg) {
@@ -258,6 +283,8 @@ function generateFitType() {
258
283
  generateProperty('protocolVersion', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
259
284
  generateProperty('profileVersion', typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.NumberKeyword)),
260
285
  generateProperty('messages', typescript_1.default.factory.createTypeReferenceNode('ParsedMessages')),
286
+ generateArrayProperty('raw_developer_fields', typescript_1.default.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
287
+ generateArrayProperty('raw_messages', typescript_1.default.factory.createTypeReferenceNode('ParsedRawFitMessage')),
261
288
  ...Object.keys(referenceProperties).map(prop => generateProperty(prop, typescript_1.default.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
262
289
  ...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
263
290
  ? typescript_1.default.factory.createKeywordTypeNode(typescript_1.default.SyntaxKind.UnknownKeyword)
@@ -2,6 +2,7 @@ import type { Buffer } from 'buffer';
2
2
  import type { ParsedFit } from './fit_types.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export type { FitEncoderField, FitEncoderOptions } from './fit-encoder.js';
5
+ export type { ParsedFit, ParsedRawDeveloperField, ParsedRawFitField, ParsedRawFitMessage, ParsedRawFitMessageDeveloperField, } from './fit_types.js';
5
6
  export interface FitParserOptions {
6
7
  force?: boolean;
7
8
  speedUnit?: string;
@@ -10,6 +11,12 @@ export interface FitParserOptions {
10
11
  elapsedRecordField?: boolean;
11
12
  pressureUnit?: string;
12
13
  mode?: 'list' | 'cascade' | 'both';
14
+ /** Retains exact developer-field bytes for all or selected global message numbers. */
15
+ includeRawDeveloperFields?: boolean | readonly number[];
16
+ /** Retains exact native and developer fields for all or selected global message numbers. */
17
+ includeRawMessages?: boolean | readonly number[];
18
+ /** Returns only parser metadata and retained raw messages instead of decoded activity collections. */
19
+ rawMessagesOnly?: boolean;
13
20
  }
14
21
  type FitParserCallback = (error: string | undefined, data: ParsedFit | undefined) => void;
15
22
  export default class FitParser {
@@ -3,6 +3,7 @@ import { mapDataIntoLap, mapDataIntoSession } from './helper.js';
3
3
  export { FitBaseType, FitEncoder } from './fit-encoder.js';
4
4
  export default class FitParser {
5
5
  constructor(options = {}) {
6
+ var _a, _b, _c;
6
7
  this.options = {
7
8
  force: options.force != null ? options.force : true,
8
9
  speedUnit: options.speedUnit || 'm/s',
@@ -11,6 +12,9 @@ export default class FitParser {
11
12
  elapsedRecordField: options.elapsedRecordField || false,
12
13
  pressureUnit: options.pressureUnit || 'bar',
13
14
  mode: options.mode || 'list',
15
+ includeRawDeveloperFields: (_a = options.includeRawDeveloperFields) !== null && _a !== void 0 ? _a : false,
16
+ includeRawMessages: (_b = options.includeRawMessages) !== null && _b !== void 0 ? _b : false,
17
+ rawMessagesOnly: (_c = options.rawMessagesOnly) !== null && _c !== void 0 ? _c : false,
14
18
  };
15
19
  }
16
20
  parseAsync(content) {
@@ -26,7 +30,7 @@ export default class FitParser {
26
30
  });
27
31
  }
28
32
  parse(content, callback) {
29
- var _a, _b;
33
+ var _a, _b, _c;
30
34
  const blob = content instanceof ArrayBuffer
31
35
  ? new Uint8Array(content)
32
36
  : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
@@ -113,6 +117,15 @@ export default class FitParser {
113
117
  const time_in_zone = [];
114
118
  const activity_metrics = [];
115
119
  const user_metrics = [];
120
+ const rawDeveloperFields = this.options.includeRawDeveloperFields === true
121
+ || Array.isArray(this.options.includeRawDeveloperFields)
122
+ ? []
123
+ : undefined;
124
+ const rawMessages = this.options.includeRawMessages === true
125
+ || Array.isArray(this.options.includeRawMessages)
126
+ ? []
127
+ : undefined;
128
+ const messageCountsByGlobalNumber = new Map();
116
129
  let loopIndex = headerLength;
117
130
  const messageTypes = [];
118
131
  const developerFields = [];
@@ -123,12 +136,41 @@ export default class FitParser {
123
136
  let lastStopTimestamp;
124
137
  let pausedTime = 0;
125
138
  while (loopIndex < crcStart) {
126
- const { nextIndex, messageType, message } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState);
139
+ const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, } = readRecord(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
127
140
  loopIndex = nextIndex;
141
+ if (globalMessageNumber !== undefined) {
142
+ const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
143
+ messageCountsByGlobalNumber.set(globalMessageNumber, messageIndex + 1);
144
+ if (recordRawFields && littleEndian !== undefined) {
145
+ rawMessages === null || rawMessages === void 0 ? void 0 : rawMessages.push(Object.assign(Object.assign({ global_message_number: globalMessageNumber, message_index: messageIndex, little_endian: littleEndian }, (compressedTimestamp === undefined
146
+ ? {}
147
+ : { compressed_timestamp: compressedTimestamp })), { fields: recordRawFields.map(field => ({
148
+ field_definition_number: field.fieldDefinitionNumber,
149
+ base_type: field.baseType,
150
+ raw_value: field.rawValue,
151
+ })), developer_fields: (recordRawDeveloperFields !== null && recordRawDeveloperFields !== void 0 ? recordRawDeveloperFields : []).map(field => ({
152
+ developer_data_index: field.developerDataIndex,
153
+ field_definition_number: field.fieldDefinitionNumber,
154
+ raw_value: field.rawValue,
155
+ })) }));
156
+ }
157
+ recordRawDeveloperFields === null || recordRawDeveloperFields === void 0 ? void 0 : recordRawDeveloperFields.forEach((field) => {
158
+ rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push({
159
+ global_message_number: globalMessageNumber,
160
+ message_index: messageIndex,
161
+ developer_data_index: field.developerDataIndex,
162
+ field_definition_number: field.fieldDefinitionNumber,
163
+ raw_value: field.rawValue,
164
+ });
165
+ });
166
+ }
167
+ if (this.options.rawMessagesOnly) {
168
+ continue;
169
+ }
128
170
  if (messageType !== ''
129
171
  && messageType !== 'definition'
130
172
  && message !== undefined) {
131
- ((_a = messages[messageType]) !== null && _a !== void 0 ? _a : (messages[messageType] = [])).push(message);
173
+ ((_b = messages[messageType]) !== null && _b !== void 0 ? _b : (messages[messageType] = [])).push(message);
132
174
  }
133
175
  switch (messageType) {
134
176
  case 'lap':
@@ -244,6 +286,16 @@ export default class FitParser {
244
286
  break;
245
287
  }
246
288
  }
289
+ if (this.options.rawMessagesOnly) {
290
+ if (rawDeveloperFields) {
291
+ fitObj.raw_developer_fields = rawDeveloperFields;
292
+ }
293
+ if (rawMessages) {
294
+ fitObj.raw_messages = rawMessages;
295
+ }
296
+ callback(undefined, fitObj);
297
+ return;
298
+ }
247
299
  fitObj.hr_zone = hr_zone;
248
300
  fitObj.power_zone = power_zone;
249
301
  fitObj.dive_gases = dive_gases;
@@ -263,11 +315,17 @@ export default class FitParser {
263
315
  fitObj.activity_metrics = activity_metrics;
264
316
  fitObj.user_metrics = user_metrics;
265
317
  fitObj.messages = messages;
318
+ if (rawDeveloperFields) {
319
+ fitObj.raw_developer_fields = rawDeveloperFields;
320
+ }
321
+ if (rawMessages) {
322
+ fitObj.raw_messages = rawMessages;
323
+ }
266
324
  if (isCascadeNeeded) {
267
325
  laps = mapDataIntoLap(laps, 'records', records);
268
326
  laps = mapDataIntoLap(laps, 'lengths', lengths);
269
327
  sessions = mapDataIntoSession(sessions, laps);
270
- fitObj.activity = Object.assign(Object.assign({}, ((_b = fitObj.activity) !== null && _b !== void 0 ? _b : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
328
+ fitObj.activity = Object.assign(Object.assign({}, ((_c = fitObj.activity) !== null && _c !== void 0 ? _c : {})), { // ugly but we assume the activity was parsed correctly with all other members correctly
271
329
  sessions,
272
330
  events,
273
331
  hrv,
@@ -9,6 +9,31 @@ export type Unit<T extends string> = Record<T, {
9
9
  multiplier: number;
10
10
  offset: number;
11
11
  }>;
12
+ export interface ParsedRawDeveloperField {
13
+ global_message_number: number;
14
+ message_index: number;
15
+ developer_data_index: number;
16
+ field_definition_number: number;
17
+ raw_value: number[];
18
+ }
19
+ export interface ParsedRawFitField {
20
+ field_definition_number: number;
21
+ base_type: number;
22
+ raw_value: number[];
23
+ }
24
+ export interface ParsedRawFitMessageDeveloperField {
25
+ developer_data_index: number;
26
+ field_definition_number: number;
27
+ raw_value: number[];
28
+ }
29
+ export interface ParsedRawFitMessage {
30
+ global_message_number: number;
31
+ message_index: number;
32
+ little_endian: boolean;
33
+ compressed_timestamp?: number;
34
+ fields: ParsedRawFitField[];
35
+ developer_fields: ParsedRawFitMessageDeveloperField[];
36
+ }
12
37
  export type Activity = 'manual' | 'auto_multi_sport' | number;
13
38
  export type ActivityClass = 'level_max' | 'level' | 'athlete' | number;
14
39
  export type ActivityLevel = 'low' | 'medium' | 'high' | number;
@@ -2070,6 +2095,8 @@ export interface ParsedFit {
2070
2095
  protocolVersion?: number;
2071
2096
  profileVersion?: number;
2072
2097
  messages?: ParsedMessages;
2098
+ raw_developer_fields?: ParsedRawDeveloperField[];
2099
+ raw_messages?: ParsedRawFitMessage[];
2073
2100
  file_creator: ParsedFileCreator;
2074
2101
  device_settings: ParsedDeviceSettings;
2075
2102
  dive_summary?: ParsedDiveSummary;
@@ -154,6 +154,31 @@ export function generateUtilities() {
154
154
  ]),
155
155
  ]));
156
156
  nodes.push(unitType);
157
+ nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawDeveloperField', undefined, undefined, [
158
+ generateProperty('global_message_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
159
+ generateProperty('message_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
160
+ generateProperty('developer_data_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
161
+ generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
162
+ generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
163
+ ]));
164
+ nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitField', undefined, undefined, [
165
+ generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
166
+ generateProperty('base_type', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
167
+ generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
168
+ ]));
169
+ nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessageDeveloperField', undefined, undefined, [
170
+ generateProperty('developer_data_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
171
+ generateProperty('field_definition_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
172
+ generateArrayProperty('raw_value', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
173
+ ]));
174
+ nodes.push(ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], 'ParsedRawFitMessage', undefined, undefined, [
175
+ generateProperty('global_message_number', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
176
+ generateProperty('message_index', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword), false),
177
+ generateProperty('little_endian', ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword), false),
178
+ generateProperty('compressed_timestamp', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
179
+ generateArrayProperty('fields', ts.factory.createTypeReferenceNode('ParsedRawFitField'), false),
180
+ generateArrayProperty('developer_fields', ts.factory.createTypeReferenceNode('ParsedRawFitMessageDeveloperField'), false),
181
+ ]));
157
182
  return nodes;
158
183
  }
159
184
  function generateAdditionalFields(msg) {
@@ -237,6 +262,8 @@ export function generateFitType() {
237
262
  generateProperty('protocolVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
238
263
  generateProperty('profileVersion', ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)),
239
264
  generateProperty('messages', ts.factory.createTypeReferenceNode('ParsedMessages')),
265
+ generateArrayProperty('raw_developer_fields', ts.factory.createTypeReferenceNode('ParsedRawDeveloperField')),
266
+ generateArrayProperty('raw_messages', ts.factory.createTypeReferenceNode('ParsedRawFitMessage')),
240
267
  ...Object.keys(referenceProperties).map(prop => generateProperty(prop, ts.factory.createTypeReferenceNode(snakeToCamel(referenceProperties[prop].replace('?', ''))), referenceProperties[prop].startsWith('?'))),
241
268
  ...Object.keys(collectionProperties).map(prop => generateArrayProperty(prop, collectionProperties[prop] === 'unknown'
242
269
  ? ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fit-file-parser",
3
3
  "type": "module",
4
- "version": "5.0.2",
4
+ "version": "5.2.0",
5
5
  "private": false,
6
6
  "description": "Parse your .FIT files easily, directly from JS (Garmin, Polar, Suunto)",
7
7
  "author": {