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.
Files changed (114) hide show
  1. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/PKG-INFO +1 -1
  2. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/deno.json +18 -15
  3. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/pyproject.toml +1 -1
  4. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/bids-validator.ts +1 -0
  5. bids_validator_deno-2.1.1/src/files/access.test.ts +40 -0
  6. bids_validator_deno-2.1.1/src/files/access.ts +35 -0
  7. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/browser.ts +2 -2
  8. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/deno.ts +1 -1
  9. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/gzip.test.ts +3 -0
  10. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/gzip.ts +2 -1
  11. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/inheritance.test.ts +18 -0
  12. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/inheritance.ts +1 -1
  13. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/json.test.ts +13 -5
  14. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/json.ts +6 -2
  15. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/nifti.test.ts +28 -2
  16. bids_validator_deno-2.1.1/src/files/nifti.ts +157 -0
  17. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/streams.ts +15 -5
  18. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/tiff.test.ts +3 -0
  19. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/tiff.ts +2 -1
  20. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/tsv.test.ts +131 -2
  21. bids_validator_deno-2.1.1/src/files/tsv.ts +103 -0
  22. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/issues/list.ts +16 -7
  23. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/applyRules.test.ts +1 -46
  24. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/applyRules.ts +39 -29
  25. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/associations.ts +4 -2
  26. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/context.test.ts +7 -1
  27. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/context.ts +75 -47
  28. bids_validator_deno-2.1.1/src/schema/datatypes.test.ts +57 -0
  29. bids_validator_deno-2.1.1/src/schema/datatypes.ts +30 -0
  30. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/expressionLanguage.test.ts +81 -2
  31. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/expressionLanguage.ts +53 -0
  32. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/tables.test.ts +86 -33
  33. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/tables.ts +94 -101
  34. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/walk.ts +2 -0
  35. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/loadSchema.ts +5 -5
  36. bids_validator_deno-2.1.1/src/tests/local/nifti_rules.test.ts +111 -0
  37. bids_validator_deno-2.1.1/src/tests/utils.ts +14 -0
  38. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/filetree.ts +1 -1
  39. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/bids.ts +3 -0
  40. bids_validator_deno-2.1.1/src/validators/filenameCase.test.ts +32 -0
  41. bids_validator_deno-2.1.1/src/validators/filenameCase.ts +16 -0
  42. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/filenameIdentify.test.ts +3 -21
  43. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/filenameIdentify.ts +0 -25
  44. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/internal/unusedFile.ts +9 -5
  45. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/validateFiles.test.ts +15 -13
  46. bids_validator_deno-2.0.11/src/files/nifti.ts +0 -72
  47. bids_validator_deno-2.0.11/src/files/tsv.ts +0 -68
  48. bids_validator_deno-2.0.11/src/schema/modalities.ts +0 -16
  49. bids_validator_deno-2.0.11/src/tests/utils.ts +0 -12
  50. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/LICENSE +0 -0
  51. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/README.md +0 -0
  52. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/pdm_build.py +0 -0
  53. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/.git-meta.json +0 -0
  54. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/browser.test.ts +0 -0
  55. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/deno.test.ts +0 -0
  56. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/dwi.test.ts +0 -0
  57. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/dwi.ts +0 -0
  58. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/filetree.test.ts +0 -0
  59. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/filetree.ts +0 -0
  60. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/ignore.test.ts +0 -0
  61. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/ignore.ts +0 -0
  62. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/files/streams.test.ts +0 -0
  63. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/issues/datasetIssues.test.ts +0 -0
  64. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/issues/datasetIssues.ts +0 -0
  65. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/issues/list.test.ts +0 -0
  66. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/main.ts +0 -0
  67. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/entities.test.ts +0 -0
  68. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/entities.ts +0 -0
  69. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/fixtures.test.ts +0 -0
  70. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/schema/walk.test.ts +0 -0
  71. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/loadSchema.test.ts +0 -0
  72. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/options.test.ts +0 -0
  73. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/options.ts +0 -0
  74. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/setup/requestPermissions.ts +0 -0
  75. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/summary/collectSubjectMetadata.ts +0 -0
  76. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/summary/summary.test.ts +0 -0
  77. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/summary/summary.ts +0 -0
  78. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/README.md +0 -0
  79. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/bom-utf16.tsv +0 -0
  80. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/bom-utf8.json +0 -0
  81. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/generate-filenames.ts +0 -0
  82. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/bids_examples.test.ts +0 -0
  83. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/common.ts +0 -0
  84. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/derivatives.test.ts +0 -0
  85. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/empty_files.test.ts +0 -0
  86. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/hed-integration.test.ts +0 -0
  87. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/valid_dataset.test.ts +0 -0
  88. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/valid_filenames.test.ts +0 -0
  89. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/local/valid_headers.test.ts +0 -0
  90. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/nullReadBytes.ts +0 -0
  91. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/regression.test.ts +0 -0
  92. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/schema-expression-language.test.ts +0 -0
  93. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/tests/simple-dataset.ts +0 -0
  94. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/check.ts +0 -0
  95. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/columns.test.ts +0 -0
  96. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/columns.ts +0 -0
  97. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/issues.ts +0 -0
  98. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/schema.ts +0 -0
  99. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/types/validation-result.ts +0 -0
  100. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/errors.ts +0 -0
  101. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/logger.test.ts +0 -0
  102. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/logger.ts +0 -0
  103. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/memoize.ts +0 -0
  104. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/objectPathHandler.ts +0 -0
  105. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/utils/output.ts +0 -0
  106. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/bids.test.ts +0 -0
  107. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/citation.test.ts +0 -0
  108. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/citation.ts +0 -0
  109. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/filenameValidate.test.ts +0 -0
  110. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/filenameValidate.ts +0 -0
  111. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/hed.ts +0 -0
  112. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/internal/emptyFile.ts +0 -0
  113. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/validators/json.ts +0 -0
  114. {bids_validator_deno-2.0.11 → bids_validator_deno-2.1.1}/src/version.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bids-validator-deno
