bids-validator-deno 2.0.11__tar.gz → 2.1.1__tar.gz
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.
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/PKG-INFO +1 -1
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/deno.json +18 -15
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/pyproject.toml +1 -1
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/bids-validator.ts +1 -0
- bids_validator_deno-2.1.1/src/files/access.test.ts +40 -0
- bids_validator_deno-2.1.1/src/files/access.ts +35 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/browser.ts +2 -2
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/deno.ts +1 -1
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/gzip.test.ts +3 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/gzip.ts +2 -1
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/inheritance.test.ts +18 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/inheritance.ts +1 -1
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/json.test.ts +13 -5
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/json.ts +6 -2
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/nifti.test.ts +28 -2
- bids_validator_deno-2.1.1/src/files/nifti.ts +157 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/streams.ts +15 -5
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/tiff.test.ts +3 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/tiff.ts +2 -1
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/tsv.test.ts +131 -2
- bids_validator_deno-2.1.1/src/files/tsv.ts +103 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/issues/list.ts +16 -7
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/applyRules.test.ts +1 -46
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/applyRules.ts +39 -29
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/associations.ts +4 -2
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/context.test.ts +7 -1
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/context.ts +75 -47
- bids_validator_deno-2.1.1/src/schema/datatypes.test.ts +57 -0
- bids_validator_deno-2.1.1/src/schema/datatypes.ts +30 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/expressionLanguage.test.ts +81 -2
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/expressionLanguage.ts +53 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/tables.test.ts +86 -33
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/tables.ts +94 -101
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/walk.ts +2 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/loadSchema.ts +5 -5
- bids_validator_deno-2.1.1/src/tests/local/nifti_rules.test.ts +111 -0
- bids_validator_deno-2.1.1/src/tests/utils.ts +14 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/filetree.ts +1 -1
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/bids.ts +3 -0
- bids_validator_deno-2.1.1/src/validators/filenameCase.test.ts +32 -0
- bids_validator_deno-2.1.1/src/validators/filenameCase.ts +16 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/filenameIdentify.test.ts +3 -21
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/filenameIdentify.ts +0 -25
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/internal/unusedFile.ts +9 -5
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/validateFiles.test.ts +15 -13
- bids_validator_deno-2.0.11/src/files/nifti.ts +0 -72
- bids_validator_deno-2.0.11/src/files/tsv.ts +0 -68
- bids_validator_deno-2.0.11/src/schema/modalities.ts +0 -16
- bids_validator_deno-2.0.11/src/tests/utils.ts +0 -12
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/LICENSE +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/README.md +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/pdm_build.py +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/.git-meta.json +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/browser.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/deno.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/dwi.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/dwi.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/filetree.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/filetree.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/ignore.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/ignore.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/streams.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/issues/datasetIssues.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/issues/datasetIssues.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/issues/list.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/main.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/entities.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/entities.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/fixtures.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/walk.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/loadSchema.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/options.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/options.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/requestPermissions.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/summary/collectSubjectMetadata.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/summary/summary.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/summary/summary.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/README.md +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/bom-utf16.tsv +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/bom-utf8.json +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/generate-filenames.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/bids_examples.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/common.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/derivatives.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/empty_files.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/hed-integration.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/valid_dataset.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/valid_filenames.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/valid_headers.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/nullReadBytes.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/regression.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/schema-expression-language.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/simple-dataset.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/check.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/columns.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/columns.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/issues.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/schema.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/validation-result.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/errors.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/logger.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/logger.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/memoize.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/objectPathHandler.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/output.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/bids.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/citation.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/citation.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/filenameValidate.test.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/filenameValidate.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/hed.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/internal/emptyFile.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/json.ts +0 -0
- {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/version.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bids/validator",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": "./src/bids-validator.ts",
|
|
6
6
|
"./main": "./src/main.ts",
|
|
@@ -27,22 +27,22 @@
|
|
|
27
27
|
]
|
|
28
28
|
},
|
|
29
29
|
"imports": {
|
|
30
|
-
"@ajv": "npm:ajv
|
|
31
|
-
"@bids/schema": "jsr:@bids/schema@~1.0
|
|
30
|
+
"@ajv": "npm:ajv@^8.17.1",
|
|
31
|
+
"@bids/schema": "jsr:@bids/schema@~1.1.0",
|
|
32
32
|
"@cliffy/command": "jsr:@effigies/cliffy-command@1.0.0-dev.8",
|
|
33
33
|
"@cliffy/table": "jsr:@effigies/cliffy-table@1.0.0-dev.5",
|
|
34
|
-
"@hed/validator": "npm:hed-validator
|
|
35
|
-
"@ignore": "npm:ignore
|
|
36
|
-
"@libs/xml": "jsr:@libs/xml
|
|
37
|
-
"@mango/nifti": "npm:@bids/nifti-reader-js
|
|
38
|
-
"@std/assert": "jsr:@std/assert
|
|
39
|
-
"@std/fmt": "jsr:@std/fmt
|
|
40
|
-
"@std/fs": "jsr:@std/fs
|
|
41
|
-
"@std/io": "jsr:@std/io
|
|
42
|
-
"@std/log": "jsr:@std/log
|
|
43
|
-
"@std/path": "jsr:@std/path
|
|
44
|
-
"@std/streams": "jsr:@std/streams
|
|
45
|
-
"@std/yaml": "jsr:@std/yaml@^1.0.
|
|
34
|
+
"@hed/validator": "npm:hed-validator@~4.0.1",
|
|
35
|
+
"@ignore": "npm:ignore@^7.0.5",
|
|
36
|
+
"@libs/xml": "jsr:@libs/xml@^6.0.8",
|
|
37
|
+
"@mango/nifti": "npm:@bids/nifti-reader-js@^0.6.9",
|
|
38
|
+
"@std/assert": "jsr:@std/assert@^1.0.14",
|
|
39
|
+
"@std/fmt": "jsr:@std/fmt@^1.0.8",
|
|
40
|
+
"@std/fs": "jsr:@std/fs@^1.0.19",
|
|
41
|
+
"@std/io": "jsr:@std/io@^0.225.2",
|
|
42
|
+
"@std/log": "jsr:@std/log@^0.224.14",
|
|
43
|
+
"@std/path": "jsr:@std/path@^1.1.2",
|
|
44
|
+
"@std/streams": "jsr:@std/streams@^1.0.12",
|
|
45
|
+
"@std/yaml": "jsr:@std/yaml@^1.0.9"
|
|
46
46
|
},
|
|
47
47
|
"tasks": {
|
|
48
48
|
"test": "deno test -A src/"
|
|
@@ -54,6 +54,9 @@
|
|
|
54
54
|
"proseWrap": "preserve",
|
|
55
55
|
"include": [
|
|
56
56
|
"src/"
|
|
57
|
+
],
|
|
58
|
+
"exclude": [
|
|
59
|
+
"src/tests/bom-utf8.json"
|
|
57
60
|
]
|
|
58
61
|
}
|
|
59
62
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { assert, assertArrayIncludes, assertObjectMatch } from '@std/assert'
|
|
2
|
+
import { basename, dirname } from '@std/path'
|
|
3
|
+
import { BIDSFileDeno } from './deno.ts'
|
|
4
|
+
|
|
5
|
+
export function testAsyncFileAccess(
|
|
6
|
+
name: string,
|
|
7
|
+
fn: (file: BIDSFileDeno, ...args: any[]) => Promise<any>,
|
|
8
|
+
...args: any[]
|
|
9
|
+
) {
|
|
10
|
+
Deno.test({
|
|
11
|
+
name,
|
|
12
|
+
ignore: Deno.build.os === 'windows',
|
|
13
|
+
async fn(t) {
|
|
14
|
+
await t.step('Dangling symlink', async () => {
|
|
15
|
+
const file = new BIDSFileDeno('tests/data', '/broken-symlink')
|
|
16
|
+
try {
|
|
17
|
+
await fn(file, ...args)
|
|
18
|
+
assert(false, 'Expected error')
|
|
19
|
+
} catch (e: any) {
|
|
20
|
+
assertObjectMatch(e, {
|
|
21
|
+
code: 'FILE_READ',
|
|
22
|
+
location: '/broken-symlink',
|
|
23
|
+
})
|
|
24
|
+
assertArrayIncludes(['NotFound', 'FilesystemLoop'], [e.subCode])
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
await t.step('Insufficient permissions', async () => {
|
|
28
|
+
const tmpfile = await Deno.makeTempFile()
|
|
29
|
+
await Deno.chmod(tmpfile, 0o000)
|
|
30
|
+
const file = new BIDSFileDeno('', tmpfile)
|
|
31
|
+
try {
|
|
32
|
+
await fn(file, ...args)
|
|
33
|
+
assert(false, 'Expected error')
|
|
34
|
+
} catch (e: any) {
|
|
35
|
+
assertObjectMatch(e, { code: 'FILE_READ', subCode: 'PermissionDenied' })
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type BIDSFile } from '../types/filetree.ts'
|
|
2
|
+
import { type Issue } from '../types/issues.ts'
|
|
3
|
+
|
|
4
|
+
function IOErrorToIssue(err: { code: string; name: string }): Issue {
|
|
5
|
+
const subcode = err.name
|
|
6
|
+
let issueMessage: string | undefined = undefined
|
|
7
|
+
if (err.code === 'ENOENT' || err.code === 'ELOOP') {
|
|
8
|
+
issueMessage = 'Possible dangling symbolic link'
|
|
9
|
+
}
|
|
10
|
+
return { code: 'FILE_READ', subCode: err.name, issueMessage }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function openStream(file: BIDSFile): ReadableStream<Uint8Array<ArrayBuffer>> {
|
|
14
|
+
try {
|
|
15
|
+
return file.stream
|
|
16
|
+
} catch (err: any) {
|
|
17
|
+
throw { location: file.path, ...IOErrorToIssue(err) }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function readBytes(
|
|
22
|
+
file: BIDSFile,
|
|
23
|
+
size: number,
|
|
24
|
+
offset = 0,
|
|
25
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
26
|
+
return file.readBytes(size, offset).catch((err: any) => {
|
|
27
|
+
throw { location: file.path, ...IOErrorToIssue(err) }
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function readText(file: BIDSFile): Promise<string> {
|
|
32
|
+
return file.text().catch((err: any) => {
|
|
33
|
+
throw { location: file.path, ...IOErrorToIssue(err) }
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -29,8 +29,8 @@ export class BIDSFileBrowser implements BIDSFile {
|
|
|
29
29
|
return this.#file.size
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
get stream(): ReadableStream<Uint8Array
|
|
33
|
-
return this.#file.stream()
|
|
32
|
+
get stream(): ReadableStream<Uint8Array<ArrayBuffer>> {
|
|
33
|
+
return this.#file.stream() as ReadableStream<Uint8Array<ArrayBuffer>>
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
get ignored(): boolean {
|
|
@@ -45,7 +45,7 @@ export class BIDSFileDeno implements BIDSFile {
|
|
|
45
45
|
return this.#fileInfo ? this.#fileInfo.size : -1
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
get stream(): ReadableStream<Uint8Array
|
|
48
|
+
get stream(): ReadableStream<Uint8Array<ArrayBuffer>> {
|
|
49
49
|
const handle = this.#openHandle()
|
|
50
50
|
return handle.readable
|
|
51
51
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assert, assertObjectMatch } from '@std/assert'
|
|
2
2
|
import { parseGzip } from './gzip.ts'
|
|
3
3
|
import { BIDSFileDeno } from './deno.ts'
|
|
4
|
+
import { testAsyncFileAccess } from './access.test.ts'
|
|
4
5
|
|
|
5
6
|
Deno.test('parseGzip', async (t) => {
|
|
6
7
|
await t.step('parses anonymized file', async () => {
|
|
@@ -40,3 +41,5 @@ Deno.test('parseGzip', async (t) => {
|
|
|
40
41
|
assert(!gzip)
|
|
41
42
|
})
|
|
42
43
|
})
|
|
44
|
+
|
|
45
|
+
testAsyncFileAccess('Test file access errors for parseGzip', parseGzip)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Gzip } from '@bids/schema/context'
|
|
6
6
|
import type { BIDSFile } from '../types/filetree.ts'
|
|
7
|
+
import { readBytes } from './access.ts'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Parse a gzip header from a file
|
|
@@ -19,7 +20,7 @@ export async function parseGzip(
|
|
|
19
20
|
file: BIDSFile,
|
|
20
21
|
maxBytes: number = 512,
|
|
21
22
|
): Promise<Gzip | undefined> {
|
|
22
|
-
const buf = await
|
|
23
|
+
const buf = await readBytes(file, maxBytes)
|
|
23
24
|
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
24
25
|
if (view.byteLength < 2 || view.getUint16(0, false) !== 0x1f8b) return undefined
|
|
25
26
|
|
|
@@ -72,4 +72,22 @@ Deno.test('walkback inheritance tests', async (t) => {
|
|
|
72
72
|
assertEquals(rootElectrodes.path, '/space-talairach_electrodes.tsv')
|
|
73
73
|
},
|
|
74
74
|
)
|
|
75
|
+
await t.step(
|
|
76
|
+
'The presence of target entities does not trigger exact match logic',
|
|
77
|
+
async () => {
|
|
78
|
+
const rootFileTree = pathsToTree([
|
|
79
|
+
'/sub-01/ieeg/sub-01_task-rest_ieeg.edf',
|
|
80
|
+
'/sub-01/ieeg/sub-01_task-rest_space-anat_electrodes.tsv',
|
|
81
|
+
'/sub-01/ieeg/sub-01_task-rest_space-MNI_electrodes.tsv',
|
|
82
|
+
])
|
|
83
|
+
const dataFile = rootFileTree.get('sub-01/ieeg/sub-01_task-rest_ieeg.edf') as BIDSFile
|
|
84
|
+
const electrodes = walkBack(dataFile, true, ['.tsv'], 'electrodes', ['space'])
|
|
85
|
+
const localElectrodes: BIDSFile[] = electrodes.next().value
|
|
86
|
+
assert(Array.isArray(localElectrodes))
|
|
87
|
+
assertEquals(localElectrodes.map((f) => f.path), [
|
|
88
|
+
'/sub-01/ieeg/sub-01_task-rest_space-anat_electrodes.tsv',
|
|
89
|
+
'/sub-01/ieeg/sub-01_task-rest_space-MNI_electrodes.tsv',
|
|
90
|
+
])
|
|
91
|
+
},
|
|
92
|
+
)
|
|
75
93
|
})
|
|
@@ -50,7 +50,7 @@ export function* walkBack<T extends string[]>(
|
|
|
50
50
|
if (candidates.length > 1) {
|
|
51
51
|
const exactMatch = candidates.find((file) => {
|
|
52
52
|
const { entities } = readEntities(file.name)
|
|
53
|
-
return Object.keys(sourceParts.entities).every((entity) =>
|
|
53
|
+
return [...Object.keys(sourceParts.entities), ...(targetEntities ?? [])].every((entity) =>
|
|
54
54
|
entities[entity] === sourceParts.entities[entity]
|
|
55
55
|
)
|
|
56
56
|
})
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type assert, assertObjectMatch } from '@std/assert'
|
|
2
2
|
import type { BIDSFile } from '../types/filetree.ts'
|
|
3
3
|
import type { FileIgnoreRules } from './ignore.ts'
|
|
4
|
+
import { testAsyncFileAccess } from './access.test.ts'
|
|
4
5
|
|
|
6
|
+
import { pathsToTree } from '../files/filetree.ts'
|
|
5
7
|
import { loadJSON } from './json.ts'
|
|
6
8
|
|
|
7
9
|
function encodeUTF16(text: string) {
|
|
@@ -17,9 +19,12 @@ function encodeUTF16(text: string) {
|
|
|
17
19
|
return buffer
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
function makeFile(text: string, encoding: string): BIDSFile {
|
|
22
|
+
function makeFile(path: string, text: string, encoding: string): BIDSFile {
|
|
21
23
|
const bytes = encoding === 'utf-8' ? new TextEncoder().encode(text) : encodeUTF16(text)
|
|
24
|
+
const file = pathsToTree([path]).get(path) as BIDSFile
|
|
22
25
|
return {
|
|
26
|
+
path: file.path,
|
|
27
|
+
parent: file.parent,
|
|
23
28
|
readBytes: async (size: number) => {
|
|
24
29
|
return new Uint8Array(bytes)
|
|
25
30
|
},
|
|
@@ -29,13 +34,13 @@ function makeFile(text: string, encoding: string): BIDSFile {
|
|
|
29
34
|
|
|
30
35
|
Deno.test('Test JSON error conditions', async (t) => {
|
|
31
36
|
await t.step('Load valid JSON', async () => {
|
|
32
|
-
const JSONfile = makeFile('{"a": 1}', 'utf-8')
|
|
37
|
+
const JSONfile = makeFile('/valid-contents.json', '{"a": 1}', 'utf-8')
|
|
33
38
|
const result = await loadJSON(JSONfile)
|
|
34
39
|
assertObjectMatch(result, { a: 1 })
|
|
35
40
|
})
|
|
36
41
|
|
|
37
42
|
await t.step('Error on BOM', async () => {
|
|
38
|
-
const BOMfile = makeFile('\uFEFF{"a": 1}', 'utf-8')
|
|
43
|
+
const BOMfile = makeFile('/BOM.json', '\uFEFF{"a": 1}', 'utf-8')
|
|
39
44
|
let error: any = undefined
|
|
40
45
|
await loadJSON(BOMfile).catch((e) => {
|
|
41
46
|
error = e
|
|
@@ -44,7 +49,7 @@ Deno.test('Test JSON error conditions', async (t) => {
|
|
|
44
49
|
})
|
|
45
50
|
|
|
46
51
|
await t.step('Error on UTF-16', async () => {
|
|
47
|
-
const UTF16file = makeFile('{"a": 1}', 'utf-16')
|
|
52
|
+
const UTF16file = makeFile('/utf16.json', '{"a": 1}', 'utf-16')
|
|
48
53
|
let error: any = undefined
|
|
49
54
|
await loadJSON(UTF16file).catch((e) => {
|
|
50
55
|
error = e
|
|
@@ -53,11 +58,14 @@ Deno.test('Test JSON error conditions', async (t) => {
|
|
|
53
58
|
})
|
|
54
59
|
|
|
55
60
|
await t.step('Error on invalid JSON syntax', async () => {
|
|
56
|
-
const badJSON = makeFile('{"a": 1]', 'utf-8')
|
|
61
|
+
const badJSON = makeFile('/bad-syntax.json', '{"a": 1]', 'utf-8')
|
|
57
62
|
let error: any = undefined
|
|
58
63
|
await loadJSON(badJSON).catch((e) => {
|
|
59
64
|
error = e
|
|
60
65
|
})
|
|
61
66
|
assertObjectMatch(error, { code: 'JSON_INVALID' })
|
|
62
67
|
})
|
|
68
|
+
loadJSON.cache.clear()
|
|
63
69
|
})
|
|
70
|
+
|
|
71
|
+
testAsyncFileAccess('Test file access errors for loadJSON', loadJSON)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { filememoizeAsync } from '../utils/memoize.ts'
|
|
1
2
|
import type { BIDSFile } from '../types/filetree.ts'
|
|
3
|
+
import { readBytes } from './access.ts'
|
|
2
4
|
|
|
3
5
|
async function readJSONText(file: BIDSFile): Promise<string> {
|
|
4
6
|
// Read JSON text from a file
|
|
@@ -6,7 +8,7 @@ async function readJSONText(file: BIDSFile): Promise<string> {
|
|
|
6
8
|
const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true })
|
|
7
9
|
// Streaming TextDecoders are buggy in Deno and Chrome, so read the
|
|
8
10
|
// entire file into memory before decoding and parsing
|
|
9
|
-
const data = await
|
|
11
|
+
const data = await readBytes(file, file.size)
|
|
10
12
|
try {
|
|
11
13
|
const text = decoder.decode(data)
|
|
12
14
|
if (text.startsWith('\uFEFF')) {
|
|
@@ -20,7 +22,7 @@ async function readJSONText(file: BIDSFile): Promise<string> {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
async function _loadJSON(file: BIDSFile): Promise<Record<string, unknown>> {
|
|
24
26
|
const text = await readJSONText(file) // Raise encoding errors
|
|
25
27
|
let parsedText
|
|
26
28
|
try {
|
|
@@ -36,3 +38,5 @@ export async function loadJSON(file: BIDSFile): Promise<Record<string, unknown>>
|
|
|
36
38
|
}
|
|
37
39
|
return parsedText
|
|
38
40
|
}
|
|
41
|
+
|
|
42
|
+
export const loadJSON = filememoizeAsync(_loadJSON)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { assert, assertObjectMatch } from '@std/assert'
|
|
1
|
+
import { assert, assertEquals, assertObjectMatch } from '@std/assert'
|
|
2
2
|
import { FileIgnoreRules } from './ignore.ts'
|
|
3
3
|
import { BIDSFileDeno } from './deno.ts'
|
|
4
|
+
import { testAsyncFileAccess } from './access.test.ts'
|
|
4
5
|
|
|
5
|
-
import { loadHeader } from './nifti.ts'
|
|
6
|
+
import { axisCodes, loadHeader } from './nifti.ts'
|
|
6
7
|
|
|
7
8
|
Deno.test('Test loading nifti header', async (t) => {
|
|
8
9
|
const ignore = new FileIgnoreRules([])
|
|
@@ -73,3 +74,28 @@ Deno.test('Test loading nifti header', async (t) => {
|
|
|
73
74
|
})
|
|
74
75
|
})
|
|
75
76
|
})
|
|
77
|
+
|
|
78
|
+
Deno.test('Test extracting axis codes', async (t) => {
|
|
79
|
+
await t.step('Identify RAS', async () => {
|
|
80
|
+
const affine = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
|
|
81
|
+
assertEquals(axisCodes(affine), ['R', 'A', 'S'])
|
|
82
|
+
})
|
|
83
|
+
await t.step('Identify LPS (flips)', async () => {
|
|
84
|
+
const affine = [[-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
|
|
85
|
+
assertEquals(axisCodes(affine), ['L', 'P', 'S'])
|
|
86
|
+
})
|
|
87
|
+
await t.step('Identify SPL (flips + swap)', async () => {
|
|
88
|
+
const affine = [[0, 0, -1, 0], [0, -1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1]]
|
|
89
|
+
assertEquals(axisCodes(affine), ['S', 'P', 'L'])
|
|
90
|
+
})
|
|
91
|
+
await t.step('Identify SLP (flips + rotate)', async () => {
|
|
92
|
+
const affine = [[0, -1, 0, 0], [0, 0, -1, 0], [1, 0, 0, 0], [0, 0, 0, 1]]
|
|
93
|
+
assertEquals(axisCodes(affine), ['S', 'L', 'P'])
|
|
94
|
+
})
|
|
95
|
+
await t.step('Identify ASR (rotate)', async () => {
|
|
96
|
+
const affine = [[0, 0, 1, 0], [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1]]
|
|
97
|
+
assertEquals(axisCodes(affine), ['A', 'S', 'R'])
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
testAsyncFileAccess('Test file access errors for loadHeader', loadHeader)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { isCompressed, isNIFTI1, isNIFTI2, NIFTI1, NIFTI2 } from '@mango/nifti'
|
|
2
|
+
import type { BIDSFile } from '../types/filetree.ts'
|
|
3
|
+
import { logger } from '../utils/logger.ts'
|
|
4
|
+
import type { NiftiHeader } from '@bids/schema/context'
|
|
5
|
+
import { readBytes } from './access.ts'
|
|
6
|
+
|
|
7
|
+
async function extract(buffer: Uint8Array, nbytes: number): Promise<Uint8Array<ArrayBuffer>> {
|
|
8
|
+
// The fflate decompression that is used in nifti-reader does not like
|
|
9
|
+
// truncated data, so pretend that we have a stream and stop reading
|
|
10
|
+
// when we have enough bytes.
|
|
11
|
+
const result = new Uint8Array(nbytes)
|
|
12
|
+
const stream = new ReadableStream({
|
|
13
|
+
start(controller) {
|
|
14
|
+
controller.enqueue(buffer)
|
|
15
|
+
controller.close()
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
const reader = stream.pipeThrough(new DecompressionStream('gzip')).getReader()
|
|
19
|
+
let offset = 0
|
|
20
|
+
try {
|
|
21
|
+
while (offset < nbytes) {
|
|
22
|
+
const { value, done } = await reader.read()
|
|
23
|
+
if (done || !value) {
|
|
24
|
+
break
|
|
25
|
+
}
|
|
26
|
+
result.set(value.subarray(0, Math.min(value.length, nbytes - offset)), offset)
|
|
27
|
+
offset += value.length
|
|
28
|
+
}
|
|
29
|
+
} finally {
|
|
30
|
+
await reader.cancel()
|
|
31
|
+
}
|
|
32
|
+
return result.subarray(0, offset)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
|
|
36
|
+
const buf = await readBytes(file, 1024)
|
|
37
|
+
try {
|
|
38
|
+
const data = isCompressed(buf.buffer) ? await extract(buf, 540) : buf.slice(0, 540)
|
|
39
|
+
let header
|
|
40
|
+
if (isNIFTI1(data.buffer)) {
|
|
41
|
+
header = new NIFTI1()
|
|
42
|
+
// Truncate to 348 bytes to avoid attempting to parse extensions
|
|
43
|
+
header.readHeader(data.buffer.slice(0, 348))
|
|
44
|
+
} else if (isNIFTI2(data.buffer)) {
|
|
45
|
+
header = new NIFTI2()
|
|
46
|
+
header.readHeader(data.buffer)
|
|
47
|
+
}
|
|
48
|
+
if (!header) {
|
|
49
|
+
throw { code: 'NIFTI_HEADER_UNREADABLE' }
|
|
50
|
+
}
|
|
51
|
+
const ndim = header.dims[0]
|
|
52
|
+
return {
|
|
53
|
+
dim: header.dims,
|
|
54
|
+
// Hack: round pixdim to 3 decimal places; schema should add rounding function
|
|
55
|
+
pixdim: header.pixDims.map((pixdim) => Math.round(pixdim * 1000) / 1000),
|
|
56
|
+
shape: header.dims.slice(1, ndim + 1),
|
|
57
|
+
voxel_sizes: header.pixDims.slice(1, ndim + 1),
|
|
58
|
+
dim_info: {
|
|
59
|
+
freq: header.dim_info & 0x03,
|
|
60
|
+
phase: (header.dim_info >> 2) & 0x03,
|
|
61
|
+
slice: (header.dim_info >> 4) & 0x03,
|
|
62
|
+
},
|
|
63
|
+
xyzt_units: {
|
|
64
|
+
xyz: ['unknown', 'meter', 'mm', 'um'][header.xyzt_units & 0x03],
|
|
65
|
+
t: ['unknown', 'sec', 'msec', 'usec'][(header.xyzt_units >> 3) & 0x03],
|
|
66
|
+
},
|
|
67
|
+
qform_code: header.qform_code,
|
|
68
|
+
sform_code: header.sform_code,
|
|
69
|
+
axis_codes: axisCodes(header.affine),
|
|
70
|
+
} as NiftiHeader
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw { code: 'NIFTI_HEADER_UNREADABLE' }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Vector addition */
|
|
77
|
+
function add(a: number[], b: number[]): number[] {
|
|
78
|
+
return a.map((x, i) => x + b[i])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Vector subtraction */
|
|
82
|
+
function sub(a: number[], b: number[]): number[] {
|
|
83
|
+
return a.map((x, i) => x - b[i])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Scalar multiplication */
|
|
87
|
+
function scale(vec: number[], scalar: number): number[] {
|
|
88
|
+
return vec.map((x) => x * scalar)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Dot product */
|
|
92
|
+
function dot(a: number[], b: number[]): number {
|
|
93
|
+
return a.map((x, i) => x * b[i]).reduce((acc, x) => acc + x, 0)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function argMax(arr: number[]): number {
|
|
97
|
+
return arr.reduce((acc, x, i) => (x > arr[acc] ? i : acc), 0)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Identify the nearest principle axes of an image affine.
|
|
102
|
+
*
|
|
103
|
+
* Affines transform indices in a data array into mm right, anterior and superior of
|
|
104
|
+
* an origin in "world coordinates". If moving along an axis in the positive direction
|
|
105
|
+
* predominantly moves right, that axis is labeled "R".
|
|
106
|
+
*
|
|
107
|
+
* @example The identity matrix is in "RAS" orientation:
|
|
108
|
+
*
|
|
109
|
+
* # Usage
|
|
110
|
+
*
|
|
111
|
+
* ```ts
|
|
112
|
+
* const affine = [[1, 0, 0, 0],
|
|
113
|
+
* [0, 1, 0, 0],
|
|
114
|
+
* [0, 0, 1, 0],
|
|
115
|
+
* [0, 0, 0, 1]]
|
|
116
|
+
*
|
|
117
|
+
* axisCodes(affine)
|
|
118
|
+
* ```
|
|
119
|
+
*
|
|
120
|
+
* # Result
|
|
121
|
+
* ```ts
|
|
122
|
+
* ['R', 'A', 'S']
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @returns character codes describing the orientation of an image affine.
|
|
126
|
+
*/
|
|
127
|
+
export function axisCodes(affine: number[][]): string[] {
|
|
128
|
+
// This function is an extract of the Python function transforms3d.affines.decompose44
|
|
129
|
+
// (https://github.com/matthew-brett/transforms3d/blob/6a43a98/transforms3d/affines.py#L10-L153)
|
|
130
|
+
//
|
|
131
|
+
// As an optimization, this only orthogonalizes the basis,
|
|
132
|
+
// and does not normalize to unit vectors.
|
|
133
|
+
|
|
134
|
+
// Operate on columns, which are the cosines that project input coordinates onto output axes
|
|
135
|
+
const [cosX, cosY, cosZ] = [0, 1, 2].map((j) => [0, 1, 2].map((i) => affine[i][j]))
|
|
136
|
+
|
|
137
|
+
// Orthogonalize cosY with respect to cosX
|
|
138
|
+
const orthY = sub(cosY, scale(cosX, dot(cosX, cosY)))
|
|
139
|
+
|
|
140
|
+
// Orthogonalize cosZ with respect to cosX and orthY
|
|
141
|
+
const orthZ = sub(
|
|
142
|
+
cosZ,
|
|
143
|
+
add(scale(cosX, dot(cosX, cosZ)), scale(orthY, dot(orthY, cosZ))),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
const basis = [cosX, orthY, orthZ]
|
|
147
|
+
const maxIndices = basis.map((row) => argMax(row.map(Math.abs)))
|
|
148
|
+
|
|
149
|
+
// Check that indices are 0, 1 and 2 in some order
|
|
150
|
+
if (maxIndices.toSorted().some((idx, i) => idx !== i)) {
|
|
151
|
+
throw { key: 'AMBIGUOUS_AFFINE' }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Positive/negative codes for each world axis
|
|
155
|
+
const codes = ['RL', 'AP', 'SI']
|
|
156
|
+
return maxIndices.map((idx, i) => codes[idx][basis[i][idx] > 0 ? 0 : 1])
|
|
157
|
+
}
|
|
@@ -8,6 +8,16 @@ export class UnicodeDecodeError extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const _decode = TextDecoder.prototype.decode
|
|
12
|
+
|
|
13
|
+
TextDecoder.prototype.decode = function (input, options) {
|
|
14
|
+
try {
|
|
15
|
+
return _decode.call(this, input, options)
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw { code: 'INVALID_FILE_ENCODING', message: error }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
/**
|
|
12
22
|
* A transformer that ensures the input stream is valid UTF-8 and throws
|
|
13
23
|
* a UnicodeDecodeError if UTF-16 BOM is detected
|
|
@@ -16,15 +26,15 @@ export class UTF8StreamTransformer implements Transformer<Uint8Array, string> {
|
|
|
16
26
|
private decoder: TextDecoder
|
|
17
27
|
private firstChunk: boolean
|
|
18
28
|
|
|
19
|
-
constructor() {
|
|
20
|
-
this.decoder = new TextDecoder('utf-8')
|
|
29
|
+
constructor(options = { fatal: false }) {
|
|
30
|
+
this.decoder = new TextDecoder('utf-8', options)
|
|
21
31
|
this.firstChunk = true
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
transform(chunk: Uint8Array, controller: TransformStreamDefaultController<string>) {
|
|
25
35
|
// Check first chunk for UTF-16 BOM
|
|
26
36
|
if (this.firstChunk) {
|
|
27
|
-
|
|
37
|
+
let decoded = this.decoder.decode(chunk, { stream: true })
|
|
28
38
|
if (decoded.startsWith('\uFFFD')) {
|
|
29
39
|
throw new UnicodeDecodeError('This file appears to be UTF-16')
|
|
30
40
|
}
|
|
@@ -46,6 +56,6 @@ export class UTF8StreamTransformer implements Transformer<Uint8Array, string> {
|
|
|
46
56
|
/**
|
|
47
57
|
* Creates a TransformStream that validates and decodes UTF-8 text
|
|
48
58
|
*/
|
|
49
|
-
export function createUTF8Stream() {
|
|
50
|
-
return new TransformStream(new UTF8StreamTransformer())
|
|
59
|
+
export function createUTF8Stream(options = { fatal: false }) {
|
|
60
|
+
return new TransformStream(new UTF8StreamTransformer(options))
|
|
51
61
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assert, assertObjectMatch } from '@std/assert'
|
|
2
2
|
import { parseTIFF } from './tiff.ts'
|
|
3
3
|
import { BIDSFileDeno } from './deno.ts'
|
|
4
|
+
import { testAsyncFileAccess } from './access.test.ts'
|
|
4
5
|
|
|
5
6
|
Deno.test('parseTIFF', async (t) => {
|
|
6
7
|
await t.step('parse example file as TIFF', async () => {
|
|
@@ -53,3 +54,5 @@ Deno.test('parseTIFF', async (t) => {
|
|
|
53
54
|
})
|
|
54
55
|
})
|
|
55
56
|
})
|
|
57
|
+
|
|
58
|
+
testAsyncFileAccess('Test file access errors for parseTIFF', parseTIFF)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { Ome, Tiff } from '@bids/schema/context'
|
|
6
6
|
import * as XML from '@libs/xml'
|
|
7
7
|
import type { BIDSFile } from '../types/filetree.ts'
|
|
8
|
+
import { readBytes } from './access.ts'
|
|
8
9
|
|
|
9
10
|
function getImageDescription(
|
|
10
11
|
dataview: DataView<ArrayBuffer>,
|
|
@@ -44,7 +45,7 @@ export async function parseTIFF(
|
|
|
44
45
|
file: BIDSFile,
|
|
45
46
|
OME: boolean,
|
|
46
47
|
): Promise<{ tiff?: Tiff; ome?: Ome }> {
|
|
47
|
-
const buf = await
|
|
48
|
+
const buf = await readBytes(file, 4096)
|
|
48
49
|
const dataview = new DataView(buf.buffer, buf.byteOffset, buf.byteLength)
|
|
49
50
|
const magic = dataview.getUint16(0, true)
|
|
50
51
|
const littleEndian = magic === 0x4949
|