fit-file-parser 6.1.2 → 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 +56 -0
- package/PROFILE.md +23 -0
- package/README.md +72 -5
- package/dist/binary.js +30 -17
- package/dist/cjs/binary.js +30 -17
- package/dist/cjs/fit-parser.d.ts +11 -0
- package/dist/cjs/fit-parser.js +112 -6
- package/dist/cjs/fit_types.d.ts +23 -1
- package/dist/cjs/profile.js +166 -0
- package/dist/cjs/type_generator.js +1 -0
- package/dist/fit-parser.d.ts +11 -0
- package/dist/fit-parser.js +112 -6
- package/dist/fit_types.d.ts +23 -1
- package/dist/profile.js +166 -0
- package/dist/type_generator.js +1 -0
- package/package.json +14 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
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
|
+
|
|
27
|
+
## 6.2.0 - 2026-10-10
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Add `parseFilesAsync()` to return every complete FIT file in concatenated
|
|
32
|
+
input as a separate result, with independent decoder state and file order.
|
|
33
|
+
- Decode documented session and lap swimming summaries, segment and gear-shift
|
|
34
|
+
counts, lap bounds, metabolic calories, and grade-adjusted speed.
|
|
35
|
+
- Retain every battery sample in `batteries` and `messages.battery`, including
|
|
36
|
+
timestamp and capacity percentage when present.
|
|
37
|
+
- Add `--all-files` to the external corpus checker to validate appended files.
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Preserve exact bytes for developer fields without usable names in opt-in
|
|
42
|
+
`unmapped_messages`, including fields whose descriptions have been cached.
|
|
43
|
+
- Recover unsigned-byte record temperatures from 0 through 126 with normal
|
|
44
|
+
unit conversion, while retaining their original bytes in opt-in output.
|
|
45
|
+
- Reject records and definitions extending beyond the declared data section
|
|
46
|
+
when using the complete-file `parseFilesAsync()` API.
|
|
47
|
+
|
|
48
|
+
### Compatibility
|
|
49
|
+
|
|
50
|
+
- Existing `parseAsync()` and callback APIs retain their single-file output
|
|
51
|
+
and recovery behavior. Newly supported fields and battery collections are
|
|
52
|
+
additive.
|
|
53
|
+
- `parseFilesAsync()` returns a one-element array for ordinary FIT input and
|
|
54
|
+
rejects incomplete files or trailing non-FIT data, even with `force: true`.
|
|
55
|
+
CRC validation follows the caller's `force` option for every file.
|
|
56
|
+
- Unsigned temperature bytes from 127 through 255 and other undocumented or
|
|
57
|
+
incompatible fields remain undecoded and available through opt-in retention.
|
|
58
|
+
|
|
3
59
|
## 6.1.2 - 2026-09-22
|
|
4
60
|
|
|
5
61
|
### Changed
|
package/PROFILE.md
CHANGED
|
@@ -31,6 +31,23 @@ complete raw representation of selected messages can continue to use
|
|
|
31
31
|
`includeRawMessages`; `unmapped_messages` stores only the fields the maintained
|
|
32
32
|
table cannot decode.
|
|
33
33
|
|
|
34
|
+
The unsigned-byte record-temperature interoperability recovery also retains
|
|
35
|
+
the original byte here, even when its unambiguous value is decoded.
|
|
36
|
+
|
|
37
|
+
## Focused interoperability additions
|
|
38
|
+
|
|
39
|
+
Session fields 79, 80, 106, 107, 108, and 211; lap fields 27–30, 73, 90, 96,
|
|
40
|
+
97, 155, and 161; and battery message 104 fields 2 and 253 were checked against
|
|
41
|
+
independently observed wire definitions in the external FIT corpus and the
|
|
42
|
+
redistributable interoperability definitions for
|
|
43
|
+
[session](https://github.com/muktihari/fit/blob/126e1a11233f87b7f139254d991bc3d2d952b62c/profile/mesgdef/session_gen.go),
|
|
44
|
+
[lap](https://github.com/muktihari/fit/blob/126e1a11233f87b7f139254d991bc3d2d952b62c/profile/mesgdef/lap_gen.go),
|
|
45
|
+
and [battery](https://github.com/muktihari/fit/blob/126e1a11233f87b7f139254d991bc3d2d952b62c/profile/mesgdef/battery_gen.go).
|
|
46
|
+
The focused mappings expose swimming summaries, segment and shift counts,
|
|
47
|
+
lap bounds, metabolic calories, grade-adjusted speed, and battery samples.
|
|
48
|
+
Synthetic regression tests pin identifiers, wire types, scales, units, invalid
|
|
49
|
+
sentinels, and collection behavior. Other battery fields remain unmapped.
|
|
50
|
+
|
|
34
51
|
## Updating the table
|
|
35
52
|
|
|
36
53
|
Every mapping change requires focused regression coverage. Do not guess
|
|
@@ -45,7 +62,13 @@ npm run profile:check
|
|
|
45
62
|
npm run check
|
|
46
63
|
npm run compatibility:check -- /path/to/reference-package /path/to/fit-corpus
|
|
47
64
|
npm run corpus:check -- ../FIT-test-files --allow-force-recovery
|
|
65
|
+
npm run corpus:check -- ../FIT-test-files --allow-force-recovery --all-files
|
|
48
66
|
```
|
|
49
67
|
|
|
50
68
|
`src/fit_types.ts` is generated from the maintained table and must not be
|
|
51
69
|
edited manually.
|
|
70
|
+
|
|
71
|
+
`--all-files` validates every appended FIT file through `parseFilesAsync()` and
|
|
72
|
+
reports both parsed FIT file counts and concatenated input counts. Recovery
|
|
73
|
+
counts apply to whole inputs; unmapped summaries count each parsed FIT file
|
|
74
|
+
separately in this mode.
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ including developer-defined data.
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- Parse Node.js `Buffer` and standard `ArrayBuffer` inputs.
|
|
14
|
+
- Parse concatenated FIT files as separate results with `parseFilesAsync()`.
|
|
14
15
|
- Choose flat lists, nested activity data, or both output shapes.
|
|
15
16
|
- Convert speed, length, temperature, and pressure fields to preferred units.
|
|
16
17
|
- Decode developer fields while preserving record alignment when descriptions
|
|
@@ -33,7 +34,7 @@ npm install fit-file-parser
|
|
|
33
34
|
|
|
34
35
|
## Profile coverage
|
|
35
36
|
|
|
36
|
-
The static profile contains
|
|
37
|
+
The static profile contains 127 messages, 1,462 fields, 200 types, and 4,406
|
|
37
38
|
mapped values. Pass `includeUnmappedMessages: true` to retain fields outside
|
|
38
39
|
that maintained surface by number and exact bytes; applications that need all
|
|
39
40
|
bytes for recognized messages can also opt into `raw_messages`.
|
|
@@ -62,6 +63,38 @@ console.log({
|
|
|
62
63
|
})
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
### Concatenated FIT files
|
|
67
|
+
|
|
68
|
+
Some exports append several complete FIT files into one input. Use
|
|
69
|
+
`parseFilesAsync()` to return a `ParsedFit[]` in file order:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const files = await new FitParser({ force: false }).parseFilesAsync(content)
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
console.log(file.sessions?.length ?? 0)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Each result has independent message definitions, developer descriptions,
|
|
79
|
+
timestamps, and activity collections. Results are never merged. A single FIT
|
|
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.
|
|
97
|
+
|
|
65
98
|
### Callback API
|
|
66
99
|
|
|
67
100
|
```javascript
|
|
@@ -86,8 +119,9 @@ readFile('./activity.fit', (readError, content) => {
|
|
|
86
119
|
})
|
|
87
120
|
```
|
|
88
121
|
|
|
89
|
-
|
|
90
|
-
|
|
122
|
+
The callback reports validation errors as strings. `parseAsync()` rejects with
|
|
123
|
+
those values; decoder failures can reject with an `Error`. `parseFilesAsync()`
|
|
124
|
+
consistently rejects with an `Error`.
|
|
91
125
|
|
|
92
126
|
## Parser options
|
|
93
127
|
|
|
@@ -97,6 +131,7 @@ All options are optional.
|
|
|
97
131
|
| --------------------------- | --------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
98
132
|
| `mode` | `list`, `cascade`, `both` | `list` | Controls whether primary activity collections are returned as root lists, nested data, or both. |
|
|
99
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`. |
|
|
100
135
|
| `speedUnit` | `m/s`, `km/h`, `mph` | `m/s` | Converts speed-related fields. |
|
|
101
136
|
| `lengthUnit` | `m`, `km`, `mi` | `m` | Converts distance, altitude, and other length-related fields. |
|
|
102
137
|
| `temperatureUnit` | `celsius`, `°C`, `kelvin`, `fahrenheit` | `celsius` | Converts temperature fields. `°C` remains available as a legacy alias. |
|
|
@@ -110,6 +145,24 @@ All options are optional.
|
|
|
110
145
|
too short, have an invalid header size or signature, or declare data beyond the
|
|
111
146
|
available bytes are rejected in both modes.
|
|
112
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
|
+
|
|
113
166
|
### Lossless developer fields
|
|
114
167
|
|
|
115
168
|
By default, developer fields retain the existing decoded, name-keyed output.
|
|
@@ -148,8 +201,8 @@ message numbers.
|
|
|
148
201
|
|
|
149
202
|
Set `includeUnmappedMessages: true` to return fields that are absent from the
|
|
150
203
|
maintained profile or belong to an unknown global message in
|
|
151
|
-
`unmapped_messages`.
|
|
152
|
-
are not duplicated:
|
|
204
|
+
`unmapped_messages`. Fields without usable developer names are also retained.
|
|
205
|
+
Normal known fields are not duplicated:
|
|
153
206
|
|
|
154
207
|
```javascript
|
|
155
208
|
const data = await new FitParser({ includeUnmappedMessages: true })
|
|
@@ -169,6 +222,20 @@ Each entry uses the same wire-level field representation as `raw_messages`.
|
|
|
169
222
|
This opt-in ensures a newer FIT field cannot disappear merely because it
|
|
170
223
|
does not yet have a reviewed semantic mapping.
|
|
171
224
|
|
|
225
|
+
For record temperature encoded as one unsigned byte, values from 0 through
|
|
226
|
+
126 can be decoded as Celsius before the selected temperature conversion.
|
|
227
|
+
Values from 127 through 255 remain undecoded. This narrow interoperability
|
|
228
|
+
recovery preserves the original unsigned byte in `unmapped_messages` when
|
|
229
|
+
enabled, including values that also receive a decoded temperature.
|
|
230
|
+
|
|
231
|
+
### Battery samples
|
|
232
|
+
|
|
233
|
+
`batteries` retains every battery message in file order, including its
|
|
234
|
+
timestamp and capacity percentage when present. These samples are also
|
|
235
|
+
available in `messages.battery`. Undocumented battery fields remain available
|
|
236
|
+
through `includeUnmappedMessages`; the parser does not infer their device
|
|
237
|
+
association or calculate battery consumption.
|
|
238
|
+
|
|
172
239
|
### Lossless selected messages
|
|
173
240
|
|
|
174
241
|
Set `includeRawMessages` when a consumer needs native FIT numeric codes rather
|
package/dist/binary.js
CHANGED
|
@@ -305,6 +305,7 @@ function applyOptions(data, field, options, fields) {
|
|
|
305
305
|
case 'enhanced_speed':
|
|
306
306
|
case 'vertical_speed':
|
|
307
307
|
case 'avg_speed':
|
|
308
|
+
case 'avg_grade_adjusted_speed':
|
|
308
309
|
case 'max_speed':
|
|
309
310
|
case 'speed1s':
|
|
310
311
|
case 'ball_speed':
|
|
@@ -423,14 +424,14 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
423
424
|
else if ((recordHeader & 64) === 64) {
|
|
424
425
|
// is definition message
|
|
425
426
|
// startIndex + 1 is reserved
|
|
426
|
-
if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
|
|
427
|
+
if ((options.strict || retainsRawMessages(options)) && startIndex + 6 > dataEnd) {
|
|
427
428
|
throw new Error('Invalid FIT definition bounds');
|
|
428
429
|
}
|
|
429
430
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
430
431
|
const lEnd = blob[startIndex + 2] === 0;
|
|
431
432
|
const numberOfFields = blob[startIndex + 5];
|
|
432
433
|
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
433
|
-
if (retainsRawMessages(options)
|
|
434
|
+
if ((options.strict || retainsRawMessages(options))
|
|
434
435
|
&& ((recordHeader & 16) !== 0
|
|
435
436
|
|| blob[startIndex + 1] !== 0
|
|
436
437
|
|| blob[startIndex + 2] > 1
|
|
@@ -443,7 +444,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
443
444
|
: 0;
|
|
444
445
|
const definitionEnd = nativeDefinitionsEnd
|
|
445
446
|
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
446
|
-
if (retainsRawMessages(options) && definitionEnd > dataEnd) {
|
|
447
|
+
if ((options.strict || retainsRawMessages(options)) && definitionEnd > dataEnd) {
|
|
447
448
|
throw new Error('Invalid FIT developer definition bounds');
|
|
448
449
|
}
|
|
449
450
|
const mTypeDef = {
|
|
@@ -465,7 +466,12 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
465
466
|
const fieldSize = blob[fDefIndex + 1];
|
|
466
467
|
const wireType = FIT.types.fit_base_type[baseType];
|
|
467
468
|
const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
|
|
468
|
-
|
|
469
|
+
// Narrow producer interoperability case: only the shared, non-sentinel
|
|
470
|
+
// temperature range is decoded below. All original bytes remain unmapped.
|
|
471
|
+
const unsignedTemperature = mTypeDef.globalMessageNumber === 20
|
|
472
|
+
&& fieldNumber === 13 && fieldSize === 1
|
|
473
|
+
&& profileBaseType === 'sint8' && wireType === 'uint8';
|
|
474
|
+
const profileCompatible = unsignedTemperature || areProfileBaseTypesCompatible(profileBaseType, wireType);
|
|
469
475
|
const fDef = {
|
|
470
476
|
type: profileCompatible ? type : wireType,
|
|
471
477
|
rawType: wireType,
|
|
@@ -524,7 +530,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
524
530
|
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
525
531
|
const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
|
|
526
532
|
const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
|
|
527
|
-
if (retainsRawMessages(options)) {
|
|
533
|
+
if (options.strict || retainsRawMessages(options)) {
|
|
528
534
|
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
529
535
|
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
530
536
|
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
@@ -539,7 +545,11 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
539
545
|
rawData[i] = InvalidFieldData;
|
|
540
546
|
continue;
|
|
541
547
|
}
|
|
542
|
-
|
|
548
|
+
const unsignedTemperature = messageType.globalMessageNumber === 20
|
|
549
|
+
&& fDef.fDefNo === 13 && fDef.size === 1
|
|
550
|
+
&& fDef.type === 'sint8' && fDef.rawType === 'uint8';
|
|
551
|
+
const preserveNative = !isOutputFieldName(fDef.name) || unsignedTemperature;
|
|
552
|
+
if ((rawFields || (unmappedFields && preserveNative))
|
|
543
553
|
&& readDataFromIndex + fDef.size <= dataEnd) {
|
|
544
554
|
const rawField = {
|
|
545
555
|
fieldDefinitionNumber: fDef.fDefNo,
|
|
@@ -547,11 +557,13 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
547
557
|
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
|
|
548
558
|
};
|
|
549
559
|
rawFields === null || rawFields === void 0 ? void 0 : rawFields.push(rawField);
|
|
550
|
-
if (
|
|
560
|
+
if (preserveNative) {
|
|
551
561
|
unmappedFields === null || unmappedFields === void 0 ? void 0 : unmappedFields.push(rawField);
|
|
552
562
|
}
|
|
553
563
|
}
|
|
554
|
-
const data =
|
|
564
|
+
const data = unsignedTemperature && blob[readDataFromIndex] >= 127
|
|
565
|
+
? InvalidFieldData
|
|
566
|
+
: readData(blob, dataView, fDef, readDataFromIndex);
|
|
555
567
|
if (data !== InvalidFieldData
|
|
556
568
|
&& !isInvalidValue(data, fDef.type)
|
|
557
569
|
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
@@ -573,20 +585,21 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
573
585
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
574
586
|
const developerFieldDef = developerFieldDefs[i];
|
|
575
587
|
const rawDataIndex = messageType.fieldDefs.length + i;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
588
|
+
// Resolve before deciding whether raw retention is needed. A resolved
|
|
589
|
+
// description can still have no usable output name, including after cache use.
|
|
590
|
+
const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
|
|
591
|
+
const preserveDeveloper = !fDef || !isOutputFieldName(fDef.name);
|
|
592
|
+
if ((rawDeveloperFields || (unmappedDeveloperFields && preserveDeveloper))
|
|
579
593
|
&& readDataFromIndex + developerFieldDef.size <= dataEnd) {
|
|
580
|
-
rawDeveloperField = {
|
|
594
|
+
const rawDeveloperField = {
|
|
581
595
|
developerDataIndex: developerFieldDef.developerDataIndex,
|
|
582
596
|
fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
|
|
583
597
|
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
|
|
584
598
|
};
|
|
585
599
|
rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push(rawDeveloperField);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
|
|
600
|
+
if (preserveDeveloper) {
|
|
601
|
+
unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
|
|
602
|
+
}
|
|
590
603
|
}
|
|
591
604
|
if (fDef) {
|
|
592
605
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
@@ -652,7 +665,7 @@ export function readRecord(blob, messageTypes, developerFields, startIndex, opti
|
|
|
652
665
|
if (isCompressedTimestamp) {
|
|
653
666
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
654
667
|
if (previousTimestamp === undefined) {
|
|
655
|
-
if (!options.force) {
|
|
668
|
+
if (!options.force || options.strict) {
|
|
656
669
|
throw new Error('Compressed timestamp requires a previous timestamp');
|
|
657
670
|
}
|
|
658
671
|
}
|
package/dist/cjs/binary.js
CHANGED
|
@@ -311,6 +311,7 @@ function applyOptions(data, field, options, fields) {
|
|
|
311
311
|
case 'enhanced_speed':
|
|
312
312
|
case 'vertical_speed':
|
|
313
313
|
case 'avg_speed':
|
|
314
|
+
case 'avg_grade_adjusted_speed':
|
|
314
315
|
case 'max_speed':
|
|
315
316
|
case 'speed1s':
|
|
316
317
|
case 'ball_speed':
|
|
@@ -429,14 +430,14 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
429
430
|
else if ((recordHeader & 64) === 64) {
|
|
430
431
|
// is definition message
|
|
431
432
|
// startIndex + 1 is reserved
|
|
432
|
-
if (retainsRawMessages(options) && startIndex + 6 > dataEnd) {
|
|
433
|
+
if ((options.strict || retainsRawMessages(options)) && startIndex + 6 > dataEnd) {
|
|
433
434
|
throw new Error('Invalid FIT definition bounds');
|
|
434
435
|
}
|
|
435
436
|
const hasDeveloperData = (recordHeader & 32) === 32;
|
|
436
437
|
const lEnd = blob[startIndex + 2] === 0;
|
|
437
438
|
const numberOfFields = blob[startIndex + 5];
|
|
438
439
|
const nativeDefinitionsEnd = startIndex + 6 + numberOfFields * 3;
|
|
439
|
-
if (retainsRawMessages(options)
|
|
440
|
+
if ((options.strict || retainsRawMessages(options))
|
|
440
441
|
&& ((recordHeader & 16) !== 0
|
|
441
442
|
|| blob[startIndex + 1] !== 0
|
|
442
443
|
|| blob[startIndex + 2] > 1
|
|
@@ -449,7 +450,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
449
450
|
: 0;
|
|
450
451
|
const definitionEnd = nativeDefinitionsEnd
|
|
451
452
|
+ (hasDeveloperData ? 1 + numberOfDeveloperDataFields * 3 : 0);
|
|
452
|
-
if (retainsRawMessages(options) && definitionEnd > dataEnd) {
|
|
453
|
+
if ((options.strict || retainsRawMessages(options)) && definitionEnd > dataEnd) {
|
|
453
454
|
throw new Error('Invalid FIT developer definition bounds');
|
|
454
455
|
}
|
|
455
456
|
const mTypeDef = {
|
|
@@ -471,7 +472,12 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
471
472
|
const fieldSize = blob[fDefIndex + 1];
|
|
472
473
|
const wireType = fit_js_1.FIT.types.fit_base_type[baseType];
|
|
473
474
|
const { field, type, baseType: profileBaseType, array, scale, offset, units, } = message.getAttributes(blob[fDefIndex]);
|
|
474
|
-
|
|
475
|
+
// Narrow producer interoperability case: only the shared, non-sentinel
|
|
476
|
+
// temperature range is decoded below. All original bytes remain unmapped.
|
|
477
|
+
const unsignedTemperature = mTypeDef.globalMessageNumber === 20
|
|
478
|
+
&& fieldNumber === 13 && fieldSize === 1
|
|
479
|
+
&& profileBaseType === 'sint8' && wireType === 'uint8';
|
|
480
|
+
const profileCompatible = unsignedTemperature || areProfileBaseTypesCompatible(profileBaseType, wireType);
|
|
475
481
|
const fDef = {
|
|
476
482
|
type: profileCompatible ? type : wireType,
|
|
477
483
|
rawType: wireType,
|
|
@@ -530,7 +536,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
530
536
|
const rawDeveloperFields = includeRawDeveloperFields || includeRawMessage ? [] : undefined;
|
|
531
537
|
const unmappedFields = options.includeUnmappedMessages ? [] : undefined;
|
|
532
538
|
const unmappedDeveloperFields = options.includeUnmappedMessages ? [] : undefined;
|
|
533
|
-
if (retainsRawMessages(options)) {
|
|
539
|
+
if (options.strict || retainsRawMessages(options)) {
|
|
534
540
|
const nativeSize = messageType.fieldDefs.reduce((total, field, index) => (total + (isCompressedTimestamp && index === 0 && field.fDefNo === 253 ? 0 : field.size)), 0);
|
|
535
541
|
const developerSize = developerFieldDefs.reduce((total, field) => total + field.size, 0);
|
|
536
542
|
if (startIndex + 1 + nativeSize + developerSize > dataEnd) {
|
|
@@ -545,7 +551,11 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
545
551
|
rawData[i] = InvalidFieldData;
|
|
546
552
|
continue;
|
|
547
553
|
}
|
|
548
|
-
|
|
554
|
+
const unsignedTemperature = messageType.globalMessageNumber === 20
|
|
555
|
+
&& fDef.fDefNo === 13 && fDef.size === 1
|
|
556
|
+
&& fDef.type === 'sint8' && fDef.rawType === 'uint8';
|
|
557
|
+
const preserveNative = !isOutputFieldName(fDef.name) || unsignedTemperature;
|
|
558
|
+
if ((rawFields || (unmappedFields && preserveNative))
|
|
549
559
|
&& readDataFromIndex + fDef.size <= dataEnd) {
|
|
550
560
|
const rawField = {
|
|
551
561
|
fieldDefinitionNumber: fDef.fDefNo,
|
|
@@ -553,11 +563,13 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
553
563
|
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + fDef.size)),
|
|
554
564
|
};
|
|
555
565
|
rawFields === null || rawFields === void 0 ? void 0 : rawFields.push(rawField);
|
|
556
|
-
if (
|
|
566
|
+
if (preserveNative) {
|
|
557
567
|
unmappedFields === null || unmappedFields === void 0 ? void 0 : unmappedFields.push(rawField);
|
|
558
568
|
}
|
|
559
569
|
}
|
|
560
|
-
const data =
|
|
570
|
+
const data = unsignedTemperature && blob[readDataFromIndex] >= 127
|
|
571
|
+
? InvalidFieldData
|
|
572
|
+
: readData(blob, dataView, fDef, readDataFromIndex);
|
|
561
573
|
if (data !== InvalidFieldData
|
|
562
574
|
&& !isInvalidValue(data, fDef.type)
|
|
563
575
|
&& !isInvalidBaseTypeValue(data, fDef.baseTypeNo)) {
|
|
@@ -579,20 +591,21 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
579
591
|
for (let i = 0; i < developerFieldDefs.length; i++) {
|
|
580
592
|
const developerFieldDef = developerFieldDefs[i];
|
|
581
593
|
const rawDataIndex = messageType.fieldDefs.length + i;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
594
|
+
// Resolve before deciding whether raw retention is needed. A resolved
|
|
595
|
+
// description can still have no usable output name, including after cache use.
|
|
596
|
+
const fDef = resolveDeveloperFieldDefinition(developerFieldDef, messageType.littleEndian, developerFields, options);
|
|
597
|
+
const preserveDeveloper = !fDef || !isOutputFieldName(fDef.name);
|
|
598
|
+
if ((rawDeveloperFields || (unmappedDeveloperFields && preserveDeveloper))
|
|
585
599
|
&& readDataFromIndex + developerFieldDef.size <= dataEnd) {
|
|
586
|
-
rawDeveloperField = {
|
|
600
|
+
const rawDeveloperField = {
|
|
587
601
|
developerDataIndex: developerFieldDef.developerDataIndex,
|
|
588
602
|
fieldDefinitionNumber: developerFieldDef.fieldDefinitionNumber,
|
|
589
603
|
rawValue: Array.from(blob.subarray(readDataFromIndex, readDataFromIndex + developerFieldDef.size)),
|
|
590
604
|
};
|
|
591
605
|
rawDeveloperFields === null || rawDeveloperFields === void 0 ? void 0 : rawDeveloperFields.push(rawDeveloperField);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
|
|
606
|
+
if (preserveDeveloper) {
|
|
607
|
+
unmappedDeveloperFields === null || unmappedDeveloperFields === void 0 ? void 0 : unmappedDeveloperFields.push(rawDeveloperField);
|
|
608
|
+
}
|
|
596
609
|
}
|
|
597
610
|
if (fDef) {
|
|
598
611
|
const data = readData(blob, dataView, fDef, readDataFromIndex);
|
|
@@ -658,7 +671,7 @@ function readRecord(blob, messageTypes, developerFields, startIndex, options, st
|
|
|
658
671
|
if (isCompressedTimestamp) {
|
|
659
672
|
const previousTimestamp = decoderState.lastTimestamp;
|
|
660
673
|
if (previousTimestamp === undefined) {
|
|
661
|
-
if (!options.force) {
|
|
674
|
+
if (!options.force || options.strict) {
|
|
662
675
|
throw new Error('Compressed timestamp requires a previous timestamp');
|
|
663
676
|
}
|
|
664
677
|
}
|
package/dist/cjs/fit-parser.d.ts
CHANGED
|
@@ -7,7 +7,10 @@ export { getFitCoursePointId, getFitGarminProductDisplayName, getFitGarminProduc
|
|
|
7
7
|
export { FitMessageReaderError, fitTimestampToUnixMilliseconds, getFitBaseTypeId, readFitMessages, readFitStringField, readFitUnsignedField, } from './raw-message-reader.js';
|
|
8
8
|
export type { FitMessageReaderErrorCode, FitMessageReaderIssue, FitMessageReaderIssueCode, FitMessageReaderOptions, FitMessageReaderResult, FitRawDeveloperField, FitRawField, FitRawMessage, } from './raw-message-reader.js';
|
|
9
9
|
export interface FitParserOptions {
|
|
10
|
+
/** Skips CRC validation and enables supported best-effort recovery. Defaults to true. */
|
|
10
11
|
force?: boolean;
|
|
12
|
+
/** Requires complete files and records, and rejects trailing bytes. Defaults to false. */
|
|
13
|
+
strict?: boolean;
|
|
11
14
|
speedUnit?: string;
|
|
12
15
|
lengthUnit?: string;
|
|
13
16
|
temperatureUnit?: string;
|
|
@@ -28,5 +31,13 @@ export default class FitParser {
|
|
|
28
31
|
private readonly options;
|
|
29
32
|
constructor(options?: FitParserOptions);
|
|
30
33
|
parseAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit>;
|
|
34
|
+
/**
|
|
35
|
+
* Parses FIT files in input order with independent decoder state. Uses the
|
|
36
|
+
* same force and strict policies as parseAsync; strict mode rejects trailing
|
|
37
|
+
* bytes, while the default ignores a non-FIT suffix after parsed files.
|
|
38
|
+
* Rejects with an Error when any file cannot be parsed.
|
|
39
|
+
*/
|
|
40
|
+
parseFilesAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit[]>;
|
|
31
41
|
parse(content: ArrayBuffer | Buffer<ArrayBuffer>, callback: FitParserCallback): void;
|
|
42
|
+
private parseFile;
|
|
32
43
|
}
|