3
- Version: 2.0.11
3
+ Version: 2.1.1
4
4
  Summary: Typescript implementation of the BIDS validator
5
5
  Keywords: BIDS,BIDS validator
6
6
  Author: bids-standard developers
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bids/validator",
3
- "version": "2.0.11",
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@8.17.1",
31
- "@bids/schema": "jsr:@bids/schema@~1.0.14",
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@4.0.1",
35
- "@ignore": "npm:ignore@7.0.3",
36
- "@libs/xml": "jsr:@libs/xml@6.0.4",
37
- "@mango/nifti": "npm:@bids/nifti-reader-js@0.6.9",
38
- "@std/assert": "jsr:@std/assert@1.0.12",
39
- "@std/fmt": "jsr:@std/fmt@1.0.6",
40
- "@std/fs": "jsr:@std/fs@1.0.15",
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.0.8",
44
- "@std/streams": "jsr:@std/streams@1.0.9",
45
- "@std/yaml": "jsr:@std/yaml@^1.0.5"
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
  }
@@ -18,7 +18,7 @@ keywords = [
18
18
  "BIDS validator",
19
19
  ]
20
20
  dynamic = []
21
- version = "2.0.11"
21
+ version = "2.1.1"
22
22
 
23
23
  [project.license]
24
24
  text = "MIT"
@@ -6,3 +6,4 @@ const errors = result.issues.get({ severity: 'error' })
6
6
  if (errors.length) {
7
7
  Deno.exit(16)
8
8
  }
9
+ Deno.exit(0)
@@ -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 file.readBytes(maxBytes)
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 file.readBytes(file.size)
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
- export async function loadJSON(file: BIDSFile): Promise<Record<string, unknown>> {
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
- const decoded = this.decoder.decode(chunk, { stream: true })
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 file.readBytes(4096)
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