bids-validator-deno 2.1.0__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 (112) hide show
  1. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/PKG-INFO +1 -1
  2. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/deno.json +17 -14
  3. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/pyproject.toml +1 -1
  4. bids_validator_deno-2.1.1/src/files/access.test.ts +40 -0
  5. bids_validator_deno-2.1.1/src/files/access.ts +35 -0
  6. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/browser.ts +2 -2
  7. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/deno.ts +1 -1
  8. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/gzip.test.ts +3 -0
  9. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/gzip.ts +2 -1
  10. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/inheritance.test.ts +18 -0
  11. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/inheritance.ts +1 -1
  12. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/json.test.ts +13 -5
  13. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/json.ts +6 -2
  14. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/nifti.test.ts +4 -1
  15. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/nifti.ts +4 -2
  16. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/streams.ts +15 -5
  17. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/tiff.test.ts +3 -0
  18. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/tiff.ts +2 -1
  19. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/tsv.test.ts +32 -12
  20. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/tsv.ts +6 -4
  21. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/issues/list.ts +7 -1
  22. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/applyRules.test.ts +1 -46
  23. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/applyRules.ts +39 -29
  24. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/associations.ts +4 -2
  25. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/context.ts +16 -9
  26. bids_validator_deno-2.1.1/src/schema/datatypes.test.ts +57 -0
  27. bids_validator_deno-2.1.1/src/schema/datatypes.ts +30 -0
  28. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/expressionLanguage.test.ts +81 -2
  29. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/expressionLanguage.ts +53 -0
  30. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/tables.test.ts +67 -33
  31. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/tables.ts +78 -99
  32. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/walk.ts +2 -0
  33. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/setup/loadSchema.ts +5 -5
  34. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/nifti_rules.test.ts +14 -14
  35. bids_validator_deno-2.1.1/src/tests/utils.ts +14 -0
  36. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/types/filetree.ts +1 -1
  37. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/bids.ts +2 -0
  38. bids_validator_deno-2.1.1/src/validators/filenameCase.test.ts +32 -0
  39. bids_validator_deno-2.1.1/src/validators/filenameCase.ts +16 -0
  40. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/filenameIdentify.test.ts +1 -19
  41. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/filenameIdentify.ts +0 -25
  42. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/validateFiles.test.ts +15 -13
  43. bids_validator_deno-2.1.0/src/schema/modalities.ts +0 -16
  44. bids_validator_deno-2.1.0/src/tests/utils.ts +0 -12
  45. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/LICENSE +0 -0
  46. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/README.md +0 -0
  47. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/pdm_build.py +0 -0
  48. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/.git-meta.json +0 -0
  49. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/bids-validator.ts +0 -0
  50. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/browser.test.ts +0 -0
  51. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/deno.test.ts +0 -0
  52. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/dwi.test.ts +0 -0
  53. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/dwi.ts +0 -0
  54. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/filetree.test.ts +0 -0
  55. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/filetree.ts +0 -0
  56. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/ignore.test.ts +0 -0
  57. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/ignore.ts +0 -0
  58. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/files/streams.test.ts +0 -0
  59. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/issues/datasetIssues.test.ts +0 -0
  60. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/issues/datasetIssues.ts +0 -0
  61. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/issues/list.test.ts +0 -0
  62. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/main.ts +0 -0
  63. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/context.test.ts +0 -0
  64. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/entities.test.ts +0 -0
  65. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/entities.ts +0 -0
  66. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/fixtures.test.ts +0 -0
  67. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/schema/walk.test.ts +0 -0
  68. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/setup/loadSchema.test.ts +0 -0
  69. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/setup/options.test.ts +0 -0
  70. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/setup/options.ts +0 -0
  71. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/setup/requestPermissions.ts +0 -0
  72. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/summary/collectSubjectMetadata.ts +0 -0
  73. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/summary/summary.test.ts +0 -0
  74. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/summary/summary.ts +0 -0
  75. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/README.md +0 -0
  76. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/bom-utf16.tsv +0 -0
  77. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/bom-utf8.json +0 -0
  78. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/generate-filenames.ts +0 -0
  79. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/bids_examples.test.ts +0 -0
  80. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/common.ts +0 -0
  81. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/derivatives.test.ts +0 -0
  82. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/empty_files.test.ts +0 -0
  83. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/hed-integration.test.ts +0 -0
  84. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/valid_dataset.test.ts +0 -0
  85. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/valid_filenames.test.ts +0 -0
  86. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/local/valid_headers.test.ts +0 -0
  87. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/nullReadBytes.ts +0 -0
  88. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/regression.test.ts +0 -0
  89. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/schema-expression-language.test.ts +0 -0
  90. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/tests/simple-dataset.ts +0 -0
  91. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/types/check.ts +0 -0
  92. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/types/columns.test.ts +0 -0
  93. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/types/columns.ts +0 -0
  94. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/types/issues.ts +0 -0
  95. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/types/schema.ts +0 -0
  96. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/types/validation-result.ts +0 -0
  97. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/utils/errors.ts +0 -0
  98. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/utils/logger.test.ts +0 -0
  99. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/utils/logger.ts +0 -0
  100. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/utils/memoize.ts +0 -0
  101. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/utils/objectPathHandler.ts +0 -0
  102. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/utils/output.ts +0 -0
  103. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/bids.test.ts +0 -0
  104. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/citation.test.ts +0 -0
  105. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/citation.ts +0 -0
  106. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/filenameValidate.test.ts +0 -0
  107. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/filenameValidate.ts +0 -0
  108. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/hed.ts +0 -0
  109. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/internal/emptyFile.ts +0 -0
  110. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/internal/unusedFile.ts +0 -0
  111. {bids_validator_deno-2.1.0 → bids_validator_deno-2.1.1}/src/validators/json.ts +0 -0
  112. {bids_validator_deno-2.1.0 → 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.1.0
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.1.0",
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",
30
+ "@ajv": "npm:ajv@^8.17.1",
31
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.1.0"
21
+ version = "2.1.1"
22
22
 
23
23
  [project.license]
24
24
  text = "MIT"
@@ -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
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, axisCodes } 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([])
@@ -96,3 +97,5 @@ Deno.test('Test extracting axis codes', async (t) => {
96
97
  assertEquals(axisCodes(affine), ['A', 'S', 'R'])
97
98
  })
