fit-file-parser 6.1.1 → 6.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 +48 -0
- package/PROFILE.md +39 -8
- package/README.md +49 -10
- package/dist/binary.js +25 -12
- package/dist/cjs/binary.js +25 -12
- package/dist/cjs/fit-parser.d.ts +8 -0
- package/dist/cjs/fit-parser.js +84 -0
- package/dist/cjs/fit_types.d.ts +23 -1
- package/dist/cjs/profile-lookup-data.d.ts +12 -0
- package/dist/cjs/profile-lookup-data.js +990 -0
- package/dist/cjs/profile-lookup.js +8 -13
- package/dist/cjs/profile.js +172 -981
- package/dist/cjs/type_generator.js +1 -0
- package/dist/fit-parser.d.ts +8 -0
- package/dist/fit-parser.js +84 -0
- package/dist/fit_types.d.ts +23 -1
- package/dist/profile-lookup-data.d.ts +12 -0
- package/dist/profile-lookup-data.js +987 -0
- package/dist/profile-lookup.js +1 -6
- package/dist/profile.js +172 -981
- package/dist/type_generator.js +1 -0
- package/package.json +18 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 6.2.0 - 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Add `parseFilesAsync()` to return every complete FIT file in concatenated
|
|
8
|
+
input as a separate result, with independent decoder state and file order.
|
|
9
|
+
- Decode documented session and lap swimming summaries, segment and gear-shift
|
|
10
|
+
counts, lap bounds, metabolic calories, and grade-adjusted speed.
|
|
11
|
+
- Retain every battery sample in `batteries` and `messages.battery`, including
|
|
12
|
+
timestamp and capacity percentage when present.
|
|
13
|
+
- Add `--all-files` to the external corpus checker to validate appended files.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Preserve exact bytes for developer fields without usable names in opt-in
|
|
18
|
+
`unmapped_messages`, including fields whose descriptions have been cached.
|
|
19
|
+
- Recover unsigned-byte record temperatures from 0 through 126 with normal
|
|
20
|
+
unit conversion, while retaining their original bytes in opt-in output.
|
|
21
|
+
- Reject records and definitions extending beyond the declared data section
|
|
22
|
+
when using the complete-file `parseFilesAsync()` API.
|
|
23
|
+
|
|
24
|
+
### Compatibility
|
|
25
|
+
|
|
26
|
+
- Existing `parseAsync()` and callback APIs retain their single-file output
|
|
27
|
+
and recovery behavior. Newly supported fields and battery collections are
|
|
28
|
+
additive.
|
|
29
|
+
- `parseFilesAsync()` returns a one-element array for ordinary FIT input and
|
|
30
|
+
rejects incomplete files or trailing non-FIT data, even with `force: true`.
|
|
31
|
+
CRC validation follows the caller's `force` option for every file.
|
|
32
|
+
- Unsigned temperature bytes from 127 through 255 and other undocumented or
|
|
33
|
+
incompatible fields remain undecoded and available through opt-in retention.
|
|
34
|
+
|
|
35
|
+
## 6.1.2 - 2026-09-22
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Keep the manufacturer, Garmin product, sport, sub-sport, and course-point
|
|
40
|
+
lookup maps in one authoritative internal module shared by the full decoder
|
|
41
|
+
profile and the lightweight `fit-file-parser/profile` entry point.
|
|
42
|
+
- Prevent lookup-only consumers from loading the full decoder profile and its
|
|
43
|
+
message definitions.
|
|
44
|
+
- Enforce the lookup entry point's dependency graph and a 25 KB minified bundle
|
|
45
|
+
ceiling in the profile checks run by CI.
|
|
46
|
+
|
|
47
|
+
### Compatibility
|
|
48
|
+
|
|
49
|
+
- Profile mappings, parsed output, and the public TypeScript API are unchanged.
|
|
50
|
+
|
|
3
51
|
## 6.1.1 - 2026-09-21
|
|
4
52
|
|
|
5
53
|
### Added
|
package/PROFILE.md
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
# FIT Profile Maintenance
|
|
2
2
|
|
|
3
|
-
The parser uses one static interoperability
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
The parser uses one static interoperability profile for normal installs,
|
|
4
|
+
builds, tests, and releases. Message definitions and general enum tables live
|
|
5
|
+
in `src/profile.ts`; the manufacturer, Garmin product, sport, sub-sport, and
|
|
6
|
+
course-point tables live in `src/profile-lookup-data.ts`. The full profile
|
|
7
|
+
composes those exact objects, so lookup-only consumers and the decoder share
|
|
8
|
+
one authoritative value for every mapping. `npm run profile:check` pins
|
|
9
|
+
full-table fingerprints and the resulting message, field, type, enum-value,
|
|
10
|
+
and product counts so accidental drift fails CI.
|
|
11
|
+
|
|
12
|
+
`npm run profile:budget` also bundles the lookup entry point in isolation. It
|
|
13
|
+
fails if anything beyond the lookup helpers and their authoritative data enters
|
|
14
|
+
that graph, or if the minified bundle exceeds 25,000 bytes. Review any budget
|
|
15
|
+
change together with the corresponding profile growth.
|
|
9
16
|
|
|
10
17
|
## Unknown and newer fields
|
|
11
18
|
|
|
@@ -24,12 +31,30 @@ complete raw representation of selected messages can continue to use
|
|
|
24
31
|
`includeRawMessages`; `unmapped_messages` stores only the fields the maintained
|
|
25
32
|
table cannot decode.
|
|
26
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
|
+
|
|
27
51
|
## Updating the table
|
|
28
52
|
|
|
29
53
|
Every mapping change requires focused regression coverage. Do not guess
|
|
30
54
|
adjacent identifiers or add a mapping solely to make `unmapped_messages`
|
|
31
|
-
disappear. Update `src/profile.ts
|
|
32
|
-
|
|
55
|
+
disappear. Update the owning table in `src/profile.ts` or
|
|
56
|
+
`src/profile-lookup-data.ts`, the relevant tests, and the public types, then
|
|
57
|
+
verify the complete compatibility contract:
|
|
33
58
|
|
|
34
59
|
```sh
|
|
35
60
|
npm run codegen
|
|
@@ -37,7 +62,13 @@ npm run profile:check
|
|
|
37
62
|
npm run check
|
|
38
63
|
npm run compatibility:check -- /path/to/reference-package /path/to/fit-corpus
|
|
39
64
|
npm run corpus:check -- ../FIT-test-files --allow-force-recovery
|
|
65
|
+
npm run corpus:check -- ../FIT-test-files --allow-force-recovery --all-files
|
|
40
66
|
```
|
|
41
67
|
|
|
42
68
|
`src/fit_types.ts` is generated from the maintained table and must not be
|
|
43
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,27 @@ 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. Empty input, incomplete files (including
|
|
81
|
+
missing file CRC bytes), and trailing non-FIT bytes reject the entire call,
|
|
82
|
+
even with `force: true`. Records and definitions extending beyond the declared
|
|
83
|
+
data section are also rejected. CRC validation follows the `force` option for every
|
|
84
|
+
file. Failures reject with an `Error`. The existing `parseAsync()` and callback
|
|
85
|
+
APIs continue to parse the first FIT file only.
|
|
86
|
+
|
|
65
87
|
### Callback API
|
|
66
88
|
|
|
67
89
|
```javascript
|
|
@@ -86,8 +108,9 @@ readFile('./activity.fit', (readError, content) => {
|
|
|
86
108
|
})
|
|
87
109
|
```
|
|
88
110
|
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
The callback reports validation errors as strings. `parseAsync()` rejects with
|
|
112
|
+
those values; decoder failures can reject with an `Error`. `parseFilesAsync()`
|
|
113
|
+
consistently rejects with an `Error`.
|
|
91
114
|
|
|
92
115
|
## Parser options
|
|
93
116
|
|
|
@@ -148,8 +171,8 @@ message numbers.
|
|
|
148
171
|
|
|
149
172
|
Set `includeUnmappedMessages: true` to return fields that are absent from the
|
|
150
173
|
maintained profile or belong to an unknown global message in
|
|
151
|
-
`unmapped_messages`.
|
|
152
|
-
are not duplicated:
|
|
174
|
+
`unmapped_messages`. Fields without usable developer names are also retained.
|
|
175
|
+
Normal known fields are not duplicated:
|
|
153
176
|
|
|
154
177
|
```javascript
|
|
155
178
|
const data = await new FitParser({ includeUnmappedMessages: true })
|
|
@@ -169,6 +192,20 @@ Each entry uses the same wire-level field representation as `raw_messages`.
|
|
|
169
192
|
This opt-in ensures a newer FIT field cannot disappear merely because it
|
|
170
193
|
does not yet have a reviewed semantic mapping.
|
|
171
194
|
|
|
195
|
+
For record temperature encoded as one unsigned byte, values from 0 through
|
|
196
|
+
126 can be decoded as Celsius before the selected temperature conversion.
|
|
197
|
+
Values from 127 through 255 remain undecoded. This narrow interoperability
|
|
198
|
+
recovery preserves the original unsigned byte in `unmapped_messages` when
|
|
199
|
+
enabled, including values that also receive a decoded temperature.
|
|
200
|
+
|
|
201
|
+
### Battery samples
|
|
202
|
+
|
|
203
|
+
`batteries` retains every battery message in file order, including its
|
|
204
|
+
timestamp and capacity percentage when present. These samples are also
|
|
205
|
+
available in `messages.battery`. Undocumented battery fields remain available
|
|
206
|
+
through `includeUnmappedMessages`; the parser does not infer their device
|
|
207
|
+
association or calculate battery consumption.
|
|
208
|
+
|
|
172
209
|
### Lossless selected messages
|
|
173
210
|
|
|
174
211
|
Set `includeRawMessages` when a consumer needs native FIT numeric codes rather
|
|
@@ -240,8 +277,9 @@ unchanged.
|
|
|
240
277
|
## Profile-backed output
|
|
241
278
|
|
|
242
279
|
Recognized message names, field names, enum values, wire types, scales,
|
|
243
|
-
offsets, arrays, and units come from the community-maintained
|
|
244
|
-
`src/profile.ts`. Its maintenance and
|
|
280
|
+
offsets, arrays, and units come from the community-maintained profile in
|
|
281
|
+
`src/profile.ts` and `src/profile-lookup-data.ts`. Its maintenance and
|
|
282
|
+
verification contract is documented in
|
|
245
283
|
[`PROFILE.md`](./PROFILE.md).
|
|
246
284
|
|
|
247
285
|
Public names use `snake_case` while preserving established alphanumeric tokens
|
|
@@ -443,9 +481,10 @@ a reference package, compares their complete default outputs, and reports only
|
|
|
443
481
|
aggregate counts. Files rejected by both strict parsers are retried in force
|
|
444
482
|
mode.
|
|
445
483
|
|
|
446
|
-
Edit `src/profile.ts` only through reviewed
|
|
447
|
-
regression coverage and a documented source. Do
|
|
448
|
-
manually; run `npm run codegen` after changing the
|
|
484
|
+
Edit `src/profile.ts` and `src/profile-lookup-data.ts` only through reviewed
|
|
485
|
+
profile changes with focused regression coverage and a documented source. Do
|
|
486
|
+
not edit `src/fit_types.ts` manually; run `npm run codegen` after changing the
|
|
487
|
+
maintained profile.
|
|
449
488
|
|
|
450
489
|
Repository-specific automation guidance is tracked in
|
|
451
490
|
[`.agent/README.md`](./.agent/README.md). More examples are available in the
|
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':
|
|
@@ -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,
|
|
@@ -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);
|
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':
|
|
@@ -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,
|
|
@@ -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);
|
package/dist/cjs/fit-parser.d.ts
CHANGED
|
@@ -28,5 +28,13 @@ export default class FitParser {
|
|
|
28
28
|
private readonly options;
|
|
29
29
|
constructor(options?: FitParserOptions);
|
|
30
30
|
parseAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit>;
|
|
31
|
+
/**
|
|
32
|
+
* Parses every complete FIT file in an input, preserving file order and
|
|
33
|
+
* independent decoder state. Rejects incomplete files and trailing bytes;
|
|
34
|
+
* each file uses this parser's options, including its CRC policy.
|
|
35
|
+
* Rejects with an Error when any file cannot be parsed.
|
|
36
|
+
*/
|
|
37
|
+
parseFilesAsync(content: ArrayBuffer | Buffer<ArrayBuffer>): Promise<ParsedFit[]>;
|
|
31
38
|
parse(content: ArrayBuffer | Buffer<ArrayBuffer>, callback: FitParserCallback): void;
|
|
39
|
+
private parseFile;
|
|
32
40
|
}
|
package/dist/cjs/fit-parser.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
2
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
12
|
exports.readFitUnsignedField = exports.readFitStringField = exports.readFitMessages = exports.getFitBaseTypeId = exports.fitTimestampToUnixMilliseconds = exports.FitMessageReaderError = exports.getFitSubSportName = exports.getFitSubSportId = exports.getFitSportName = exports.getFitSportId = exports.getFitManufacturerName = exports.getFitGarminProductName = exports.getFitGarminProductDisplayName = exports.getFitCoursePointId = exports.FitEncoder = exports.FitBaseType = void 0;
|
|
4
13
|
const binary_js_1 = require("./binary.js");
|
|
@@ -51,7 +60,71 @@ class FitParser {
|
|
|
51
60
|
});
|
|
52
61
|
});
|
|
53
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Parses every complete FIT file in an input, preserving file order and
|
|
65
|
+
* independent decoder state. Rejects incomplete files and trailing bytes;
|
|
66
|
+
* each file uses this parser's options, including its CRC policy.
|
|
67
|
+
* Rejects with an Error when any file cannot be parsed.
|
|
68
|
+
*/
|
|
69
|
+
parseFilesAsync(content) {
|
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
const blob = content instanceof ArrayBuffer
|
|
72
|
+
? new Uint8Array(content)
|
|
73
|
+
: new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
|
|
74
|
+
const results = [];
|
|
75
|
+
let start = 0;
|
|
76
|
+
do {
|
|
77
|
+
const remaining = blob.length - start;
|
|
78
|
+
if (remaining < 12) {
|
|
79
|
+
throw new Error('File to small to be a FIT file');
|
|
80
|
+
}
|
|
81
|
+
const headerLength = blob[start];
|
|
82
|
+
if (headerLength !== 12 && headerLength !== 14) {
|
|
83
|
+
throw new Error('Incorrect header size');
|
|
84
|
+
}
|
|
85
|
+
if (remaining < headerLength) {
|
|
86
|
+
throw new Error('File to small to be a FIT file');
|
|
87
|
+
}
|
|
88
|
+
if (blob[start + 8] !== 0x2E
|
|
89
|
+
|| blob[start + 9] !== 0x46
|
|
90
|
+
|| blob[start + 10] !== 0x49
|
|
91
|
+
|| blob[start + 11] !== 0x54) {
|
|
92
|
+
throw new Error('Missing \'.FIT\' in header');
|
|
93
|
+
}
|
|
94
|
+
const dataLength = new DataView(blob.buffer, blob.byteOffset + start, remaining)
|
|
95
|
+
.getUint32(4, true);
|
|
96
|
+
const crcStart = start + headerLength + dataLength;
|
|
97
|
+
if (crcStart > blob.length) {
|
|
98
|
+
throw new Error('File data exceeds input length');
|
|
99
|
+
}
|
|
100
|
+
if (crcStart + 2 > blob.length) {
|
|
101
|
+
throw new Error('File CRC missing');
|
|
102
|
+
}
|
|
103
|
+
const end = crcStart + 2;
|
|
104
|
+
try {
|
|
105
|
+
results.push(yield new Promise((resolve, reject) => {
|
|
106
|
+
this.parseFile(blob.slice(start, end).buffer, (error, data) => {
|
|
107
|
+
if (error) {
|
|
108
|
+
reject(error);
|
|
109
|
+
}
|
|
110
|
+
else if (data) {
|
|
111
|
+
resolve(data);
|
|
112
|
+
}
|
|
113
|
+
}, true);
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
118
|
+
}
|
|
119
|
+
start = end;
|
|
120
|
+
} while (start < blob.length);
|
|
121
|
+
return results;
|
|
122
|
+
});
|
|
123
|
+
}
|
|
54
124
|
parse(content, callback) {
|
|
125
|
+
this.parseFile(content, callback);
|
|
126
|
+
}
|
|
127
|
+
parseFile(content, callback, validateRecordBounds = false) {
|
|
55
128
|
var _a, _b, _c;
|
|
56
129
|
const blob = content instanceof ArrayBuffer
|
|
57
130
|
? new Uint8Array(content)
|
|
@@ -111,6 +184,7 @@ class FitParser {
|
|
|
111
184
|
protocolVersion,
|
|
112
185
|
};
|
|
113
186
|
const messages = {};
|
|
187
|
+
const batteries = [];
|
|
114
188
|
let sessions = [];
|
|
115
189
|
let laps = [];
|
|
116
190
|
const records = [];
|
|
@@ -160,6 +234,12 @@ class FitParser {
|
|
|
160
234
|
let pausedTime = 0;
|
|
161
235
|
while (loopIndex < crcStart) {
|
|
162
236
|
const { globalMessageNumber, littleEndian, message, messageType, nextIndex, compressedTimestamp, rawFields: recordRawFields, rawDeveloperFields: recordRawDeveloperFields, unmappedFields: recordUnmappedFields, unmappedDeveloperFields: recordUnmappedDeveloperFields, } = (0, binary_js_1.readRecord)(blob, messageTypes, developerFields, loopIndex, this.options, startDate, pausedTime, dataView, decoderState, crcStart);
|
|
237
|
+
// Complete-file parsing also validates bounds without requiring raw
|
|
238
|
+
// retention. The single-file API keeps its existing recovery behavior.
|
|
239
|
+
if (validateRecordBounds
|
|
240
|
+
&& (!Number.isInteger(nextIndex) || nextIndex <= loopIndex || nextIndex > crcStart)) {
|
|
241
|
+
throw new Error('Invalid FIT record bounds');
|
|
242
|
+
}
|
|
163
243
|
loopIndex = nextIndex;
|
|
164
244
|
if (globalMessageNumber !== undefined) {
|
|
165
245
|
const messageIndex = (_a = messageCountsByGlobalNumber.get(globalMessageNumber)) !== null && _a !== void 0 ? _a : 0;
|
|
@@ -210,6 +290,9 @@ class FitParser {
|
|
|
210
290
|
((_b = messages[messageType]) !== null && _b !== void 0 ? _b : (messages[messageType] = [])).push(message);
|
|
211
291
|
}
|
|
212
292
|
switch (messageType) {
|
|
293
|
+
case 'battery':
|
|
294
|
+
batteries.push(message);
|
|
295
|
+
break;
|
|
213
296
|
case 'lap':
|
|
214
297
|
laps.push(message);
|
|
215
298
|
break;
|
|
@@ -354,6 +437,7 @@ class FitParser {
|
|
|
354
437
|
fitObj.time_in_zone = time_in_zone;
|
|
355
438
|
fitObj.activity_metrics = activity_metrics;
|
|
356
439
|
fitObj.user_metrics = user_metrics;
|
|
440
|
+
fitObj.batteries = batteries;
|
|
357
441
|
fitObj.messages = messages;
|
|
358
442
|
if (rawDeveloperFields) {
|
|
359
443
|
fitObj.raw_developer_fields = rawDeveloperFields;
|