98
99
  })
100
+
101
+ testAsyncFileAccess('Test file access errors for loadHeader', loadHeader)
@@ -2,6 +2,7 @@ import { isCompressed, isNIFTI1, isNIFTI2, NIFTI1, NIFTI2 } from '@mango/nifti'
2
2
  import type { BIDSFile } from '../types/filetree.ts'
3
3
  import { logger } from '../utils/logger.ts'
4
4
  import type { NiftiHeader } from '@bids/schema/context'
5
+ import { readBytes } from './access.ts'
5
6
 
6
7
  async function extract(buffer: Uint8Array, nbytes: number): Promise<Uint8Array<ArrayBuffer>> {
7
8
  // The fflate decompression that is used in nifti-reader does not like
@@ -32,8 +33,8 @@ async function extract(buffer: Uint8Array, nbytes: number): Promise<Uint8Array<A
32
33
  }
33
34
 
34
35
  export async function loadHeader(file: BIDSFile): Promise<NiftiHeader> {
36
+ const buf = await readBytes(file, 1024)
35
37
  try {
36
- const buf = await file.readBytes(1024)
37
38
  const data = isCompressed(buf.buffer) ? await extract(buf, 540) : buf.slice(0, 540)
38
39
  let header
39
40
  if (isNIFTI1(data.buffer)) {
@@ -138,7 +139,8 @@ export function axisCodes(affine: number[][]): string[] {
138
139
 
139
140
  // Orthogonalize cosZ with respect to cosX and orthY
140
141
  const orthZ = sub(
141
- cosZ, add(scale(cosX, dot(cosX, cosZ)), scale(orthY, dot(orthY, cosZ)))
142
+ cosZ,
143
+ add(scale(cosX, dot(cosX, cosZ)), scale(orthY, dot(orthY, cosZ))),
142
144
  )
143
145
 
144
146
  const basis = [cosX, orthY, orthZ]
@@ -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
@@ -6,9 +6,17 @@ import {
6
6
  assertStrictEquals,
7
7
  } from '@std/assert'
8
8
  import { pathToFile } from './filetree.ts'
9
+ import { BIDSFileDeno } from './deno.ts'
9
10
  import { loadTSV, loadTSVGZ } from './tsv.ts'
10
11
  import { streamFromString } from '../tests/utils.ts'
11
12
  import { ColumnsMap } from '../types/columns.ts'
13
+ import { testAsyncFileAccess } from './access.test.ts'
14
+
15
+ function compressedStreamFromString(str: string): ReadableStream<Uint8Array<ArrayBuffer>> {
16
+ return streamFromString(str).pipeThrough(new CompressionStream('gzip')) as ReadableStream<
17
+ Uint8Array<ArrayBuffer>
18
+ >
19
+ }
12
20
 
13
21
  Deno.test('TSV loading', async (t) => {
14
22
  await t.step('Empty file produces empty map', async () => {
@@ -175,6 +183,17 @@ Deno.test('TSV loading', async (t) => {
175
183
  }
176
184
  })
177
185
 
186
+ await t.step('Raises issue on non utf-8', async () => {
187
+ const file = new BIDSFileDeno('', './tests/data/iso8859.tsv')
188
+
189
+ try {
190
+ await loadTSV(file)
191
+ assert(false, 'Expected error')
192
+ } catch (e: any) {
193
+ assertObjectMatch(e, { code: 'INVALID_FILE_ENCODING' })
194
+ }
195
+ })
196
+
178
197
  // Tests will have populated the memoization cache
179
198
  loadTSV.cache.clear()
180
199
  })
@@ -182,7 +201,7 @@ Deno.test('TSV loading', async (t) => {
182
201
  Deno.test('TSVGZ loading', async (t) => {
183
202
  await t.step('No header and empty file produces empty map', async () => {
184
203
  const file = pathToFile('/empty.tsv.gz')
185
- file.stream = streamFromString('').pipeThrough(new CompressionStream('gzip'))
204
+ file.stream = compressedStreamFromString('')
186
205
 
187
206
  const map = await loadTSVGZ(file, [])
188
207
  // map.size looks for a column called map, so work around it
@@ -191,7 +210,7 @@ Deno.test('TSVGZ loading', async (t) => {
191
210
 
192
211
  await t.step('Empty file produces header-only map', async () => {
193
212
  const file = pathToFile('/empty.tsv.gz')
194
- file.stream = streamFromString('').pipeThrough(new CompressionStream('gzip'))
213
+ file.stream = compressedStreamFromString('')
195
214
 
196
215
  const map = await loadTSVGZ(file, ['a', 'b', 'c'])
197
216
  assertEquals(map.a, [])
@@ -201,7 +220,7 @@ Deno.test('TSVGZ loading', async (t) => {
201
220
 
202
221
  await t.step('Single column file produces single column maps', async () => {
203
222
  const file = pathToFile('/single_column.tsv')
204
- file.stream = streamFromString('1\n2\n3\n').pipeThrough(new CompressionStream('gzip'))
223
+ file.stream = compressedStreamFromString('1\n2\n3\n')
205
224
 
206
225
  const map = await loadTSVGZ(file, ['a'])
207
226
  assertEquals(map.a, ['1', '2', '3'])
@@ -209,7 +228,7 @@ Deno.test('TSVGZ loading', async (t) => {
209
228
 
210
229
  await t.step('Mismatched header length throws issue', async () => {
211
230
  const file = pathToFile('/single_column.tsv.gz')
212
- file.stream = streamFromString('1\n2\n3\n').pipeThrough(new CompressionStream('gzip'))
231
+ file.stream = compressedStreamFromString('1\n2\n3\n')
213
232
 
214
233
  try {
215
234
  await loadTSVGZ(file, ['a', 'b'])
@@ -220,7 +239,7 @@ Deno.test('TSVGZ loading', async (t) => {
220
239
 
221
240
  await t.step('Missing final newline is ignored', async () => {
222
241
  const file = pathToFile('/missing_newline.tsv.gz')
223
- file.stream = streamFromString('1\n2\n3').pipeThrough(new CompressionStream('gzip'))
242
+ file.stream = compressedStreamFromString('1\n2\n3')
224
243
 
225
244
  const map = await loadTSVGZ(file, ['a'])
226
245
  assertEquals(map.a, ['1', '2', '3'])
@@ -228,7 +247,7 @@ Deno.test('TSVGZ loading', async (t) => {
228
247
 
229
248
  await t.step('Empty row throws issue', async () => {
230
249
  const file = pathToFile('/empty_row.tsv.gz')
231
- file.stream = streamFromString('1\t2\t3\n\n4\t5\t6\n').pipeThrough(new CompressionStream('gzip'))
250
+ file.stream = compressedStreamFromString('1\t2\t3\n\n4\t5\t6\n')
232
251
 
233
252
  try {
234
253
  await loadTSVGZ(file, ['a', 'b', 'c'])
@@ -253,37 +272,38 @@ Deno.test('TSVGZ loading', async (t) => {
253
272
  // Use 1500 to avoid overlap with default initial capacity
254
273
  const headers = ['a', 'b', 'c']
255
274
  const text = '1\t2\t3\n'.repeat(1500)
256
- file.stream = streamFromString(text).pipeThrough(new CompressionStream('gzip'))
275
+ file.stream = compressedStreamFromString(text)
257
276
 
258
277
  let map = await loadTSVGZ(file, headers, 0)
259
278
  assertEquals(map.a, [])
260
279
  assertEquals(map.b, [])
261
280
  assertEquals(map.c, [])
262
281
 
263
- file.stream = streamFromString(text).pipeThrough(new CompressionStream('gzip'))
282
+ file.stream = compressedStreamFromString(text)
264
283
  map = await loadTSVGZ(file, headers, 1)
265
284
  assertEquals(map.a, ['1'])
266
285
  assertEquals(map.b, ['2'])
267
286
  assertEquals(map.c, ['3'])
268
287
 
269
- file.stream = streamFromString(text).pipeThrough(new CompressionStream('gzip'))
288
+ file.stream = compressedStreamFromString(text)
270
289
  map = await loadTSVGZ(file, headers, 2)
271
290
  assertEquals(map.a, ['1', '1'])
272
291
  assertEquals(map.b, ['2', '2'])
273
292
  assertEquals(map.c, ['3', '3'])
274
293
 
275
- file.stream = streamFromString(text).pipeThrough(new CompressionStream('gzip'))
294
+ file.stream = compressedStreamFromString(text)
276
295
  map = await loadTSVGZ(file, headers, -1)
277
296
  assertEquals(map.a, Array(1500).fill('1'))
278
297
  assertEquals(map.b, Array(1500).fill('2'))
279
298
  assertEquals(map.c, Array(1500).fill('3'))
280
299
 
281
300
  // Check that maxRows does not truncate shorter files
282
- file.stream = streamFromString('1\t2\t3\n4\t5\t6\n7\t8\t9\n').pipeThrough(new CompressionStream('gzip'))
301
+ file.stream = compressedStreamFromString('1\t2\t3\n4\t5\t6\n7\t8\t9\n')
283
302
  map = await loadTSVGZ(file, headers, 4)
284
303
  assertEquals(map.a, ['1', '4', '7'])
285
304
  assertEquals(map.b, ['2', '5', '8'])
286
305
  assertEquals(map.c, ['3', '6', '9'])
287
306
  })
288
-
289
307
  })
308
+
309
+ testAsyncFileAccess('Test file access errors for loadTSV', loadTSV)
@@ -7,6 +7,8 @@ import { ColumnsMap } from '../types/columns.ts'
7
7
  import type { BIDSFile } from '../types/filetree.ts'
8
8
  import { filememoizeAsync } from '../utils/memoize.ts'
9
9
  import { createUTF8Stream } from './streams.ts'
10
+ import { openStream } from './access.ts'
11
+ import { BIDSFileDeno } from './deno.ts'
10
12
 
11
13
  async function loadColumns(
12
14
  reader: ReadableStreamDefaultReader<string>,
@@ -55,9 +57,9 @@ export async function loadTSVGZ(
55
57
  headers: string[],
56
58
  maxRows: number = -1,
57
59
  ): Promise<ColumnsMap> {
58
- const reader = file.stream
60
+ const reader = openStream(file)
59
61
  .pipeThrough(new DecompressionStream('gzip'))
60
- .pipeThrough(createUTF8Stream())
62
+ .pipeThrough(createUTF8Stream({ fatal: true }))
61
63
  .pipeThrough(new TextLineStream())
62
64
  .getReader()
63
65
 
@@ -75,8 +77,8 @@ export async function loadTSVGZ(
75
77
  }
76
78
 
77
79
  async function _loadTSV(file: BIDSFile, maxRows: number = -1): Promise<ColumnsMap> {
78
- const reader = file.stream
79
- .pipeThrough(createUTF8Stream())
80
+ const reader = openStream(file)
81
+ .pipeThrough(createUTF8Stream({ fatal: true }))
80
82
  .pipeThrough(new TextLineStream())
81
83
  .getReader()
82
84
 
@@ -130,7 +130,8 @@ export const bidsIssues: IssueDefinitionRecord = {
130
130
  },
131
131
  TSV_PSEUDO_AGE_DEPRECATED: {
132
132
  severity: 'warning',
133
- reason: 'Use of the value "89+" in column "age" is deprecated. Use 89 for all ages 89 and over.',
133
+ reason:
134
+ 'Use of the value "89+" in column "age" is deprecated. Use 89 for all ages 89 and over.',
134
135
  },
135
136
  INVALID_GZIP: {
136
137
  severity: 'error',
@@ -198,6 +199,11 @@ export const bidsIssues: IssueDefinitionRecord = {
198
199
  severity: 'warning',
199
200
  reason: 'The validation on this HED string returned a warning.',
200
201
  },
202
+ CASE_COLLISION: {
203
+ severity: 'error',
204
+ reason: 'Files with the same name but different casing have been found.',
205
+ },
206
+
201
207
  }
202
208
 
203
209
  export const nonSchemaIssues = { ...bidsIssues }