diff --git a/.eslintrc b/.eslintrc index 57b17692..a0e84494 100644 --- a/.eslintrc +++ b/.eslintrc @@ -29,7 +29,7 @@ "no-useless-catch": 1, "no-prototype-builtins": 1, "no-constant-condition": 0, - "no-useless-escape" : 0, + "no-useless-escape": 0, "no-console": "error", "eqeqeq": ["error", "smart"], "spaced-comment": [ @@ -94,7 +94,6 @@ "@typescript-eslint/no-non-null-assertion": 0, "@typescript-eslint/no-this-alias": 0, "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/ban-ts-comment": 0, "@typescript-eslint/no-empty-function": 0, "@typescript-eslint/no-empty-interface": 0, "@typescript-eslint/consistent-type-imports": ["error"], @@ -108,6 +107,7 @@ "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }], + "@typescript-eslint/await-thenable": ["error"], "@typescript-eslint/naming-convention": [ "error", { @@ -141,6 +141,12 @@ "selector": "typeProperty", "format": null } + ], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-ignore": "allow-with-description" + } ] } } diff --git a/jest.config.js b/jest.config.js index ecf87505..251074db 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,4 @@ -const { pathsToModuleNameMapper } = require('ts-jest/utils'); +const { pathsToModuleNameMapper } = require('ts-jest'); const { compilerOptions } = require('./tsconfig'); module.exports = { diff --git a/package-lock.json b/package-lock.json index ec5adc08..72fc7372 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,16 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@arrows/array": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@arrows/array/-/array-1.4.1.tgz", @@ -56,64 +66,131 @@ } }, "@babel/compat-data": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", - "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", + "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", "dev": true }, "@babel/core": { - "version": "7.15.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.5.tgz", - "integrity": "sha512-pYgXxiwAgQpgM1bNkZsDEq85f0ggXMA5L7c+o3tskGMh2BunCI9QUwB9Z4jpvXUOuMdyGKiGKQiRe11VS6Jzvg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.15.4", - "@babel/helper-compilation-targets": "^7.15.4", - "@babel/helper-module-transforms": "^7.15.4", - "@babel/helpers": "^7.15.4", - "@babel/parser": "^7.15.5", - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4", + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.9.tgz", + "integrity": "sha512-5ug+SfZCpDAkVp9SFIZAzlW18rlzsOcJGaetCjkySnrXXDUw9AR8cDUm1iByTmdWM6yxX6/zycaV76w3YTF2gw==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.9", + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helpers": "^7.17.9", + "@babel/parser": "^7.17.9", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.9", + "@babel/types": "^7.17.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" + "json5": "^2.2.1", + "semver": "^6.3.0" }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { - "@babel/highlight": "^7.14.5" + "color-name": "1.1.3" } }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, "@babel/generator": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.4.tgz", - "integrity": "sha512-d3itta0tu+UayjEORPNz6e1T3FtvWlP5N4V5M+lhp/CxT4oAA7/NcScnpRyspUMLK6tu9MNHmQHxRykuN2R7hw==", + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.9.tgz", + "integrity": "sha512-rAdDousTwxbIxbz5I7GEQ3lUip+xVCXooZNbsydCWs3xA7ZsYOv+CFRdzGxRX78BmQHu9B1Eso59AOZQOJDEdQ==", "dev": true, "requires": { - "@babel/types": "^7.15.4", + "@babel/types": "^7.17.0", "jsesc": "^2.5.1", "source-map": "^0.5.0" }, @@ -127,14 +204,14 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.4.tgz", - "integrity": "sha512-rMWPCirulnPSe4d+gwdWXLfAXTTBj8M3guAf5xFQJ0nvFY7tfNAFnWdqaHegHlgDZOCT4qvhF3BYlSJag8yhqQ==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz", + "integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==", "dev": true, "requires": { - "@babel/compat-data": "^7.15.0", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", + "@babel/compat-data": "^7.17.7", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.17.5", "semver": "^6.3.0" }, "dependencies": { @@ -146,112 +223,89 @@ } } }, - "@babel/helper-function-name": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.15.4.tgz", - "integrity": "sha512-Z91cOMM4DseLIGOnog+Z8OI6YseR9bua+HpvLAQ2XayUGU+neTtX+97caALaLdyu53I/fjhbeCnWnRH1O3jFOw==", + "@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.15.4", - "@babel/template": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, - "@babel/helper-get-function-arity": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.15.4.tgz", - "integrity": "sha512-1/AlxSF92CmGZzHnC515hm4SirTxtpDnLEJ0UyEMgTMZN+6bxXKg04dKhiRx5Enel+SUA1G1t5Ed/yQia0efrA==", + "@babel/helper-function-name": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.17.9.tgz", + "integrity": "sha512-7cRisGlVtiVqZ0MW0/yFB4atgpGLWEHUVYnb448hZK4x+vih0YO5UoS11XIYtZYqHd0dIPMdUSv8q5K4LdMnIg==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/template": "^7.16.7", + "@babel/types": "^7.17.0" } }, "@babel/helper-hoist-variables": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.15.4.tgz", - "integrity": "sha512-VTy085egb3jUGVK9ycIxQiPbquesq0HUQ+tPO0uv5mPEBZipk+5FkRKiWq5apuyTE9FUrjENB0rCf8y+n+UuhA==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.4.tgz", - "integrity": "sha512-cokOMkxC/BTyNP1AlY25HuBWM32iCEsLPI4BHDpJCHHm1FU2E7dKWWIXJgQgSFiu4lp8q3bL1BIKwqkSUviqtA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-module-imports": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.15.4.tgz", - "integrity": "sha512-jeAHZbzUwdW/xHgHQ3QmWR4Jg6j15q4w/gCfwZvtqOxoo5DKtLHk8Bsf4c5RZRC7NmLEs+ohkdq8jFefuvIxAA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-module-transforms": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.4.tgz", - "integrity": "sha512-9fHHSGE9zTC++KuXLZcB5FKgvlV83Ox+NLUmQTawovwlJ85+QMhk1CnVk406CQVj97LaWod6KVjl2Sfgw9Aktw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.15.4", - "@babel/helper-replace-supers": "^7.15.4", - "@babel/helper-simple-access": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/helper-validator-identifier": "^7.14.9", - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.15.4.tgz", - "integrity": "sha512-E/z9rfbAOt1vDW1DR7k4SzhzotVV5+qMciWV6LaG1g4jeFrkDlJedjtV4h0i4Q/ITnUu+Pk08M7fczsB9GXBDw==", - "dev": true, - "requires": { - "@babel/types": "^7.15.4" + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + } } }, "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", "dev": true }, - "@babel/helper-replace-supers": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.4.tgz", - "integrity": "sha512-/ztT6khaXF37MS47fufrKvIsiQkx1LBRvSJNzRqmbyeZnTwU9qBxXYLaaT/6KaxfKhjs2Wy8kG8ZdsFUuWBjzw==", - "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.15.4", - "@babel/helper-optimise-call-expression": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4" - } - }, "@babel/helper-simple-access": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.15.4.tgz", - "integrity": "sha512-UzazrDoIVOZZcTeHHEPYrr1MvTR/K+wgLg6MY6e1CJyaRhbibftF6fR2KU2sFRtI/nERUZR9fBd6aKgBlIBaPg==", + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.17.0" } }, "@babel/helper-split-export-declaration": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.15.4.tgz", - "integrity": "sha512-HsFqhLDZ08DxCpBdEVtKmywj6PQbwnF6HHybur0MAnkAKnlS6uHkwnmRIkElB2Owpfb4xL4NwDmDLFubueDXsw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", "dev": true, "requires": { - "@babel/types": "^7.15.4" + "@babel/types": "^7.16.7" } }, "@babel/helper-validator-identifier": { @@ -261,20 +315,20 @@ "dev": true }, "@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", "dev": true }, "@babel/helpers": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.4.tgz", - "integrity": "sha512-V45u6dqEJ3w2rlryYYXf6i9rQ5YMNu4FLS6ngs8ikblhu2VdR1AqAd6aJjBzmf2Qzh6KOLqKHxEN9+TFbAkAVQ==", + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.9.tgz", + "integrity": "sha512-cPCt915ShDWUEzEp3+UNRktO2n6v49l5RSnG9M5pS24hA+2FAc5si+Pn1i4VVbQQ+jh+bIZhPFQOJOzbrOYY1Q==", "dev": true, "requires": { - "@babel/template": "^7.15.4", - "@babel/traverse": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.9", + "@babel/types": "^7.17.0" } }, "@babel/highlight": { @@ -347,9 +401,9 @@ } }, "@babel/parser": { - "version": "7.15.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.6.tgz", - "integrity": "sha512-S/TSCcsRuCkmpUuoWijua0Snt+f3ewU/8spLo+4AXJCZfT0bVCzLD5MuOKdrx0mlAptbKzn5AdgEIIKXxXkz9Q==", + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.9.tgz", + "integrity": "sha512-vqUSBLP8dQHFPdPi9bc5GK9vRkYHJ49fsZdtoJ8EQ8ibpwk5rPKfvNIwChB0KVXcIjcepEBBd2VHC5r9Gy8ueg==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -460,70 +514,234 @@ "@babel/helper-plugin-utils": "^7.14.5" } }, + "@babel/plugin-syntax-typescript": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz", + "integrity": "sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, "@babel/template": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz", - "integrity": "sha512-UgBAfEa1oGuYgDIPM2G+aHa4Nlo9Lh6mGD2bDBGMTbYnc38vulXPuC1MGjYILIEmlwl6Rd+BPR9ee3gm20CBtg==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", "dev": true, "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4" + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "@babel/highlight": "^7.14.5" + "has-flag": "^3.0.0" } } } }, "@babel/traverse": { - "version": "7.15.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.4.tgz", - "integrity": "sha512-W6lQD8l4rUbQR/vYgSuCAE75ADyyQvOpFVsvPPdkhf6lATXAsQIG9YdtOcu8BB1dZ0LKu+Zo3c1wEcbKeuhdlA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.15.4", - "@babel/helper-function-name": "^7.15.4", - "@babel/helper-hoist-variables": "^7.15.4", - "@babel/helper-split-export-declaration": "^7.15.4", - "@babel/parser": "^7.15.4", - "@babel/types": "^7.15.4", + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.9.tgz", + "integrity": "sha512-PQO8sDIJ8SIwipTPiR71kJQCKQYB5NGImbOviK8K+kg5xkNSYXLBupuX9QhatFowrsvo9Hj8WgArg3W7ijNAQw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.9", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.9", + "@babel/types": "^7.17.0", "debug": "^4.1.0", "globals": "^11.1.0" }, "dependencies": { "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { - "@babel/highlight": "^7.14.5" + "color-name": "1.1.3" } }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, "@babel/types": { - "version": "7.15.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.6.tgz", - "integrity": "sha512-BPU+7QhqNjmWyDO0/vitH/CuhpV8ZmK1wpKva8nuyNF5MJfuRNWMc+hc14+u9xT93kvykMdncrJT19h74uB1Ig==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.14.9", + "@babel/helper-validator-identifier": "^7.16.7", "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + } } }, "@bcoe/v8-coverage": { @@ -532,16 +750,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", - "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - } - }, "@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", @@ -627,221 +835,320 @@ "dev": true }, "@jest/console": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", - "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-27.5.1.tgz", + "integrity": "sha512-kZ/tNpS3NXn0mlXXXPNuDZnb4c0oZ20r4K5eemM2k30ZC3G0T02nXUvyhf5YdbXWHPEJLc9qGLxEZ216MdL+Zg==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@jest/types": "^27.5.1", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^26.6.2", - "jest-util": "^26.6.2", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", "slash": "^3.0.0" } }, "@jest/core": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", - "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-27.5.1.tgz", + "integrity": "sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==", "dev": true, "requires": { - "@jest/console": "^26.6.2", - "@jest/reporters": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/console": "^27.5.1", + "@jest/reporters": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", + "emittery": "^0.8.1", "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^26.6.2", - "jest-config": "^26.6.3", - "jest-haste-map": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-resolve-dependencies": "^26.6.3", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "jest-watcher": "^26.6.2", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^27.5.1", + "jest-config": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-resolve-dependencies": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "jest-watcher": "^27.5.1", + "micromatch": "^4.0.4", "rimraf": "^3.0.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "@jest/environment": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", - "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", + "integrity": "sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==", "dev": true, "requires": { - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", "@types/node": "*", - "jest-mock": "^26.6.2" + "jest-mock": "^27.5.1" } }, "@jest/fake-timers": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", - "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", + "integrity": "sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "@sinonjs/fake-timers": "^6.0.1", + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", "@types/node": "*", - "jest-message-util": "^26.6.2", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2" + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" } }, "@jest/globals": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", - "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-27.5.1.tgz", + "integrity": "sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==", "dev": true, "requires": { - "@jest/environment": "^26.6.2", - "@jest/types": "^26.6.2", - "expect": "^26.6.2" + "@jest/environment": "^27.5.1", + "@jest/types": "^27.5.1", + "expect": "^27.5.1" } }, "@jest/reporters": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", - "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", + "integrity": "sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/console": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.2", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-instrument": "^5.1.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^26.6.2", - "jest-resolve": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "node-notifier": "^8.0.0", + "istanbul-reports": "^3.1.3", + "jest-haste-map": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", "slash": "^3.0.0", "source-map": "^0.6.0", "string-length": "^4.0.1", "terminal-link": "^2.0.0", - "v8-to-istanbul": "^7.0.0" + "v8-to-istanbul": "^8.1.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "@jest/source-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", - "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-27.5.1.tgz", + "integrity": "sha512-y9NIHUYF3PJRlHk98NdC/N1gl88BL08aQQgu4k4ZopQkCw9t9cV8mtl3TV8b/YCB8XaVTFrmUTAJvjsntDireg==", "dev": true, "requires": { "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.9", "source-map": "^0.6.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "@jest/test-result": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", - "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-27.5.1.tgz", + "integrity": "sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==", "dev": true, "requires": { - "@jest/console": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/console": "^27.5.1", + "@jest/types": "^27.5.1", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "@jest/test-sequencer": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", - "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.5.1.tgz", + "integrity": "sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==", "dev": true, "requires": { - "@jest/test-result": "^26.6.2", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3" + "@jest/test-result": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-runtime": "^27.5.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "@jest/transform": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", - "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-27.5.1.tgz", + "integrity": "sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==", "dev": true, "requires": { "@babel/core": "^7.1.0", - "@jest/types": "^26.6.2", - "babel-plugin-istanbul": "^6.0.0", + "@jest/types": "^27.5.1", + "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^1.4.0", "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-util": "^26.6.2", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-util": "^27.5.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", "slash": "^3.0.0", "source-map": "^0.6.1", "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", - "@types/yargs": "^15.0.0", + "@types/yargs": "^16.0.0", "chalk": "^4.0.0" } }, - "@matrixai/async-init": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.6.0.tgz", - "integrity": "sha512-I24u6McZnSH2yX1l5e2H3O/Lu8IVb2fM/sVbDeRYrzejV2XLv/9g/goz2fglSrXgJ877BBFJNW2GMxVzvvyA5A==", + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, "requires": { - "async-mutex": "^0.3.2", - "ts-custom-error": "^3.2.0" + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.6.tgz", + "integrity": "sha512-R7xHtBSNm+9SyvpJkdQl+qrM3Hm2fea3Ef197M3mUug+v+yR+Rhfbs7PBtcBUVnIWJ4JcAdjvij+c8hXS9p5aw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.0.tgz", + "integrity": "sha512-SfJxIxNVYLTsKwzB3MoOQ1yxf4w/E6MdkvTgrgAt1bfxjSrLUoHMKrDOykwN14q65waezZIdqDneUIPh4/sKxg==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@matrixai/async-init": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.7.1.tgz", + "integrity": "sha512-3ELRuEn6AoC3e0b4QccA+NxZl0LilyjYeu6a3Pf9VUVB89EepnNKsk/Afb9qa+B5LvLQwmgHtg4r9VwRpQpnQA==", + "requires": { + "@matrixai/async-locks": "^2.2.0", + "@matrixai/errors": "^1.0.1" + } + }, + "@matrixai/async-locks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-locks/-/async-locks-2.2.0.tgz", + "integrity": "sha512-i0a551EUMhD1WKpaAyhBUt83ckbOxPB4MlOH8VDzeDyPMAtLI9egCqf3HWOvhN0rSjoH97EmzmfxyAeAgiQzTw==", + "requires": { + "@matrixai/errors": "^1.0.1", + "@matrixai/resources": "^1.0.0", + "async-mutex": "^0.3.2" } }, "@matrixai/db": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@matrixai/db/-/db-1.1.5.tgz", - "integrity": "sha512-zPpP/J1A3TLRaQKaGa5smualzjW4Rin4K48cpU5/9ThyXfpVBBp/mrkbDfjL/O5z6YTcuGVf2+yLck8tF8kVUw==", - "requires": { - "@matrixai/async-init": "^1.6.0", - "@matrixai/logger": "^2.0.1", - "@matrixai/workers": "^1.2.3", - "abstract-leveldown": "^7.0.0", - "async-mutex": "^0.3.1", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@matrixai/db/-/db-3.3.0.tgz", + "integrity": "sha512-SNKXhHtTECygRsGpSPei6CYStoneqQ8rt6qqnCnP9wDvB1r1rLTteKYxxKjzoNTK6ZZfrXU28RL9d3YlOuSTWw==", + "requires": { + "@matrixai/async-init": "^1.7.0", + "@matrixai/errors": "^1.0.1", + "@matrixai/logger": "^2.1.0", + "@matrixai/resources": "^1.0.0", + "@matrixai/workers": "^1.3.0", + "@types/abstract-leveldown": "^7.2.0", "level": "7.0.1", - "levelup": "^5.0.1", - "sublevel-prefixer": "^1.0.0", - "subleveldown": "^5.0.1", - "threads": "^1.6.5", + "threads": "^1.6.5" + }, + "dependencies": { + "@types/abstract-leveldown": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-7.2.0.tgz", + "integrity": "sha512-q5veSX6zjUy/DlDhR4Y4cU0k2Ar+DT2LUraP00T19WLmTO6Se1djepCCaqU6nQrwcJ5Hyo/CWqxTzrrFg8eqbQ==" + } + } + }, + "@matrixai/errors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@matrixai/errors/-/errors-1.1.0.tgz", + "integrity": "sha512-Vi98yQxj35Qs0hcooA15P/7G5TrcJPp2kIfTpj0o/OGiCrXlaj+3npCIjN6mMQSYSE8R3ycXmYq+bB2VXl0BzA==", + "requires": { "ts-custom-error": "^3.2.0" } }, @@ -850,21 +1157,20 @@ "resolved": "https://registry.npmjs.org/@matrixai/logger/-/logger-2.1.0.tgz", "integrity": "sha512-UmLuXi2PJ03v0Scfl57217RPnjEZDRLlpfdIjIwCfju+kofnhhCI9P7OZu3/FgW147vbvSzWCrrtpwJiLROUUA==" }, + "@matrixai/resources": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@matrixai/resources/-/resources-1.1.1.tgz", + "integrity": "sha512-50lyw+sygw3jBg6Wr9TaYYInGP2vKS7wDQPXS5ucH9zG2u5LQL+sc3+zBJ69X4VIDMSoGK8s9w0Hq+h60nZkTw==" + }, "@matrixai/workers": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@matrixai/workers/-/workers-1.2.5.tgz", - "integrity": "sha512-ikI4K6RGKQbG68it7TXJJ5wX2csW+WpokUehTnz5r66d7o6FC3PkojE46LPLCDSwk3NVCGoQ743OZS2nuA8SRA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@matrixai/workers/-/workers-1.3.1.tgz", + "integrity": "sha512-AXI37aZ7gSqS5PpUCG7/yfNcnWNYiQUp4yFBY1Z9lKk0zrkQzND1zPjDuJ2E7t6E1a4rwa8vtysj5TVeMCA8jw==", "requires": { + "@matrixai/async-init": "^1.7.0", + "@matrixai/errors": "^1.0.1", "@matrixai/logger": "^2.1.0", - "threads": "^1.6.5", - "ts-custom-error": "^3.2.0" - }, - "dependencies": { - "@matrixai/logger": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@matrixai/logger/-/logger-2.1.0.tgz", - "integrity": "sha512-UmLuXi2PJ03v0Scfl57217RPnjEZDRLlpfdIjIwCfju+kofnhhCI9P7OZu3/FgW147vbvSzWCrrtpwJiLROUUA==" - } + "threads": "^1.6.5" } }, "@nodelib/fs.scandir": { @@ -903,9 +1209,9 @@ } }, "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" @@ -941,16 +1247,10 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, - "@types/abstract-leveldown": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/abstract-leveldown/-/abstract-leveldown-5.0.2.tgz", - "integrity": "sha512-+jA1XXF3jsz+Z7FcuiNqgK53hTa/luglT2TyTpKPqoYbxVY+mCPF22Rm+q3KPBrMHJwNXFrTViHszBOfU4vftQ==", - "dev": true - }, "@types/babel__core": { - "version": "7.1.16", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz", - "integrity": "sha512-EAEHtisTMM+KaKwfWdC3oyllIqswlznXCIVCt7/oRNrh+DhgT4UEBNC/jlADNjvw7UnfbcdkGQcPVZ1xYiLcrQ==", + "version": "7.1.19", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", + "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", "dev": true, "requires": { "@babel/parser": "^7.1.0", @@ -961,9 +1261,9 @@ } }, "@types/babel__generator": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.3.tgz", - "integrity": "sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA==", + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", "dev": true, "requires": { "@babel/types": "^7.0.0" @@ -980,24 +1280,14 @@ } }, "@types/babel__traverse": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz", - "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==", + "version": "7.17.1", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.17.1.tgz", + "integrity": "sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==", "dev": true, "requires": { "@babel/types": "^7.3.0" } }, - "@types/encoding-down": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/encoding-down/-/encoding-down-5.0.0.tgz", - "integrity": "sha512-G0MlS/+/U2RIQLcSEhhAcoMrXw3hXUCFSKbhbeEljoKMra2kq+NPX6tfOveSWQLX2hJXBo+YrvKgAGe+tFL1Aw==", - "dev": true, - "requires": { - "@types/abstract-leveldown": "*", - "@types/level-codec": "*" - } - }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -1008,9 +1298,9 @@ } }, "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", "dev": true }, "@types/istanbul-lib-report": { @@ -1032,13 +1322,13 @@ } }, "@types/jest": { - "version": "26.0.24", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz", - "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==", + "version": "27.4.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.4.1.tgz", + "integrity": "sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==", "dev": true, "requires": { - "jest-diff": "^26.0.0", - "pretty-format": "^26.0.0" + "jest-matcher-utils": "^27.0.0", + "pretty-format": "^27.0.0" } }, "@types/json-schema": { @@ -1053,40 +1343,6 @@ "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "dev": true }, - "@types/level": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/level/-/level-6.0.0.tgz", - "integrity": "sha512-NjaUpukKfCvnV4Wk0jUaodFi2/66HxgpYghc2aV8iP+zk2NMt/9ps1eVlifqOU/+eLzMlDIY69NWkbPaAstukQ==", - "dev": true, - "requires": { - "@types/abstract-leveldown": "*", - "@types/encoding-down": "*", - "@types/levelup": "*" - } - }, - "@types/level-codec": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/level-codec/-/level-codec-9.0.1.tgz", - "integrity": "sha512-6z7DSlBsmbax3I/bV1Q6jT1nKquDjFl95LURVThdKtwILkRawLYtXdINW19xM95N5kqN2detWb2iGrbUlPwNyw==", - "dev": true - }, - "@types/level-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/level-errors/-/level-errors-3.0.0.tgz", - "integrity": "sha512-/lMtoq/Cf/2DVOm6zE6ORyOM+3ZVm/BvzEZVxUhf6bgh8ZHglXlBqxbxSlJeVp8FCbD3IVvk/VbsaNmDjrQvqQ==", - "dev": true - }, - "@types/levelup": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/levelup/-/levelup-4.3.3.tgz", - "integrity": "sha512-K+OTIjJcZHVlZQN1HmU64VtrC0jC3dXWQozuEIR9zVvltIk90zaGPM2AgT+fIkChpzHhFE3YnvFLCbLtzAmexA==", - "dev": true, - "requires": { - "@types/abstract-leveldown": "*", - "@types/level-errors": "*", - "@types/node": "*" - } - }, "@types/node": { "version": "14.17.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.16.tgz", @@ -1094,24 +1350,18 @@ "dev": true }, "@types/node-forge": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.9.10.tgz", - "integrity": "sha512-+BbPlhZeYs/WETWftQi2LeRx9VviWSwawNo+Pid5qNrSZHb60loYjpph3OrbwXMMseadu9rE9NeK34r4BHT+QQ==", + "version": "0.10.10", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-0.10.10.tgz", + "integrity": "sha512-iixn5bedlE9fm/5mN7fPpXraXlxCVrnNWHZekys8c5fknridLVWGnNRqlaWpenwaijIuB3bNI0lEOm+JD6hZUA==", "dev": true, "requires": { "@types/node": "*" } }, - "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true - }, "@types/prettier": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.3.2.tgz", - "integrity": "sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.0.tgz", + "integrity": "sha512-G/AdOadiZhnJp0jXCaBQU449W2h716OW/EoXeYkCytxKL06X1WCXB4DZpp8TpZ8eyIJVS1cw4lrlkkSYU21cDw==", "dev": true }, "@types/readable-stream": { @@ -1130,30 +1380,19 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, - "@types/subleveldown": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@types/subleveldown/-/subleveldown-4.1.1.tgz", - "integrity": "sha512-cnup7ibJZjwHIwDATB9hdtvfM3j6hkRquBz/Ld/PJC50r1u7V+bXhm+tof665A2D56shExosPqybbET770GBEg==", - "dev": true, - "requires": { - "@types/abstract-leveldown": "*", - "@types/level-codec": "*", - "@types/levelup": "*" - } - }, "@types/yargs": { - "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", + "integrity": "sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==", "dev": true, "requires": { "@types/yargs-parser": "*" } }, "@types/yargs-parser": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", - "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, "@typescript-eslint/eslint-plugin": { @@ -1271,9 +1510,9 @@ } }, "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, "abstract-leveldown": { @@ -1401,24 +1640,6 @@ "sprintf-js": "~1.0.2" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, "array-includes": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz", @@ -1468,12 +1689,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, "array.prototype.flat": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz", @@ -1515,12 +1730,6 @@ } } }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1547,45 +1756,47 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, "babel-jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", - "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz", + "integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==", "dev": true, "requires": { - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^26.6.2", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^27.5.1", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.9", "slash": "^3.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-instrument": "^5.0.4", "test-exclude": "^6.0.0" } }, "babel-plugin-jest-hoist": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", - "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz", + "integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==", "dev": true, "requires": { "@babel/template": "^7.3.3", @@ -1615,12 +1826,12 @@ } }, "babel-preset-jest": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", - "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz", + "integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==", "dev": true, "requires": { - "babel-plugin-jest-hoist": "^26.6.2", + "babel-plugin-jest-hoist": "^27.5.1", "babel-preset-current-node-syntax": "^1.0.0" } }, @@ -1639,61 +1850,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1766,16 +1922,16 @@ "dev": true }, "browserslist": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.0.tgz", - "integrity": "sha512-g2BJ2a0nEYvEFQC208q8mVAhfNwpZ5Mu8BwgtCdZKO3qx98HChmeg448fPdUzld8aFmfLgVh7yymqV+q1lJZ5g==", + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.3.tgz", + "integrity": "sha512-NBhymBQl1zM0Y5dQT/O+xiLP9/rzOIQdKM/eMJBAq7yBgaB6krIYLGejrwVYnSHZdqjscB1SPuAjHwxjvN6Wdg==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001254", - "colorette": "^1.3.0", - "electron-to-chromium": "^1.3.830", + "caniuse-lite": "^1.0.30001332", + "electron-to-chromium": "^1.4.118", "escalade": "^3.1.1", - "node-releases": "^1.1.75" + "node-releases": "^2.0.3", + "picocolors": "^1.0.0" } }, "bs-logger": { @@ -1811,23 +1967,6 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -1849,27 +1988,15 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001257", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001257.tgz", - "integrity": "sha512-JN49KplOgHSXpIsVSF+LUyhD8PUp6xPpAXeRrrcBh4KBeP7W864jHn6RvzJgDlrReyeVjMFJL3PLpPvKIxlIHA==", + "version": "1.0.30001334", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001334.tgz", + "integrity": "sha512-kbaCEBRRVSoeNs74sCuq92MJyGrMtjWVfhltoHUCW4t4pXFvGjUBrfo47weBRViHkiV3eBYyIsfl956NtHGazw==", "dev": true }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "dev": true, - "requires": { - "rsvp": "^4.8.4" - } - }, "catering": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.0.tgz", - "integrity": "sha512-M5imwzQn6y+ODBfgi+cfgZv2hIUI6oYU/0f35Mdb1ujGeqeoI5tOnl9Q13DTH7LW+7er+NYq8stNOKZD/Z3U/A==", - "requires": { - "queue-tick": "^1.0.0" - } + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/catering/-/catering-2.1.1.tgz", + "integrity": "sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==" }, "chalk": { "version": "4.1.2", @@ -1888,40 +2015,17 @@ "dev": true }, "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz", + "integrity": "sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==", "dev": true }, "cjs-module-lexer": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", - "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -1932,14 +2036,27 @@ } }, "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "wrap-ansi": "^7.0.0" + }, + "dependencies": { + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } } }, "co": { @@ -1954,16 +2071,6 @@ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", "dev": true }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1979,12 +2086,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "colorette": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", - "dev": true - }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2000,12 +2101,6 @@ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", "dev": true }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2029,12 +2124,6 @@ } } }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, "core-js": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", @@ -2099,22 +2188,16 @@ "ms": "2.1.2" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, "decimal.js": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", "dev": true }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, "deep-is": { @@ -2146,52 +2229,6 @@ "object-keys": "^1.0.12" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "defined": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", - "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=" - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2211,9 +2248,9 @@ "dev": true }, "diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.5.1.tgz", + "integrity": "sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==", "dev": true }, "dir-glob": { @@ -2252,15 +2289,15 @@ } }, "electron-to-chromium": { - "version": "1.3.839", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.839.tgz", - "integrity": "sha512-0O7uPs9LJNjQ/U5mW78qW8gXv9H6Ba3DHZ5/yt8aBsvomOWDkV3MddT7enUYvLQEUVOURjWmgJJWVZ3K98tIwQ==", + "version": "1.4.124", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.124.tgz", + "integrity": "sha512-VhaE9VUYU6d2eIb+4xf83CATD+T+3bTzvxvlADkQE+c2hisiw3sZmvEDtsW704+Zky9WZGhBuQXijDVqSriQLA==", "dev": true }, "emittery": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", - "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz", + "integrity": "sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg==", "dev": true }, "emoji-regex": { @@ -2280,15 +2317,6 @@ "level-errors": "^3.0.0" } }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, "enquirer": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", @@ -2376,9 +2404,9 @@ }, "dependencies": { "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true }, "levn": { @@ -2795,76 +2823,21 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, - "exec-sh": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", - "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", - "dev": true - }, "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" } }, "exit": { @@ -2873,154 +2846,16 @@ "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", "dev": true }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "expect": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-27.5.1.tgz", + "integrity": "sha512-E1q5hSUG2AmYQwQJ041nvgpkODHQvB+RKlB4IYdru6uJsyFTRyZAP463M+1lINorwbqAmUggi6+WwkD8lCS/Dw==", "dev": true, "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "expect": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-styles": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0" - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } + "@jest/types": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1" } }, "fast-deep-equal": { @@ -3122,12 +2957,6 @@ "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", "dev": true }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, "form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", @@ -3139,15 +2968,6 @@ "mime-types": "^2.1.12" } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -3221,13 +3041,10 @@ "dev": true }, "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true }, "get-symbol-description": { "version": "1.0.0", @@ -3238,12 +3055,6 @@ "get-intrinsic": "^1.1.1" } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -3296,26 +3107,6 @@ "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", "dev": true }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true, - "optional": true - }, - "handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", - "dev": true, - "requires": { - "minimist": "^1.2.5", - "neo-async": "^2.6.0", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4", - "wordwrap": "^1.0.0" - } - }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3348,70 +3139,6 @@ "has-symbols": "^1.0.2" } }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, "html-encoding-sniffer": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", @@ -3439,9 +3166,9 @@ } }, "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "requires": { "agent-base": "6", @@ -3449,9 +3176,9 @@ } }, "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, "iconv-lite": { @@ -3474,11 +3201,6 @@ "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true }, - "immediate": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", - "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==" - }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -3490,9 +3212,9 @@ } }, "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, "requires": { "pkg-dir": "^4.2.0", @@ -3530,32 +3252,6 @@ "side-channel": "^1.0.4" } }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3589,15 +3285,6 @@ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, "is-core-module": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", @@ -3607,32 +3294,6 @@ "has": "^1.0.3" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -3641,38 +3302,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "optional": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3724,15 +3353,6 @@ "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==" }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, "is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -3755,9 +3375,9 @@ "dev": true }, "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true }, "is-string": { @@ -3791,55 +3411,28 @@ "call-bind": "^1.0.2" } }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "optional": true, - "requires": { - "is-docker": "^2.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true }, "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz", + "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==", "dev": true, "requires": { - "@babel/core": "^7.7.5", + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" }, "dependencies": { @@ -3863,9 +3456,9 @@ } }, "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, "requires": { "debug": "^4.1.1", @@ -3874,9 +3467,9 @@ } }, "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", + "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -3884,481 +3477,645 @@ } }, "jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", - "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", + "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "dev": true, "requires": { - "@jest/core": "^26.6.3", + "@jest/core": "^27.5.1", "import-local": "^3.0.2", - "jest-cli": "^26.6.3" + "jest-cli": "^27.5.1" }, "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, "jest-cli": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", - "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-27.5.1.tgz", + "integrity": "sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==", "dev": true, "requires": { - "@jest/core": "^26.6.3", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/core": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", "chalk": "^4.0.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.4", + "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^26.6.3", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", + "jest-config": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", "prompts": "^2.0.1", - "yargs": "^15.4.1" + "yargs": "^16.2.0" } } } }, "jest-changed-files": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", - "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz", + "integrity": "sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "execa": "^4.0.0", - "throat": "^5.0.0" - }, - "dependencies": { - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - } + "@jest/types": "^27.5.1", + "execa": "^5.0.0", + "throat": "^6.0.1" + } + }, + "jest-circus": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-27.5.1.tgz", + "integrity": "sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==", + "dev": true, + "requires": { + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.5.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" } }, "jest-config": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", - "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-27.5.1.tgz", + "integrity": "sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==", "dev": true, "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^26.6.3", - "@jest/types": "^26.6.2", - "babel-jest": "^26.6.3", + "@babel/core": "^7.8.0", + "@jest/test-sequencer": "^27.5.1", + "@jest/types": "^27.5.1", + "babel-jest": "^27.5.1", "chalk": "^4.0.0", + "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^26.6.2", - "jest-environment-node": "^26.6.2", - "jest-get-type": "^26.3.0", - "jest-jasmine2": "^26.6.3", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2" + "graceful-fs": "^4.2.9", + "jest-circus": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-jasmine2": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runner": "^27.5.1", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^27.5.1", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", + "integrity": "sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "diff-sequences": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" } }, "jest-docblock": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", - "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.5.1.tgz", + "integrity": "sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==", "dev": true, "requires": { "detect-newline": "^3.0.0" } }, "jest-each": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", - "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-27.5.1.tgz", + "integrity": "sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@jest/types": "^27.5.1", "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2" + "jest-get-type": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1" } }, "jest-environment-jsdom": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", - "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", "dev": true, "requires": { - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", "@types/node": "*", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2", - "jsdom": "^16.4.0" + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" } }, "jest-environment-node": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", - "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", + "integrity": "sha512-Jt4ZUnxdOsTGwSRAfKEnE6BcwsSPNOijjwifq5sDFSA2kesnXTvNqKHYgM0hDq3549Uf/KzdXNYn4wMZJPlFLw==", "dev": true, "requires": { - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", "@types/node": "*", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2" + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" } }, "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.5.1.tgz", + "integrity": "sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==", "dev": true }, "jest-haste-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", - "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.5.1.tgz", + "integrity": "sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@jest/types": "^27.5.1", "@types/graceful-fs": "^4.1.2", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-regex-util": "^26.0.0", - "jest-serializer": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "micromatch": "^4.0.2", - "sane": "^4.0.3", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^27.5.1", + "jest-serializer": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", + "micromatch": "^4.0.4", "walker": "^1.0.7" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "jest-jasmine2": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", - "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.5.1.tgz", + "integrity": "sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==", "dev": true, "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^26.6.2", - "@jest/source-map": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/environment": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", - "expect": "^26.6.2", + "expect": "^27.5.1", "is-generator-fn": "^2.0.0", - "jest-each": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2", - "throat": "^5.0.0" + "jest-each": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", + "pretty-format": "^27.5.1", + "throat": "^6.0.1" } }, "jest-leak-detector": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", - "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.5.1.tgz", + "integrity": "sha512-POXfWAMvfU6WMUXftV4HolnJfnPOGEu10fscNCA76KBpRRhcMN2c8d3iT2pxQS3HLbA+5X4sOUPzYO2NUyIlHQ==", "dev": true, "requires": { - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" } }, "jest-matcher-utils": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz", + "integrity": "sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==", "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "pretty-format": "^27.5.1" } }, "jest-message-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.5.1.tgz", + "integrity": "sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^26.6.2", + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.5.1", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^27.5.1", "slash": "^3.0.0", - "stack-utils": "^2.0.2" - } - }, - "jest-mock": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", - "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*" - } - }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true - }, - "jest-regex-util": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true - }, - "jest-resolve": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", - "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^26.6.2", - "read-pkg-up": "^7.0.1", - "resolve": "^1.18.1", + "stack-utils": "^2.0.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.9.tgz", + "integrity": "sha512-J9PfEKCbFIv2X5bjTMiZu6Vf341N05QIY+d6FvVKynkG1S7G0j3I0QoRtWIrXhZ+/Nlb5Q0MzqL7TokEJ5BNHg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "jest-mock": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-27.5.1.tgz", + "integrity": "sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "@types/node": "*" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true + }, + "jest-regex-util": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.5.1.tgz", + "integrity": "sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==", + "dev": true + }, + "jest-resolve": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.5.1.tgz", + "integrity": "sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==", + "dev": true, + "requires": { + "@jest/types": "^27.5.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.5.1", + "jest-validate": "^27.5.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", "slash": "^3.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "jest-resolve-dependencies": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", - "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.5.1.tgz", + "integrity": "sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-snapshot": "^26.6.2" + "@jest/types": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-snapshot": "^27.5.1" } }, "jest-runner": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", - "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-27.5.1.tgz", + "integrity": "sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==", "dev": true, "requires": { - "@jest/console": "^26.6.2", - "@jest/environment": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/console": "^27.5.1", + "@jest/environment": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.7.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-config": "^26.6.3", - "jest-docblock": "^26.0.0", - "jest-haste-map": "^26.6.2", - "jest-leak-detector": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-resolve": "^26.6.2", - "jest-runtime": "^26.6.3", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", + "emittery": "^0.8.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^27.5.1", + "jest-environment-jsdom": "^27.5.1", + "jest-environment-node": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-leak-detector": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-runtime": "^27.5.1", + "jest-util": "^27.5.1", + "jest-worker": "^27.5.1", "source-map-support": "^0.5.6", - "throat": "^5.0.0" + "throat": "^6.0.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "jest-runtime": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", - "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", - "dev": true, - "requires": { - "@jest/console": "^26.6.2", - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/globals": "^26.6.2", - "@jest/source-map": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/yargs": "^15.0.0", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", + "integrity": "sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==", + "dev": true, + "requires": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/globals": "^27.5.1", + "@jest/source-map": "^27.5.1", + "@jest/test-result": "^27.5.1", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", "chalk": "^4.0.0", - "cjs-module-lexer": "^0.6.0", + "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", + "execa": "^5.0.0", "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-config": "^26.6.3", - "jest-haste-map": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-mock": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-regex-util": "^27.5.1", + "jest-resolve": "^27.5.1", + "jest-snapshot": "^27.5.1", + "jest-util": "^27.5.1", "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.4.1" + "strip-bom": "^4.0.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "jest-serializer": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", - "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.5.1.tgz", + "integrity": "sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==", "dev": true, "requires": { "@types/node": "*", - "graceful-fs": "^4.2.4" + "graceful-fs": "^4.2.9" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "jest-snapshot": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", - "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.5.1.tgz", + "integrity": "sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==", "dev": true, "requires": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", "@babel/types": "^7.0.0", - "@jest/types": "^26.6.2", + "@jest/transform": "^27.5.1", + "@jest/types": "^27.5.1", "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.0.0", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^26.6.2", - "graceful-fs": "^4.2.4", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "jest-haste-map": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-resolve": "^26.6.2", + "expect": "^27.5.1", + "graceful-fs": "^4.2.9", + "jest-diff": "^27.5.1", + "jest-get-type": "^27.5.1", + "jest-haste-map": "^27.5.1", + "jest-matcher-utils": "^27.5.1", + "jest-message-util": "^27.5.1", + "jest-util": "^27.5.1", "natural-compare": "^1.4.0", - "pretty-format": "^26.6.2", + "pretty-format": "^27.5.1", "semver": "^7.3.2" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-27.5.1.tgz", + "integrity": "sha512-Kv2o/8jNvX1MQ0KGtw480E/w4fBCDOnH6+6DmeKi6LZUIlKA5kwY0YNdlzaWTiVgxqAqik11QyxDOKk543aKXw==", "dev": true, "requires": { - "@jest/types": "^26.6.2", + "@jest/types": "^27.5.1", "@types/node": "*", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + } } }, "jest-validate": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", - "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-27.5.1.tgz", + "integrity": "sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "camelcase": "^6.0.0", + "@jest/types": "^27.5.1", + "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", + "jest-get-type": "^27.5.1", "leven": "^3.1.0", - "pretty-format": "^26.6.2" + "pretty-format": "^27.5.1" }, "dependencies": { "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true } } }, "jest-watcher": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", - "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.5.1.tgz", + "integrity": "sha512-z676SuD6Z8o8qbmEGhoEUFOM1+jfEiL3DXHK/xgEiG2EyNYfFG60jluWcupY6dATjfEsKQuibReS1djInQnoVw==", "dev": true, "requires": { - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", + "@jest/test-result": "^27.5.1", + "@jest/types": "^27.5.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "jest-util": "^26.6.2", + "jest-util": "^27.5.1", "string-length": "^4.0.1" } }, "jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "requires": { "@types/node": "*", "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "js-tokens": { @@ -4413,9 +4170,9 @@ }, "dependencies": { "acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true } } @@ -4456,13 +4213,10 @@ } }, "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true }, "jsonc-parser": { "version": "3.0.0", @@ -4494,12 +4248,6 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -4558,14 +4306,6 @@ "run-parallel-limit": "^1.1.0" } }, - "level-option-wrap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/level-option-wrap/-/level-option-wrap-1.1.0.tgz", - "integrity": "sha1-rSDmjZ88IsiJdTHMaqevWWse0Sk=", - "requires": { - "defined": "~0.0.0" - } - }, "level-packager": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/level-packager/-/level-packager-6.0.1.tgz", @@ -4581,9 +4321,9 @@ "integrity": "sha512-E486g1NCjW5cF78KGPrMDRBYzPuueMZ6VBXHT6gC7A8UYWGiM14fGgp+s/L1oFfDWSPV/+SFkYCmZ0SiESkRKA==" }, "leveldown": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-6.1.0.tgz", - "integrity": "sha512-8C7oJDT44JXxh04aSSsfcMI8YiaGRhOFI9/pMEL7nWJLVsWajDPTRxsSHTM2WcTVY5nXM+SuRHzPPi0GbnDX+w==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/leveldown/-/leveldown-6.1.1.tgz", + "integrity": "sha512-88c+E+Eizn4CkQOBHwqlCJaTNEjGpaEIikn1S+cINc5E9HEvJ77bqY4JY/HxT5u0caWqsc3P3DcFIKBI1vHt+A==", "requires": { "abstract-leveldown": "^7.2.0", "napi-macros": "~2.0.0", @@ -4625,9 +4365,9 @@ "integrity": "sha1-UsptmYpXLmMitRX1uA45bGBD6bg=" }, "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, "locate-path": { @@ -4657,6 +4397,12 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4725,33 +4471,18 @@ "dev": true }, "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "dev": true, - "requires": { - "tmpl": "1.0.x" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, "requires": { - "object-visit": "^1.0.0" + "tmpl": "1.0.5" } }, "marked": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.4.tgz", - "integrity": "sha512-jBo8AOayNaEcvBhNobg6/BLhdsK3NvnKWJg33MAAPbvTWiG4QBn9gpW1+7RssrKu4K1dKlN+0goVQwV41xEfOA==", + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.14.tgz", + "integrity": "sha512-HL5sSPE/LP6U9qKgngIIPTthuxC0jrfxpYMZ3LdGDD3vTnLs59m2Z7r6+LNDR3ToqEQdkKd6YaaEfJhodJmijQ==", "dev": true }, "merge-stream": { @@ -4777,18 +4508,18 @@ } }, "mime-db": { - "version": "1.49.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz", - "integrity": "sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true }, "mime-types": { - "version": "2.1.32", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.32.tgz", - "integrity": "sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "requires": { - "mime-db": "1.49.0" + "mime-db": "1.52.0" } }, "mimic-fn": { @@ -4812,57 +4543,11 @@ "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, "napi-macros": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", @@ -4874,27 +4559,15 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "node-forge": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" }, "node-gyp-build": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==" + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.4.0.tgz", + "integrity": "sha512-amJnQCcgtRVw9SvoebO3BKGESClrfXGCUTX9hSn1OuGQTQBOZmVd0Z0OlecpuRksKvbsUqALE8jls/ErClAPuQ==" }, "node-int64": { "version": "0.4.0", @@ -4902,53 +4575,12 @@ "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", "dev": true }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-notifier": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", - "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", - "dev": true, - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.2.0", - "semver": "^7.3.2", - "shellwords": "^0.1.1", - "uuid": "^8.3.0", - "which": "^2.0.2" - } - }, "node-releases": { - "version": "1.1.75", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", - "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.3.tgz", + "integrity": "sha512-maHFz6OLqYxz+VQyCAtA3PTX4UP/53pa05fyDNc9CwjvJ0yEh6+xBwKsgCxMNhS8taUKBFYxfuiaD9U/55iFaw==", "dev": true }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4956,20 +4588,12 @@ "dev": true }, "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "requires": { - "path-key": "^2.0.0" - }, - "dependencies": { - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - } + "path-key": "^3.0.0" } }, "nwsapi": { @@ -4978,43 +4602,6 @@ "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", "dev": true }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "object-inspect": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", @@ -5025,15 +4612,6 @@ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, "object.assign": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", @@ -5055,15 +4633,6 @@ "es-abstract": "^1.18.0-next.2" } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, "object.values": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", @@ -5128,32 +4697,6 @@ "mimic-fn": "^2.1.0" } }, - "onigasm": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.5.tgz", - "integrity": "sha512-F+th54mPc0l1lp1ZcFMyL/jTs2Tlq4SqIHKIXGZOR/VkHkF9A7Fr5rRr5+ZG/lWeRsyrClLYRq7s/yFQ/XhWCA==", - "dev": true, - "requires": { - "lru-cache": "^5.1.1" - }, - "dependencies": { - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } - } - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -5168,18 +4711,6 @@ "word-wrap": "^1.2.3" } }, - "p-each-series": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", - "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -5231,12 +4762,6 @@ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5267,6 +4792,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", @@ -5274,13 +4805,10 @@ "dev": true }, "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true }, "pkg-dir": { "version": "4.2.0", @@ -5297,12 +4825,6 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "dev": true }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5325,15 +4847,22 @@ } }, "pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "requires": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } } }, "progress": { @@ -5343,9 +4872,9 @@ "dev": true }, "prompts": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", - "integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, "requires": { "kleur": "^3.0.3", @@ -5363,16 +4892,6 @@ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -5384,61 +4903,12 @@ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" }, - "queue-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.0.tgz", - "integrity": "sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ==" - }, - "reachdown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reachdown/-/reachdown-1.1.0.tgz", - "integrity": "sha512-6LsdRe4cZyOjw4NnvbhUd/rGG7WQ9HMopPr+kyL018Uci4kijtxcGR5kVb5Ln13k4PEE+fEFQbjfOvNw7cnXmA==" - }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -5454,40 +4924,12 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, "regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5500,12 +4942,6 @@ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, "resolve": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", @@ -5539,10 +4975,10 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "resolve.exports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", + "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", "dev": true }, "resource-counter": { @@ -5564,12 +5000,6 @@ "signal-exit": "^3.0.2" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5585,12 +5015,6 @@ "glob": "^7.1.3" } }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5613,495 +5037,109 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", - "dev": true, - "requires": { - "xmlchars": "^2.2.0" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", - "dev": true, - "optional": true - }, - "shiki": { - "version": "0.9.11", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.9.11.tgz", - "integrity": "sha512-tjruNTLFhU0hruCPoJP0y+B9LKOmcqUhTpxn7pcJB3fa+04gFChuEmxmrUfOJ7ZO6Jd+HwMnDHgY3lv3Tqonuw==", - "dev": true, - "requires": { - "jsonc-parser": "^3.0.0", - "onigasm": "^2.2.5", - "vscode-textmate": "5.2.0" - } - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", "dev": true, "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "xmlchars": "^2.2.0" } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" + "lru-cache": "^6.0.0" } }, - "source-map-support": { - "version": "0.5.20", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz", - "integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==", + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "shebang-regex": "^3.0.0" } }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "shiki": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.10.1.tgz", + "integrity": "sha512-VsY7QJVzU51j5o1+DguUd+6vmCmZ5v/6gYu4vyYAhzjuNQU6P/vmSy4uQaOhvje031qQMiW0d2BwgMH52vqMng==", "dev": true, "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "jsonc-parser": "^3.0.0", + "vscode-oniguruma": "^1.6.1", + "vscode-textmate": "5.2.0" } }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" } }, - "spdx-license-ids": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz", - "integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==", + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "requires": { - "extend-shallow": "^3.0.0" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, "sprintf-js": { @@ -6111,13 +5149,12 @@ "dev": true }, "stack-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.4.tgz", - "integrity": "sha512-ERg+H//lSSYlZhBIUu+wJnqg30AbyBbpZlIhcshpn7BNzpoRODZgfyr9J+8ERf3ooC6af3u7Lcl01nleau7MrA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", "dev": true, "requires": { - "escape-string-regexp": "^2.0.0", - "source-map-support": "^0.5.20" + "escape-string-regexp": "^2.0.0" }, "dependencies": { "escape-string-regexp": { @@ -6128,27 +5165,6 @@ } } }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, "stats-median": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stats-median/-/stats-median-1.0.1.tgz", @@ -6217,12 +5233,6 @@ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -6235,132 +5245,6 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, - "sublevel-prefixer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sublevel-prefixer/-/sublevel-prefixer-1.0.0.tgz", - "integrity": "sha1-TuRZ72Y6yFvyj8ZJ17eWX9ppEHM=" - }, - "subleveldown": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/subleveldown/-/subleveldown-5.0.1.tgz", - "integrity": "sha512-cVqd/URpp7si1HWu5YqQ3vqQkjuolAwHypY1B4itPlS71/lsf6TQPZ2Y0ijT22EYVkvH5ove9JFJf4u7VGPuZw==", - "requires": { - "abstract-leveldown": "^6.3.0", - "encoding-down": "^6.2.0", - "inherits": "^2.0.3", - "level-option-wrap": "^1.1.0", - "levelup": "^4.4.0", - "reachdown": "^1.1.0" - }, - "dependencies": { - "abstract-leveldown": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz", - "integrity": "sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ==", - "requires": { - "buffer": "^5.5.0", - "immediate": "^3.2.3", - "level-concat-iterator": "~2.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "deferred-leveldown": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz", - "integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==", - "requires": { - "abstract-leveldown": "~6.2.1", - "inherits": "^2.0.3" - }, - "dependencies": { - "abstract-leveldown": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz", - "integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==", - "requires": { - "buffer": "^5.5.0", - "immediate": "^3.2.3", - "level-concat-iterator": "~2.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - } - } - } - }, - "encoding-down": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/encoding-down/-/encoding-down-6.3.0.tgz", - "integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==", - "requires": { - "abstract-leveldown": "^6.2.1", - "inherits": "^2.0.3", - "level-codec": "^9.0.0", - "level-errors": "^2.0.0" - } - }, - "level-codec": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/level-codec/-/level-codec-9.0.2.tgz", - "integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==", - "requires": { - "buffer": "^5.6.0" - } - }, - "level-concat-iterator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", - "integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==" - }, - "level-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/level-errors/-/level-errors-2.0.1.tgz", - "integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==", - "requires": { - "errno": "~0.1.1" - } - }, - "level-iterator-stream": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz", - "integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.4.0", - "xtend": "^4.0.2" - } - }, - "level-supports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/level-supports/-/level-supports-1.0.1.tgz", - "integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==", - "requires": { - "xtend": "^4.0.2" - } - }, - "levelup": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/levelup/-/levelup-4.4.0.tgz", - "integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==", - "requires": { - "deferred-leveldown": "~5.3.0", - "level-errors": "~2.0.0", - "level-iterator-stream": "~4.0.0", - "level-supports": "~1.0.0", - "xtend": "~4.0.0" - } - } - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6466,9 +5350,9 @@ } }, "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", + "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==", "dev": true }, "tiny-worker": { @@ -6492,44 +5376,6 @@ "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6565,29 +5411,19 @@ "integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==" }, "ts-jest": { - "version": "26.5.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz", - "integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==", + "version": "27.1.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.4.tgz", + "integrity": "sha512-qjkZlVPWVctAezwsOD1OPzbZ+k7zA5z3oxII4dGdZo5ggX/PL7kvwTM0pXTr10fAtbiVpJaL3bWd502zAhpgSQ==", "dev": true, "requires": { "bs-logger": "0.x", - "buffer-from": "1.x", "fast-json-stable-stringify": "2.x", - "jest-util": "^26.1.0", + "jest-util": "^27.0.0", "json5": "2.x", - "lodash": "4.x", + "lodash.memoize": "4.x", "make-error": "1.x", - "mkdirp": "1.x", "semver": "7.x", "yargs-parser": "20.x" - }, - "dependencies": { - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } } }, "ts-node": { @@ -6706,40 +5542,71 @@ } }, "typedoc": { - "version": "0.21.9", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.21.9.tgz", - "integrity": "sha512-VRo7aII4bnYaBBM1lhw4bQFmUcDQV8m8tqgjtc7oXl87jc1Slbhfw2X5MccfcR2YnEClHDWgsiQGgNB8KJXocA==", + "version": "0.22.15", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.22.15.tgz", + "integrity": "sha512-CMd1lrqQbFvbx6S9G6fL4HKp3GoIuhujJReWqlIvSb2T26vGai+8Os3Mde7Pn832pXYemd9BMuuYWhFpL5st0Q==", "dev": true, "requires": { - "glob": "^7.1.7", - "handlebars": "^4.7.7", + "glob": "^7.2.0", "lunr": "^2.3.9", - "marked": "^3.0.2", - "minimatch": "^3.0.0", - "progress": "^2.0.3", - "shiki": "^0.9.8", - "typedoc-default-themes": "^0.12.10" + "marked": "^4.0.12", + "minimatch": "^5.0.1", + "shiki": "^0.10.1" + }, + "dependencies": { + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, + "minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } + } + } } }, - "typedoc-default-themes": { - "version": "0.12.10", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.12.10.tgz", - "integrity": "sha512-fIS001cAYHkyQPidWXmHuhs8usjP5XVJjWB8oZGqkTowZaz3v7g3KDZeeqE82FBrmkAnIBOY3jgy7lnPnqATbA==", - "dev": true - }, "typescript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", - "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", "dev": true }, - "uglify-js": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.2.tgz", - "integrity": "sha512-rtPMlmcO4agTUfz10CbgJ1k6UAoXM2gWb3GoMPPZB/+/Ackf8lNWk11K4rYi2D0apgoFRLtQOZhb+/iGNJq26A==", - "dev": true, - "optional": true - }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", @@ -6751,64 +5618,12 @@ "which-boxed-primitive": "^1.0.2" } }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, "universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6818,18 +5633,6 @@ "punycode": "^2.1.0" } }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, "util-callbackify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/util-callbackify/-/util-callbackify-1.0.0.tgz", @@ -6843,13 +5646,6 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "optional": true - }, "v8-compile-cache": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", @@ -6857,9 +5653,9 @@ "dev": true }, "v8-to-istanbul": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz", - "integrity": "sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz", + "integrity": "sha512-FGtKtv3xIpR6BYhvgH8MI/y78oT7d8Au3ww4QIxymrCtZEh5b8gCw2siywE+puhEmuWKDtmfrvF5UlB298ut3w==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.1", @@ -6875,15 +5671,11 @@ } } }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } + "vscode-oniguruma": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz", + "integrity": "sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA==", + "dev": true }, "vscode-textmate": { "version": "5.2.0", @@ -6910,12 +5702,12 @@ } }, "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, "requires": { - "makeerror": "1.0.x" + "makeerror": "1.0.12" } }, "webidl-conversions": { @@ -6971,24 +5763,12 @@ "is-symbol": "^1.0.3" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -7019,9 +5799,9 @@ } }, "ws": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", - "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", + "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", "dev": true }, "xml-name-validator": { @@ -7036,15 +5816,10 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yallist": { @@ -7054,33 +5829,25 @@ "dev": true }, "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" } }, "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true }, "yn": { "version": "3.1.1", diff --git a/package.json b/package.json index fee18475..d9429bcd 100644 --- a/package.json +++ b/package.json @@ -37,31 +37,30 @@ "test": "jest", "lint": "eslint '{src,tests,benches}/**/*.{js,ts}'", "lintfix": "eslint '{src,tests,benches}/**/*.{js,ts}' --fix", - "docs": "rm -r ./docs || true; typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src && touch ./docs/.nojekyll", + "docs": "rm -r ./docs || true; typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", "bench": "rm -r ./benches/results || true; ts-node -r tsconfig-paths/register ./benches" }, "dependencies": { - "@matrixai/async-init": "^1.6.0", - "@matrixai/db": "^1.1.5", + "@matrixai/async-init": "^1.7.1", + "@matrixai/async-locks": "^2.2.0", + "@matrixai/db": "^3.3.0", + "@matrixai/errors": "^1.1.0", "@matrixai/logger": "^2.1.0", - "@matrixai/workers": "^1.2.5", - "async-mutex": "^0.3.2", + "@matrixai/resources": "^1.1.1", + "@matrixai/workers": "^1.3.1", "errno": "^0.1.7", "lexicographic-integer": "^1.1.0", "node-forge": "^0.10.0", "readable-stream": "^3.6.0", "resource-counter": "^1.2.4", "threads": "^1.6.5", - "ts-custom-error": "^3.2.0", "util-callbackify": "^1.0.0" }, "devDependencies": { - "@types/jest": "^26.0.20", - "@types/level": "^6.0.0", + "@types/jest": "^27.0.2", "@types/node": "^14.14.35", - "@types/node-forge": "^0.9.7", + "@types/node-forge": "^0.10.4", "@types/readable-stream": "^2.3.11", - "@types/subleveldown": "^4.1.0", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", "benny": "^3.6.15", @@ -69,13 +68,13 @@ "eslint-config-prettier": "^7.1.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-prettier": "^3.3.1", - "jest": "^26.6.3", + "jest": "^27.2.5", "prettier": "^2.2.1", "systeminformation": "^5.8.9", - "ts-jest": "^26.4.4", + "ts-jest": "^27.0.5", "ts-node": "^10.4.0", "tsconfig-paths": "^3.9.0", - "typedoc": "^0.21.5", - "typescript": "^4.1.3" + "typedoc": "^0.22.15", + "typescript": "^4.5.2" } } diff --git a/src/CurrentDirectory.ts b/src/CurrentDirectory.ts index 106d667c..0f94fe79 100644 --- a/src/CurrentDirectory.ts +++ b/src/CurrentDirectory.ts @@ -22,8 +22,8 @@ class CurrentDirectory { curPath: Array, ): Promise { this._iNodeMgr.ref(ino); - await this._iNodeMgr.transact(async (tran) => { - await this._iNodeMgr.unref(tran, this._ino); + await this._iNodeMgr.withTransactionF(async (tran) => { + await this._iNodeMgr.unref(this._ino, tran); }); this._ino = ino; this._curPath = curPath; diff --git a/src/EncryptedFS.ts b/src/EncryptedFS.ts index 748e04ab..b3328e52 100644 --- a/src/EncryptedFS.ts +++ b/src/EncryptedFS.ts @@ -1,3 +1,4 @@ +import type { DBTransaction } from '@matrixai/db'; import type { Navigated, ParsedPath, @@ -8,10 +9,10 @@ import type { File, EFSWorkerManagerInterface, } from './types'; -import type { INodeIndex } from './inodes'; +import type { INodeIndex, INodeType } from './inodes'; import type { FdIndex, FileDescriptor } from './fd'; import type { OptionsStream } from './streams'; - +import type { ResourceRelease } from '@matrixai/resources'; import { code as errno } from 'errno'; import Logger from '@matrixai/logger'; import { DB, errors as dbErrors } from '@matrixai/db'; @@ -93,20 +94,28 @@ class EncryptedFS { fresh?: boolean; }): Promise { if (db == null) { - db = await DB.createDB({ - dbPath: dbPath!, - crypto: { - key: dbKey!, - ops: { - encrypt: utils.encrypt, - decrypt: utils.decrypt, + try { + db = await DB.createDB({ + dbPath: dbPath!, + crypto: { + key: dbKey!, + ops: { + encrypt: utils.encrypt, + decrypt: utils.decrypt, + }, }, - }, - logger: logger.getChild(DB.name), - fresh, - }); + logger: logger.getChild(DB.name), + fresh, + }); + } catch (e) { + if (e instanceof dbErrors.ErrorDBKey) { + throw new errors.ErrorEncryptedFSKey('Incorrect key supplied', { + cause: e, + }); + } + throw e; + } } - await EncryptedFS.setupCanary(db); iNodeMgr = iNodeMgr ?? (await INodeManager.createINodeManager({ @@ -114,29 +123,37 @@ class EncryptedFS { logger: logger.getChild(INodeManager.name), fresh, })); - let rootIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr!.inoDeallocate(rootIno); - }); + const rootIno = await iNodeMgr.withNewINodeTransactionF( + async (rootIno, tran) => { try { - await iNodeMgr!.dirCreate(tran, rootIno, { - mode: permissions.DEFAULT_ROOT_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }); - } catch (err) { - if (err instanceof inodesErrors.ErrorINodesDuplicateRoot) { + await iNodeMgr!.dirCreate( + rootIno, + { + mode: permissions.DEFAULT_ROOT_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + undefined, + tran, + ); + } catch (e) { + if (e instanceof inodesErrors.ErrorINodesDuplicateRoot) { const root = await iNodeMgr!.dirGetRoot(tran); - if (!root) throw new inodesErrors.ErrorINodesIndexMissing(); + if (!root) { + throw new inodesErrors.ErrorINodesIndexMissing( + 'Could not find pre-existing root INode, database may be corrupted', + { + cause: e, + }, + ); + } rootIno = root; } else { - throw err; + throw e; } } + return rootIno; }, - [rootIno], ); fdMgr = fdMgr ?? new FileDescriptorManager(iNodeMgr); const efs = new EncryptedFS({ @@ -275,26 +292,32 @@ class EncryptedFS { ): Promise { return utils.maybeCallback(async () => { path = this.getPath(path); - const navigated = await this.navigate(path, true); - const target = navigated.target; - if (!target) { + const target = (await this.navigate(path, true)).target; + if (target == null) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, path: path as string, + syscall: 'chroot', }); } - await this.iNodeMgr.transact( - async (tran) => { - const targetType = (await this.iNodeMgr.get(tran, target))?.type; - if (!(targetType === 'Directory')) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTDIR, - path: path as string, - }); - } - }, - target ? [target] : [], - ); + await this.iNodeMgr.withTransactionF(target, async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'chroot', + }); + } + const targetType = targetData.type; + if (!(targetType === 'Directory')) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOTDIR, + path: path as string, + syscall: 'chroot', + }); + } + }); // Chrooted EFS shares all of the dependencies // This means the dependencies are already in running state const efsChrooted = new EncryptedFS({ @@ -321,32 +344,37 @@ class EncryptedFS { path = this.getPath(path); const navigated = await this.navigate(path, true); const target = navigated.target; - await this.iNodeMgr.transact( - async (tran) => { - if (!target) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - }); - } - const targetType = (await this.iNodeMgr.get(tran, target))?.type; - const targetStat = await this.iNodeMgr.statGet(tran, target); - if (!(targetType === 'Directory')) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTDIR, - path: path as string, - }); - } - if (!this.checkPermissions(constants.X_OK, targetStat)) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, - path: path as string, - }); - } - await this._cwd.changeDir(target, navigated.pathStack); - }, - target ? [target] : [], - ); + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + }); + } + await this.iNodeMgr.withTransactionF(target, async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'readdir', + }); + } + const targetType = targetData.type; + if (!(targetType === 'Directory')) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOTDIR, + path: path as string, + }); + } + const targetStat = await this.iNodeMgr.statGet(target, tran); + if (!this.checkPermissions(constants.X_OK, targetStat)) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EACCES, + path: path as string, + }); + } + await this._cwd.changeDir(target, navigated.pathStack); + }); }, callback); } @@ -369,8 +397,16 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, true)).target; - await this.iNodeMgr.transact(async (tran) => { - if (!target) { + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'access', + }); + } + await this.iNodeMgr.withTransactionF(async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, path: path as string, @@ -380,7 +416,7 @@ class EncryptedFS { if (mode === constants.F_OK) { return; } - const targetStat = await this.iNodeMgr.statGet(tran, target); + const targetStat = await this.iNodeMgr.statGet(target, tran); if (!this.checkPermissions(mode, targetStat)) { throw new errors.ErrorEncryptedFSError({ errno: errno.EACCES, @@ -436,10 +472,9 @@ class EncryptedFS { typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; return utils.maybeCallback(async () => { options.flag = 'a'; - data = this.getBuffer(data, options.encoding); - let fdIndex; + let fdIndex: FdIndex | undefined; try { - let fd; + let fd: FileDescriptor | undefined; if (typeof file === 'number') { fd = this.fdMgr.getFd(file); if (!fd) @@ -461,13 +496,20 @@ class EncryptedFS { ); } try { - await fd.write(data, undefined, constants.O_APPEND); + await fd.write( + this.getBuffer(data, options.encoding), + undefined, + constants.O_APPEND, + ); } catch (e) { if (e instanceof RangeError) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EFBIG, - syscall: 'appendFile', - }); + throw new errors.ErrorEncryptedFSError( + { + errno: errno.EFBIG, + syscall: 'appendFile', + }, + { cause: e }, + ); } throw e; } @@ -487,35 +529,40 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, true)).target; - await this.iNodeMgr.transact( - async (tran) => { - if (!target) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - syscall: 'chmod', - }); - } - const targetStat = await this.iNodeMgr.statGet(tran, target); - if ( - this.uid !== permissions.DEFAULT_ROOT_UID && - this.uid !== targetStat.uid - ) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - path: path as string, - syscall: 'chmod', - }); - } - await this.iNodeMgr.statSetProp( - tran, - target, - 'mode', - (targetStat.mode & constants.S_IFMT) | mode, - ); - }, - target ? [target] : [], - ); + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'chmod', + }); + } + await this.iNodeMgr.withTransactionF(target, async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'chmod', + }); + } + const targetStat = await this.iNodeMgr.statGet(target, tran); + if ( + this.uid !== permissions.DEFAULT_ROOT_UID && + this.uid !== targetStat.uid + ) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EPERM, + path: path as string, + syscall: 'chmod', + }); + } + await this.iNodeMgr.statSetProp( + target, + 'mode', + (targetStat.mode & constants.S_IFMT) | mode, + tran, + ); + }); }, callback); } @@ -529,40 +576,45 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, true)).target; - await this.iNodeMgr.transact( - async (tran) => { - if (!target) { + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'chown', + }); + } + await this.iNodeMgr.withTransactionF(target, async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'chown', + }); + } + const targetStat = await this.iNodeMgr.statGet(target, tran); + if (this.uid !== permissions.DEFAULT_ROOT_UID) { + // You don't own the file + if (targetStat.uid !== this.uid) { throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, + errno: errno.EPERM, path: path as string, syscall: 'chown', }); } - const targetStat = await this.iNodeMgr.statGet(tran, target); - if (this.uid !== permissions.DEFAULT_ROOT_UID) { - // You don't own the file - if (targetStat.uid !== this.uid) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - path: path as string, - syscall: 'chown', - }); - } - // You cannot give files to others - if (this.uid !== uid) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - path: path as string, - syscall: 'chown', - }); - } - // Because we don't have user group hierarchies, we allow chowning to any group + // You cannot give files to others + if (this.uid !== uid) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EPERM, + path: path as string, + syscall: 'chown', + }); } - await this.iNodeMgr.statSetProp(tran, target, 'uid', uid); - await this.iNodeMgr.statSetProp(tran, target, 'gid', gid); - }, - target ? [target] : [], - ); + // Because we don't have user group hierarchies, we allow chowning to any group + } + await this.iNodeMgr.statSetProp(target, 'uid', uid, tran); + await this.iNodeMgr.statSetProp(target, 'gid', gid, tran); + }); }, callback); } @@ -576,7 +628,7 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); await this.chown(path, uid, gid); - let children; + let children: Array; try { children = await this.readdir(path); } catch (e) { @@ -584,7 +636,7 @@ class EncryptedFS { throw e; } for (const child of children) { - const pathChild = utils.pathJoin(path as string, child); + const pathChild = utils.pathJoin(path as string, child.toString()); // Don't traverse symlinks if (!(await this.lstat(pathChild)).isSymbolicLink()) { await this.chownr(pathChild, uid, gid); @@ -635,18 +687,25 @@ class EncryptedFS { return utils.maybeCallback(async () => { srcPath = this.getPath(srcPath); dstPath = this.getPath(dstPath); - let srcFd, srcFdIndex, dstFd, dstFdIndex; + let srcFd: FileDescriptor; + let srcFdIndex: FdIndex | undefined; + let dstFd: FileDescriptor; + let dstFdIndex: FdIndex | undefined; try { // The only things that are copied is the data and the mode [srcFd, srcFdIndex] = await this._open(srcPath, constants.O_RDONLY); const srcINode = srcFd.ino; - await this.iNodeMgr.transact(async (tran) => { - tran.queueFailure(() => { - this.iNodeMgr.inoDeallocate(dstINode); - }); - const srcINodeType = (await this.iNodeMgr.get(tran, srcINode))?.type; - const srcINodeStat = await this.iNodeMgr.statGet(tran, srcINode); - if (srcINodeType === 'Directory') { + await this.iNodeMgr.withTransactionF(async (tran) => { + const srcINodeData = await this.iNodeMgr.get(srcINode, tran); + if (srcINodeData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: srcPath as string, + syscall: 'copyFile', + }); + } + const srcINodeStat = await this.iNodeMgr.statGet(srcINode, tran); + if (srcINodeData.type === 'Directory') { throw new errors.ErrorEncryptedFSError({ errno: errno.EBADF, path: srcPath as string, @@ -664,23 +723,38 @@ class EncryptedFS { srcINodeStat.mode, ); const dstINode = dstFd.ino; - const dstINodeType = (await this.iNodeMgr.get(tran, dstINode))?.type; + const dstINodeType = (await this.iNodeMgr.get(dstINode, tran))?.type; if (dstINodeType === 'File') { let blkCounter = 0; for await (const block of this.iNodeMgr.fileGetBlocks( - tran, srcINode, this.blockSize, + undefined, + undefined, + tran, )) { await this.iNodeMgr.fileSetBlocks( - tran, dstFd.ino, block, this.blockSize, blkCounter, + tran, ); blkCounter++; } + // Setting the size + const size = await this.iNodeMgr.statGetProp( + srcINode, + 'size', + tran, + ); + const blocks = await this.iNodeMgr.statGetProp( + srcINode, + 'blocks', + tran, + ); + await this.iNodeMgr.statSetProp(dstFd.ino, 'size', size, tran); + await this.iNodeMgr.statSetProp(dstFd.ino, 'blocks', blocks, tran); } else { throw new errors.ErrorEncryptedFSError({ errno: errno.EINVAL, @@ -697,25 +771,8 @@ class EncryptedFS { }, callback); } - public async createReadStream( - path: Path, - options?: OptionsStream, - ): Promise; - public async createReadStream( - path: Path, - callback: Callback<[ReadStream]>, - ): Promise; - public async createReadStream( - path: Path, - options: OptionsStream, - callback: Callback<[ReadStream]>, - ): Promise; @ready(new errors.ErrorEncryptedFSNotRunning()) - public async createReadStream( - path: Path, - optionsOrCallback: OptionsStream | Callback<[ReadStream]> = {}, - callback?: Callback<[ReadStream]>, - ): Promise { + public createReadStream(path: Path, options?: OptionsStream): ReadStream { const defaultOps: OptionsStream = { flags: 'r', encoding: undefined, @@ -724,42 +781,18 @@ class EncryptedFS { autoClose: true, end: Infinity, }; - const options = - typeof optionsOrCallback !== 'function' - ? (this.getOptions(defaultOps, optionsOrCallback) as OptionsStream) - : defaultOps; - callback = - typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; - return utils.maybeCallback(async () => { - path = this.getPath(path); - if (options.start !== undefined) { - if (options.start > (options.end ?? Infinity)) { - throw new RangeError('ERR_VALUE_OUT_OF_RANGE'); - } + const options_: OptionsStream = this.getOptions(defaultOps, options); + path = this.getPath(path); + if (options_.start !== undefined) { + if (options_.start > (options_.end ?? Infinity)) { + throw new RangeError('ERR_VALUE_OUT_OF_RANGE'); } - return new ReadStream(path, options, this); - }, callback); + } + return new ReadStream(path, options_, this); } - public async createWriteStream( - path: Path, - options?: OptionsStream, - ): Promise; - public async createWriteStream( - path: Path, - callback: Callback<[WriteStream]>, - ): Promise; - public async createWriteStream( - path: Path, - options: OptionsStream, - callback: Callback<[WriteStream]>, - ): Promise; @ready(new errors.ErrorEncryptedFSNotRunning()) - public async createWriteStream( - path: Path, - optionsOrCallback: OptionsStream | Callback<[WriteStream]> = {}, - callback?: Callback<[WriteStream]>, - ): Promise { + public createWriteStream(path: Path, options?: OptionsStream): WriteStream { const defaultOps: OptionsStream = { flags: 'w', encoding: 'utf8', @@ -767,21 +800,14 @@ class EncryptedFS { mode: permissions.DEFAULT_FILE_PERM, autoClose: true, }; - const options = - typeof optionsOrCallback !== 'function' - ? (this.getOptions(defaultOps, optionsOrCallback) as OptionsStream) - : defaultOps; - callback = - typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; - return utils.maybeCallback(async () => { - path = this.getPath(path); - if (options.start !== undefined) { - if (options.start < 0) { - throw new RangeError('ERR_VALUE_OUT_OF_RANGE'); - } + const options_: OptionsStream = this.getOptions(defaultOps, options); + path = this.getPath(path); + if (options_.start !== undefined) { + if (options_.start < 0) { + throw new RangeError('ERR_VALUE_OUT_OF_RANGE'); } - return new WriteStream(path, options, this); - }, callback); + } + return new WriteStream(path, options_, this); } @ready(new errors.ErrorEncryptedFSNotRunning()) @@ -814,66 +840,67 @@ class EncryptedFS { }); } const fd = this.fdMgr.getFd(fdIndex); - await this.iNodeMgr.transact( - async (tran) => { - if (!fd) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EBADF, - syscall: 'fallocate', - }); - } - const iNodeType = (await this.iNodeMgr.get(tran, fd.ino))?.type; - if (!(iNodeType === 'File')) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENODEV, - syscall: 'fallocate', - }); - } - if (!(fd.flags & (constants.O_WRONLY | constants.O_RDWR))) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EBADF, - syscall: 'fallocate', - }); - } - const data = Buffer.alloc(0); - if (offset + len > data.length) { - const [index, data] = await this.iNodeMgr.fileGetLastBlock( - tran, - fd.ino, - ); - let newData; - try { - newData = Buffer.concat([ - data, - Buffer.alloc(offset + len - data.length), - ]); - } catch (e) { - if (e instanceof RangeError) { - throw new errors.ErrorEncryptedFSError({ + if (!fd) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBADF, + syscall: 'fallocate', + }); + } + await this.iNodeMgr.withTransactionF(fd.ino, async (tran) => { + const iNodeData = await this.iNodeMgr.get(fd.ino, tran); + if (iNodeData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + syscall: 'fallocate', + }); + } + if (!(iNodeData.type === 'File')) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENODEV, + syscall: 'fallocate', + }); + } + if (!(fd.flags & (constants.O_WRONLY | constants.O_RDWR))) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBADF, + syscall: 'fallocate', + }); + } + const data = Buffer.alloc(0); + if (offset + len > data.length) { + const [index, data] = await this.iNodeMgr.fileGetLastBlock( + fd.ino, + tran, + ); + let newData: Buffer; + try { + newData = Buffer.concat([ + data, + Buffer.alloc(offset + len - data.length), + ]); + } catch (e) { + if (e instanceof RangeError) { + throw new errors.ErrorEncryptedFSError( + { errno: errno.EFBIG, syscall: 'fallocate', - }); - } - throw e; + }, + { cause: e }, + ); } - await this.iNodeMgr.fileSetBlocks( - tran, - fd.ino, - newData, - this.blockSize, - index, - ); - await this.iNodeMgr.statSetProp( - tran, - fd.ino, - 'size', - newData.length, - ); + throw e; } - await this.iNodeMgr.statSetProp(tran, fd.ino, 'ctime', new Date()); - }, - fd ? [fd.ino] : [], - ); + await this.iNodeMgr.fileSetBlocks( + fd.ino, + newData, + this.blockSize, + index, + tran, + ); + await this.iNodeMgr.statSetProp(fd.ino, 'size', newData.length, tran); + } + await this.iNodeMgr.statSetProp(fd.ino, 'ctime', new Date(), tran); + }); }, callback); } @@ -885,33 +912,37 @@ class EncryptedFS { ): Promise { return utils.maybeCallback(async () => { const fd = this.fdMgr.getFd(fdIndex); - await this.iNodeMgr.transact( - async (tran) => { - if (!fd) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EBADF, - syscall: 'fchmod', - }); - } - const fdStat = await this.iNodeMgr.statGet(tran, fd.ino); - if ( - this.uid !== permissions.DEFAULT_ROOT_UID && - this.uid !== fdStat.uid - ) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - syscall: 'fchmod', - }); - } - await this.iNodeMgr.statSetProp( - tran, - fd.ino, - 'mode', - (fdStat.mode & constants.S_IFMT) | mode, - ); - }, - fd ? [fd.ino] : [], - ); + if (!fd) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBADF, + syscall: 'fchmod', + }); + } + await this.iNodeMgr.withTransactionF(fd.ino, async (tran) => { + const iNodeData = await this.iNodeMgr.get(fd.ino, tran); + if (iNodeData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + syscall: 'fchmod', + }); + } + const fdStat = await this.iNodeMgr.statGet(fd.ino, tran); + if ( + this.uid !== permissions.DEFAULT_ROOT_UID && + this.uid !== fdStat.uid + ) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EPERM, + syscall: 'fchmod', + }); + } + await this.iNodeMgr.statSetProp( + fd.ino, + 'mode', + (fdStat.mode & constants.S_IFMT) | mode, + tran, + ); + }); }, callback); } @@ -924,37 +955,41 @@ class EncryptedFS { ): Promise { return utils.maybeCallback(async () => { const fd = this.fdMgr.getFd(fdIndex); - await this.iNodeMgr.transact( - async (tran) => { - if (!fd) { + if (!fd) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBADF, + syscall: 'fchown', + }); + } + await this.iNodeMgr.withTransactionF(fd.ino, async (tran) => { + const iNodeData = await this.iNodeMgr.get(fd.ino, tran); + if (iNodeData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + syscall: 'fchown', + }); + } + const fdStat = await this.iNodeMgr.statGet(fd.ino, tran); + if (this.uid !== permissions.DEFAULT_ROOT_UID) { + // You don't own the file + if (fdStat.uid !== this.uid) { throw new errors.ErrorEncryptedFSError({ - errno: errno.EBADF, + errno: errno.EPERM, syscall: 'fchown', }); } - const fdStat = await this.iNodeMgr.statGet(tran, fd.ino); - if (this.uid !== permissions.DEFAULT_ROOT_UID) { - // You don't own the file - if (fdStat.uid !== this.uid) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - syscall: 'fchown', - }); - } - // You cannot give files to others - if (this.uid !== uid) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - syscall: 'fchown', - }); - } - // Because we don't have user group hierarchies, we allow chowning to any group + // You cannot give files to others + if (this.uid !== uid) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EPERM, + syscall: 'fchown', + }); } - await this.iNodeMgr.statSetProp(tran, fd.ino, 'uid', uid); - await this.iNodeMgr.statSetProp(tran, fd.ino, 'gid', gid); - }, - fd ? [fd.ino] : [], - ); + // Because we don't have user group hierarchies, we allow chowning to any group + } + await this.iNodeMgr.statSetProp(fd.ino, 'uid', uid, tran); + await this.iNodeMgr.statSetProp(fd.ino, 'gid', gid, tran); + }); }, callback); } @@ -982,18 +1017,24 @@ class EncryptedFS { ): Promise { return utils.maybeCallback(async () => { const fd = this.fdMgr.getFd(fdIndex); - let fdStat; - await this.iNodeMgr.transact( + if (!fd) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBADF, + syscall: 'fstat', + }); + } + const fdStat = await this.iNodeMgr.withTransactionF( + fd.ino, async (tran) => { - if (!fd) { + const iNodeData = await this.iNodeMgr.get(fd.ino, tran); + if (iNodeData == null) { throw new errors.ErrorEncryptedFSError({ - errno: errno.EBADF, + errno: errno.ENOENT, syscall: 'fstat', }); } - fdStat = await this.iNodeMgr.statGet(tran, fd.ino); + return await this.iNodeMgr.statGet(fd.ino, tran); }, - fd ? [fd.ino] : [], ); return new Stat(fdStat); }, callback); @@ -1033,77 +1074,88 @@ class EncryptedFS { syscall: 'ftruncate', }); } - let newData; + let newData: Buffer; const fd = this.fdMgr.getFd(fdIndex); - await this.iNodeMgr.transact( - async (tran) => { - if (!fd) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EBADF, - syscall: 'ftruncate', - }); - } - const iNodeType = (await this.iNodeMgr.get(tran, fd.ino))?.type; - if (!(iNodeType === 'File')) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EINVAL, - syscall: 'ftruncate', - }); - } - if (!(fd.flags & (constants.O_WRONLY | constants.O_RDWR))) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EINVAL, - syscall: 'ftruncate', - }); - } - let data = Buffer.alloc(0); - for await (const block of this.iNodeMgr.fileGetBlocks( - tran, - fd.ino, - this.blockSize, - )) { - data = Buffer.concat([data, block]); + if (!fd) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBADF, + syscall: 'ftruncate', + }); + } + await this.iNodeMgr.withTransactionF(fd.ino, async (tran) => { + const iNodeData = await this.iNodeMgr.get(fd.ino, tran); + if (iNodeData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + syscall: 'ftruncate', + }); + } + if (!(iNodeData.type === 'File')) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EINVAL, + syscall: 'ftruncate', + }); + } + if (!(fd.flags & (constants.O_WRONLY | constants.O_RDWR))) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EINVAL, + syscall: 'ftruncate', + }); + } + let data = Buffer.alloc(0); + for await (const block of this.iNodeMgr.fileGetBlocks( + fd.ino, + this.blockSize, + undefined, + undefined, + tran, + )) { + data = Buffer.concat([data, block]); + } + try { + if (len > data.length) { + newData = Buffer.alloc(len); + data.copy(newData, 0, 0, data.length); + await this.iNodeMgr.fileSetBlocks( + fd.ino, + newData, + this.blockSize, + undefined, + tran, + ); + } else if (len < data.length) { + newData = Buffer.allocUnsafe(len); + data.copy(newData, 0, 0, len); + // Clear all file blocks for this inode before setting new blocks + await this.iNodeMgr.fileClearData(fd.ino, tran); + await this.iNodeMgr.fileSetBlocks( + fd.ino, + newData, + this.blockSize, + undefined, + tran, + ); + } else { + newData = data; } - try { - if (len > data.length) { - newData = Buffer.alloc(len); - data.copy(newData, 0, 0, data.length); - await this.iNodeMgr.fileSetBlocks( - tran, - fd.ino, - newData, - this.blockSize, - ); - } else if (len < data.length) { - newData = Buffer.allocUnsafe(len); - data.copy(newData, 0, 0, len); - await this.iNodeMgr.fileClearData(tran, fd.ino); - await this.iNodeMgr.fileSetBlocks( - tran, - fd.ino, - newData, - this.blockSize, - ); - } else { - newData = data; - } - } catch (e) { - if (e instanceof RangeError) { - throw new errors.ErrorEncryptedFSError({ + } catch (e) { + if (e instanceof RangeError) { + throw new errors.ErrorEncryptedFSError( + { errno: errno.EFBIG, syscall: 'ftruncate', - }); - } - throw e; + }, + { cause: e }, + ); } - const now = new Date(); - await this.iNodeMgr.statSetProp(tran, fd.ino, 'mtime', now); - await this.iNodeMgr.statSetProp(tran, fd.ino, 'ctime', now); - await this.iNodeMgr.statSetProp(tran, fd.ino, 'size', newData.length); - await fd.setPos(tran, Math.min(newData.length, fd.pos)); - }, - fd ? [fd.ino] : [], - ); + throw e; + } + const now = new Date(); + await this.iNodeMgr.statSetProp(fd.ino, 'mtime', now, tran); + await this.iNodeMgr.statSetProp(fd.ino, 'ctime', now, tran); + await this.iNodeMgr.statSetProp(fd.ino, 'size', newData.length, tran); + await fd.setPos(Math.min(newData.length, fd.pos), undefined, tran); + }); }, callback); } @@ -1116,36 +1168,40 @@ class EncryptedFS { ): Promise { return utils.maybeCallback(async () => { const fd = this.fdMgr.getFd(fdIndex); - await this.iNodeMgr.transact( - async (tran) => { - if (!fd) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EBADF, - syscall: 'futimes', - }); - } - let newAtime; - let newMtime; - if (typeof atime === 'number') { - newAtime = new Date(atime * 1000); - } else if (typeof atime === 'string') { - newAtime = new Date(parseInt(atime) * 1000); - } else if (atime instanceof Date) { - newAtime = atime; - } - if (typeof mtime === 'number') { - newMtime = new Date(mtime * 1000); - } else if (typeof mtime === 'string') { - newMtime = new Date(parseInt(mtime) * 1000); - } else if (mtime instanceof Date) { - newMtime = mtime; - } - await this.iNodeMgr.statSetProp(tran, fd.ino, 'atime', newAtime); - await this.iNodeMgr.statSetProp(tran, fd.ino, 'mtime', newMtime); - await this.iNodeMgr.statSetProp(tran, fd.ino, 'ctime', new Date()); - }, - fd ? [fd.ino] : [], - ); + if (!fd) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBADF, + syscall: 'futimes', + }); + } + await this.iNodeMgr.withTransactionF(fd.ino, async (tran) => { + const iNodeData = await this.iNodeMgr.get(fd.ino, tran); + if (iNodeData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + syscall: 'futimes', + }); + } + let newAtime: Date; + let newMtime: Date; + if (typeof atime === 'number') { + newAtime = new Date(atime * 1000); + } else if (typeof atime === 'string') { + newAtime = new Date(parseInt(atime) * 1000); + } else { + newAtime = atime; + } + if (typeof mtime === 'number') { + newMtime = new Date(mtime * 1000); + } else if (typeof mtime === 'string') { + newMtime = new Date(parseInt(mtime) * 1000); + } else { + newMtime = mtime; + } + await this.iNodeMgr.statSetProp(fd.ino, 'atime', newAtime, tran); + await this.iNodeMgr.statSetProp(fd.ino, 'mtime', newMtime, tran); + await this.iNodeMgr.statSetProp(fd.ino, 'ctime', new Date(), tran); + }); }, callback); } @@ -1158,35 +1214,40 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, false)).target; - await this.iNodeMgr.transact( - async (tran) => { - if (!target) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - syscall: 'lchmod', - }); - } - const targetStat = await this.iNodeMgr.statGet(tran, target); - if ( - this.uid !== permissions.DEFAULT_ROOT_UID && - this.uid !== targetStat.uid - ) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - path: path as string, - syscall: 'lchmod', - }); - } - await this.iNodeMgr.statSetProp( - tran, - target, - 'mode', - (targetStat.mode & constants.S_IFMT) | mode, - ); - }, - target ? [target] : [], - ); + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'lchmod', + }); + } + await this.iNodeMgr.withTransactionF(target, async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'lchmod', + }); + } + const targetStat = await this.iNodeMgr.statGet(target, tran); + if ( + this.uid !== permissions.DEFAULT_ROOT_UID && + this.uid !== targetStat.uid + ) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EPERM, + path: path as string, + syscall: 'lchmod', + }); + } + await this.iNodeMgr.statSetProp( + target, + 'mode', + (targetStat.mode & constants.S_IFMT) | mode, + tran, + ); + }); }, callback); } @@ -1200,40 +1261,45 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, false)).target; - await this.iNodeMgr.transact( - async (tran) => { - if (!target) { + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'lchown', + }); + } + await this.iNodeMgr.withTransactionF(target, async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'lchown', + }); + } + const targetStat = await this.iNodeMgr.statGet(target, tran); + if (this.uid !== permissions.DEFAULT_ROOT_UID) { + // You don't own the file + if (targetStat.uid !== this.uid) { throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, + errno: errno.EPERM, path: path as string, syscall: 'lchown', }); } - const targetStat = await this.iNodeMgr.statGet(tran, target); - if (this.uid !== permissions.DEFAULT_ROOT_UID) { - // You don't own the file - if (targetStat.uid !== this.uid) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - path: path as string, - syscall: 'lchown', - }); - } - // You cannot give files to others - if (this.uid !== uid) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EPERM, - path: path as string, - syscall: 'lchown', - }); - } - // Because we don't have user group hierarchies, we allow chowning to any group + // You cannot give files to others + if (this.uid !== uid) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EPERM, + path: path as string, + syscall: 'lchown', + }); } - await this.iNodeMgr.statSetProp(tran, target, 'uid', uid); - await this.iNodeMgr.statSetProp(tran, target, 'gid', gid); - }, - target ? [target] : [], - ); + // Because we don't have user group hierarchies, we allow chowning to any group + } + await this.iNodeMgr.statSetProp(target, 'uid', uid, tran); + await this.iNodeMgr.statSetProp(target, 'gid', gid, tran); + }); }, callback); } @@ -1249,18 +1315,27 @@ class EncryptedFS { const navigatedExisting = await this.navigate(existingPath, false); const navigatedNew = await this.navigate(newPath, false); const existingTarget = navigatedExisting.target; - await this.iNodeMgr.transact( + if (existingTarget == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: existingPath as string, + dest: newPath as string, + syscall: 'link', + }); + } + await this.iNodeMgr.withTransactionF( + existingTarget, + navigatedNew.dir, async (tran) => { - if (!existingTarget) { + const iNodeData = await this.iNodeMgr.get(existingTarget, tran); + if (iNodeData == null) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, - path: existingPath as string, - dest: newPath as string, syscall: 'link', }); } const existingTargetType = ( - await this.iNodeMgr.get(tran, existingTarget) + await this.iNodeMgr.get(existingTarget, tran) )?.type; if (existingTargetType === 'Directory') { throw new errors.ErrorEncryptedFSError({ @@ -1272,8 +1347,8 @@ class EncryptedFS { } if (!navigatedNew.target) { const newDirStat = await this.iNodeMgr.statGet( - tran, navigatedNew.dir, + tran, ); if (newDirStat.nlink < 2) { throw new errors.ErrorEncryptedFSError({ @@ -1292,21 +1367,21 @@ class EncryptedFS { }); } const index = await this.iNodeMgr.dirGetEntry( - tran, navigatedExisting.dir, navigatedExisting.name, + tran, ); await this.iNodeMgr.dirSetEntry( - tran, navigatedNew.dir, navigatedNew.name, index as INodeIndex, + tran, ); await this.iNodeMgr.statSetProp( - tran, existingTarget, 'ctime', new Date(), + tran, ); } else { throw new errors.ErrorEncryptedFSError({ @@ -1317,9 +1392,6 @@ class EncryptedFS { }); } }, - existingTarget - ? [existingTarget, navigatedNew.dir] - : [navigatedNew.dir], ); }, callback); } @@ -1363,24 +1435,26 @@ class EncryptedFS { syscall: 'lseek', }); } - await this.iNodeMgr.transact( - async (tran) => { - if ( - [ - constants.SEEK_SET, - constants.SEEK_CUR, - constants.SEEK_END, - ].indexOf(seekFlags) === -1 - ) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EINVAL, - syscall: 'lseek', - }); - } - await fd.setPos(tran, position, seekFlags); - }, - fd ? [fd.ino] : [], - ); + await this.iNodeMgr.withTransactionF(fd.ino, async (tran) => { + const iNodeData = await this.iNodeMgr.get(fd.ino, tran); + if (iNodeData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + syscall: 'lseek', + }); + } + if ( + [constants.SEEK_SET, constants.SEEK_CUR, constants.SEEK_END].indexOf( + seekFlags, + ) === -1 + ) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EINVAL, + syscall: 'lseek', + }); + } + await fd.setPos(position, seekFlags, tran); + }); return fd.pos; }, callback); } @@ -1395,24 +1469,36 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, false)).target; - if (target) { - let targetStat; - await this.iNodeMgr.transact( - async (tran) => { - targetStat = await this.iNodeMgr.statGet(tran, target); - }, - [target], - ); - return new Stat({ ...targetStat }); - } else { + if (target == null) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, + path: path as string, syscall: 'lstat', }); } + const targetStat = await this.iNodeMgr.withTransactionF( + target, + async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'lstat', + }); + } + return await this.iNodeMgr.statGet(target, tran); + }, + ); + return new Stat({ ...targetStat }); }, callback); } + /** + * Makes a directory + * + * This call must handle concurrent races to create the directory inode + */ public async mkdir(path: Path, options?: Options | number): Promise; public async mkdir(path: Path, callback: Callback): Promise; public async mkdir( @@ -1442,106 +1528,150 @@ class EncryptedFS { typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; return utils.maybeCallback(async () => { path = this.getPath(path); - // We expect a directory + // If the path has trailing slashes, navigation would traverse into it + // we must trim off these trailing slashes to allow these directories to be removed path = path.replace(/(.+?)\/+$/, '$1'); - let currentDir, navigatedTargetType; - let navigated = await this.navigate(path, true); - let root = true; + let navigated = await this.navigate(path, false); + // Mutable transaction contexts may be inherited across loop iterations + // During concurrent mkdir calls, calls race to create the inode + // One call will win the lock to create the inode, all other calls must coalesce + // Coalescing calls must handle the now existing target, to do so + // It must continue the loop, by restarting the loop with an inherited transaction context + // This ensures that handling the existing inode is consistent + let tran: DBTransaction | null = null; + let tranRelease: ResourceRelease | null = null; + // Loop necessary due to recursive directory creation while (true) { - if (!navigated.target) { - root = false; - if (navigated.remaining && !options.recursive) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - syscall: 'mkdir', - }); - } - let navigatedDirStat; - const dirINode = this.iNodeMgr.inoAllocate(); - await this.iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - this.iNodeMgr.inoDeallocate(dirINode); + if (navigated.target != null) { + // Handle existing target + if (tran == null || tranRelease == null) { + const tranAcquire = this.iNodeMgr.transaction(navigated.target); + [tranRelease, tran] = (await tranAcquire()) as [ + ResourceRelease, + DBTransaction, + ]; + } + let e: Error | undefined; + try { + const targetType = (await this.iNodeMgr.get(navigated.target, tran)) + ?.type; + // If recursive, then loop through the path components + if (!(targetType === 'Directory' && options.recursive)) { + // Target already exists + throw new errors.ErrorEncryptedFSError({ + errno: errno.EEXIST, + path: path as string, + syscall: 'mkdir', }); - navigatedDirStat = await this.iNodeMgr.statGet( - tran, - navigated.dir, - ); - if (navigatedDirStat.nlink < 2) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - syscall: 'mkdir', - }); - } - if (!this.checkPermissions(constants.W_OK, navigatedDirStat)) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, - path: path as string, - syscall: 'mkdir', - }); - } - await this.iNodeMgr.dirCreate( - tran, - dirINode, - { - mode: utils.applyUmask( - options.mode ?? permissions.DEFAULT_DIRECTORY_PERM, - this.umask, - ), - uid: this.uid, - gid: this.gid, - }, - await this.iNodeMgr.dirGetEntry(tran, navigated.dir, '.'), - ); - await this.iNodeMgr.dirSetEntry( - tran, - navigated.dir, - navigated.name, - dirINode, - ); - }, - [navigated.dir, dirINode], - ); - if (navigated.remaining) { - currentDir = dirINode; + } + // No more path components to process + if (!navigated.remaining) { + return; + } + // Continue the navigation process navigated = await this.navigateFrom( - currentDir, + navigated.target, navigated.remaining, true, + undefined, + undefined, + undefined, + // Preserve the transaction context for `navigated.target` + tran, ); - } else { - break; + // Restart the opening procedure with the new target + } catch (e_) { + e = e_; + throw e_; + } finally { + await tranRelease(e); + // Clear the transaction variables + tran = null; + tranRelease = null; } } else { - if ( - (navigated.remaining === '' && navigated.name !== '.') || - (navigated.name === '.' && root) - ) { + // Handle non-existing target + if (navigated.remaining && !options.recursive) { + // Intermediate path component does not exist throw new errors.ErrorEncryptedFSError({ - errno: errno.EEXIST, + errno: errno.ENOENT, path: path as string, syscall: 'mkdir', }); } - const navigatedTarget = navigated.target; - await this.iNodeMgr.transact( - async (tran) => { - navigatedTargetType = ( - await this.iNodeMgr.get(tran, navigatedTarget) - )?.type; - }, - [navigated.target], - ); - if (navigatedTargetType !== 'Directory') { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTDIR, - path: path as string, - syscall: 'mkdir', - }); + const inoAcquire = this.iNodeMgr.inoAllocation(navigated); + const [inoRelease, ino] = (await inoAcquire()) as [ + ResourceRelease, + INodeIndex, + ]; + const tranAcquire = this.iNodeMgr.transaction(ino, navigated.dir); + [tranRelease, tran] = (await tranAcquire()) as [ + ResourceRelease, + DBTransaction, + ]; + // INode may be created while waiting for lock + // Transaction is maintained and not released + // This is to ensure that the already created locks are held + if ((await this.iNodeMgr.get(ino, tran)) != null) { + navigated.target = ino; + await inoRelease(); + continue; + } + let e: Error | undefined; + try { + const navigatedDirStat = await this.iNodeMgr.statGet( + navigated.dir, + tran, + ); + if (navigatedDirStat.nlink < 2) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'mkdir', + }); + } + if (!this.checkPermissions(constants.W_OK, navigatedDirStat)) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EACCES, + path: path as string, + syscall: 'mkdir', + }); + } + await this.iNodeMgr.dirCreate( + ino, + { + mode: utils.applyUmask( + options.mode ?? permissions.DEFAULT_DIRECTORY_PERM, + this.umask, + ), + uid: this.uid, + gid: this.gid, + }, + navigated.dir, + tran, + ); + await this.iNodeMgr.dirSetEntry( + navigated.dir, + navigated.name, + ino, + tran, + ); + } catch (e_) { + e = e_; + throw e_; + } finally { + await tranRelease(e); + await inoRelease(e); + // Clear the transaction variables + tran = null; + tranRelease = null; + } + // No more path components to process + if (!navigated.remaining) { + return; } - break; + // Continue the navigation process + navigated = await this.navigateFrom(ino, navigated.remaining, true); } } }, callback); @@ -1583,7 +1713,7 @@ class EncryptedFS { 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; return possibleChars[Math.floor(Math.random() * possibleChars.length)]; }; - let pathS; + let pathS: string; while (true) { pathS = pathSPrefix.concat( Array.from({ length: 6 }, () => getChar) @@ -1606,6 +1736,11 @@ class EncryptedFS { }, callback); } + /** + * Makes an inode + * + * This call must handle concurrent races to create the inode + */ public async mknod( path: Path, type: number, @@ -1645,13 +1780,19 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const navigated = await this.navigate(path, false); - const iNode = this.iNodeMgr.inoAllocate(); - await this.iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - this.iNodeMgr.inoDeallocate(iNode); - }); - if (navigated.target) { + if (navigated.target != null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EEXIST, + path: path as string, + syscall: 'mknod', + }); + } + await this.iNodeMgr.withNewINodeTransactionF( + navigated, + navigated.dir, + async (ino, tran) => { + // INode may be created while waiting for lock + if ((await this.iNodeMgr.get(ino, tran)) != null) { throw new errors.ErrorEncryptedFSError({ errno: errno.EEXIST, path: path as string, @@ -1659,8 +1800,8 @@ class EncryptedFS { }); } const navigatedDirStat = await this.iNodeMgr.statGet( - tran, navigated.dir, + tran, ); if (navigatedDirStat.nlink < 2) { throw new errors.ErrorEncryptedFSError({ @@ -1679,14 +1820,15 @@ class EncryptedFS { switch (type) { case constants.S_IFREG: await this.iNodeMgr.fileCreate( - tran, - iNode, + ino, { mode: utils.applyUmask(mode, this.umask), uid: this.uid, gid: this.gid, }, this.blockSize, + undefined, + tran, ); break; default: @@ -1697,13 +1839,12 @@ class EncryptedFS { }); } await this.iNodeMgr.dirSetEntry( - tran, navigated.dir, navigated.name, - iNode, + ino, + tran, ); }, - [navigated.dir, iNode], ); }, callback); } @@ -1743,6 +1884,9 @@ class EncryptedFS { }, callback); } + /** + * This call must handle concurrent races to create the file inode + */ protected async _open( path: Path, flags: string | number, @@ -1803,187 +1947,232 @@ class EncryptedFS { throw new TypeError('Unknown file open flag: ' + flags); } } - if (typeof flags !== 'number') { - throw new TypeError('Unknown file open flag: ' + flags); - } + // Creates the file descriptor, used at the very end + const createFd = async ( + flags: number, + target: INodeIndex, + tran: DBTransaction, + ) => { + // Convert file descriptor flags into bitwise permission flags + let access: number; + if (flags & constants.O_RDWR) { + access = constants.R_OK | constants.W_OK; + } else if ((flags & constants.O_WRONLY) | (flags & constants.O_TRUNC)) { + access = constants.W_OK; + } else { + access = constants.R_OK; + } + // Check the permissions + const targetStat = await this.iNodeMgr.statGet(target, tran); + if (!this.checkPermissions(access, targetStat)) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EACCES, + path: path as string, + syscall: 'open', + }); + } + // Returns the created FileDescriptor + try { + return await this.fdMgr.createFd(target, flags); + } catch (e) { + if (e instanceof errors.ErrorEncryptedFSError) { + e.setPaths(path as string); + e.setSyscall('open'); + } + throw e; + } + }; let navigated = await this.navigate(path, false); - let target = navigated.target; - const openFlags = flags; - const openPath = path; - let openRet; - // This is needed for the purpose of symlinks, if the navigated target exists - // and is a symlink we need to go inside and check the target again. So a while - // loop suits us best. In VFS this was easier as the type checking wasn't as strict - await this.iNodeMgr.transact( - async (tran) => { - while (true) { - if (!target) { - // O_CREAT only applies if there's a left over name without any remaining path - if (!navigated.remaining && openFlags & constants.O_CREAT) { - let navigatedDirStat; - const fileINode = this.iNodeMgr.inoAllocate(); - await this.iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - this.iNodeMgr.inoDeallocate(fileINode); - }); - navigatedDirStat = await this.iNodeMgr.statGet( - tran, - navigated.dir, - ); - // Cannot create if the current directory has been unlinked from its parent directory - if (navigatedDirStat.nlink < 2) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - syscall: 'open', - }); - } - if ( - !this.checkPermissions(constants.W_OK, navigatedDirStat) - ) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, - path: path as string, - syscall: 'open', - }); - } - await this.iNodeMgr.fileCreate( - tran, - fileINode, - { - mode: utils.applyUmask(mode, this.umask), - uid: this.uid, - gid: this.gid, - }, - this.blockSize, - ); - await this.iNodeMgr.dirSetEntry( - tran, - navigated.dir, - navigated.name, - fileINode, - ); - }, - [fileINode, navigated.dir], - ); - target = fileINode; - break; - } else { + // Mutable transaction contexts may be inherited across loop iterations + // During concurrent open calls with `O_CREAT`, calls may race to create the inode + // One call will win the lock to create the inode, all other calls must coalesce + // Coalescing calls must handle the now existing target, to do so + // It must continue the loop, by restarting the loop with an inherited transaction context + // This ensures that handling the existing inode is consistent + let raced = false; + let tran: DBTransaction | null = null; + let tranRelease: ResourceRelease | null = null; + // Loop necessary due to following symlinks and optional `O_CREAT` file creation + while (true) { + if (navigated.target != null) { + // Handle existing target + if (tran == null || tranRelease == null) { + const tranAcquire = this.iNodeMgr.transaction(navigated.target); + [tranRelease, tran] = (await tranAcquire()) as [ + ResourceRelease, + DBTransaction, + ]; + } + let e: Error | undefined; + try { + const target = await this.iNodeMgr.get(navigated.target, tran); + if (target == null) { + // Try to find the target again + navigated = await this.navigate(path, false); + continue; + } + const targetType = target.type; + if (targetType === 'Symlink') { + // Cannot be symlink if O_NOFOLLOW + if (flags & constants.O_NOFOLLOW) { throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, + errno: errno.ELOOP, path: path as string, syscall: 'open', }); } + // Follow the symlink + navigated = await this.navigateFrom( + navigated.dir, + navigated.name + navigated.remaining, + true, + undefined, + undefined, + path, + // Only preserve the transaction context if it was inherited + // from a coalesced call, as it would already have be for `navigated.dir` + raced ? tran : undefined, + ); + // Restart the opening procedure with the new target + continue; } else { - const targetType = (await this.iNodeMgr.get(tran, target))?.type; - if (targetType === 'Symlink') { - // Cannot be symlink if O_NOFOLLOW - if (openFlags & constants.O_NOFOLLOW) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ELOOP, - path: path as string, - syscall: 'open', - }); - } - navigated = await this.navigateFrom( - navigated.dir, - navigated.name + navigated.remaining, - true, - undefined, + // Target already exists cannot be created exclusively + if (flags & constants.O_CREAT && flags & constants.O_EXCL) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EEXIST, + path: path as string, + syscall: 'open', + }); + } + // Cannot be directory if write capabilities are requested + if ( + targetType === 'Directory' && + flags & + (constants.O_WRONLY | + (flags & (constants.O_RDWR | (flags & constants.O_TRUNC)))) + ) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EISDIR, + path: path as string, + syscall: 'open', + }); + } + // Must be directory if O_DIRECTORY + if (flags & constants.O_DIRECTORY && targetType !== 'Directory') { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOTDIR, + path: path as string, + syscall: 'open', + }); + } + // Must truncate a file if O_TRUNC + if ( + targetType === 'File' && + flags & constants.O_TRUNC && + flags & (constants.O_WRONLY | constants.O_RDWR) + ) { + await this.iNodeMgr.fileClearData(navigated.target, tran); + await this.iNodeMgr.fileSetBlocks( + navigated.target, + Buffer.alloc(0), + this.blockSize, undefined, - openPath, + tran, ); - target = navigated.target; - } else { - // Target already exists cannot be created exclusively - if ( - openFlags & constants.O_CREAT && - openFlags & constants.O_EXCL - ) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EEXIST, - path: path as string, - syscall: 'open', - }); - } - // Cannot be directory if write capabilities are requested - if ( - targetType === 'Directory' && - openFlags & - (constants.O_WRONLY | - (openFlags & - (constants.O_RDWR | (openFlags & constants.O_TRUNC)))) - ) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EISDIR, - path: path as string, - syscall: 'open', - }); - } - // Must be directory if O_DIRECTORY - if ( - openFlags & constants.O_DIRECTORY && - !(targetType === 'Directory') - ) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTDIR, - path: path as string, - syscall: 'open', - }); - } - // Must truncate a file if O_TRUNC - if ( - openFlags & constants.O_TRUNC && - targetType === 'File' && - openFlags & (constants.O_WRONLY | constants.O_RDWR) - ) { - await this.iNodeMgr.fileClearData(tran, target); - await this.iNodeMgr.fileSetBlocks( - tran, - target, - Buffer.alloc(0), - this.blockSize, - ); - } - break; } + // Terminates loop, creates file descriptor + return await createFd(flags, navigated.target, tran!); } + } catch (e_) { + e = e_; + throw e_; + } finally { + await tranRelease(e); + // Clear the transaction variables + tran = null; + tranRelease = null; } - // Convert file descriptor access flags into bitwise permission flags - let access; - if (openFlags & constants.O_RDWR) { - access = constants.R_OK | constants.W_OK; - } else if ( - (openFlags & constants.O_WRONLY) | - (openFlags & constants.O_TRUNC) - ) { - access = constants.W_OK; - } else { - access = constants.R_OK; - } - const targetStat = await this.iNodeMgr.statGet(tran, target); - if (!this.checkPermissions(access, targetStat)) { + } else { + // Handle non-existing target + if (navigated.remaining || !(flags & constants.O_CREAT)) { + // Intermediate path component does not exist throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, + errno: errno.ENOENT, path: path as string, syscall: 'open', }); } + const inoAcquire = this.iNodeMgr.inoAllocation(navigated); + const [inoRelease, ino] = (await inoAcquire()) as [ + ResourceRelease, + INodeIndex, + ]; + const tranAcquire = this.iNodeMgr.transaction(ino, navigated.dir); + [tranRelease, tran] = (await tranAcquire()) as [ + ResourceRelease, + DBTransaction, + ]; + // INode may be created while waiting for lock + // Transaction is maintained and not released + // This is to ensure that the already created locks are held + if ((await this.iNodeMgr.get(ino, tran)) != null) { + navigated.target = ino; + await inoRelease(); + raced = true; + continue; + } + let e: Error | undefined; try { - openRet = await this.fdMgr.createFd(target, openFlags); - } catch (e) { - if (e instanceof errors.ErrorEncryptedFSError) { - e.setPaths(path as string); - e.setSyscall('open'); + const navigatedDirStat = await this.iNodeMgr.statGet( + navigated.dir, + tran, + ); + // Cannot create if the current directory has been unlinked from its parent directory + if (navigatedDirStat.nlink < 2) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'open', + }); } - throw e; + if (!this.checkPermissions(constants.W_OK, navigatedDirStat)) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EACCES, + path: path as string, + syscall: 'open', + }); + } + await this.iNodeMgr.fileCreate( + ino!, + { + mode: utils.applyUmask(mode, this.umask), + uid: this.uid, + gid: this.gid, + }, + this.blockSize, + undefined, + tran, + ); + await this.iNodeMgr.dirSetEntry( + navigated.dir, + navigated.name, + ino!, + tran, + ); + // Terminates loop, creates file descriptor + return await createFd(flags, ino!, tran!); + } catch (e_) { + e = e_; + throw e_; + } finally { + await tranRelease(e); + await inoRelease(e); + // Clear the transaction variables + tran = null; + tranRelease = null; } - }, - navigated.target ? [navigated.target] : [], - ); - return openRet; + } + } } public async read( @@ -2057,44 +2246,46 @@ class EncryptedFS { }); } - let fdStat; - await this.iNodeMgr.transact( - async (tran) => { - fdStat = await this.iNodeMgr.statGet(tran, fd.ino); - }, - [fd.ino], - ); - if (fdStat.isDirectory()) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EISDIR, - syscall: 'read', - }); - } - const flags = fd.flags; - if (flags & constants.O_WRONLY) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EBADF, - syscall: 'read', - }); - } - if (offset < 0 || offset > buffer.length) { - throw new RangeError('Offset is out of bounds'); - } - if (length < 0 || length > buffer.length) { - throw new RangeError('Length extends beyond buffer'); - } - buffer = this.getBuffer(buffer).slice(offset, offset + length); - let bytesRead; - try { - bytesRead = await fd.read(buffer as Buffer, position); - } catch (e) { - if (e instanceof errors.ErrorEncryptedFSError) { - e.setSyscall('read'); + return await this.iNodeMgr.withTransactionF(fd.ino, async (tran) => { + const iNodeData = await this.iNodeMgr.get(fd.ino, tran); + if (iNodeData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + syscall: 'read', + }); } - throw e; - } - - return bytesRead; + const fdStat = await this.iNodeMgr.statGet(fd.ino, tran); + if (fdStat.isDirectory()) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EISDIR, + syscall: 'read', + }); + } + const flags = fd.flags; + if (flags & constants.O_WRONLY) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBADF, + syscall: 'read', + }); + } + if (offset < 0 || offset > buffer.length) { + throw new RangeError('Offset is out of bounds'); + } + if (length < 0 || length > buffer.length) { + throw new RangeError('Length extends beyond buffer'); + } + buffer = this.getBuffer(buffer).slice(offset, offset + length); + let bytesRead: number; + try { + bytesRead = await fd.read(buffer as Buffer, position, tran); + } catch (e) { + if (e instanceof errors.ErrorEncryptedFSError) { + e.setSyscall('read'); + } + throw e; + } + return bytesRead; + }); }, callback); } @@ -2125,41 +2316,44 @@ class EncryptedFS { typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; return utils.maybeCallback(async () => { path = this.getPath(path); - const navigated = await this.navigate(path, true); - let navigatedTargetType, navigatedTargetStat; - const target = navigated.target; + const target = (await this.navigate(path, true)).target; + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'readdir', + }); + } const navigatedTargetEntries: Array<[string | Buffer, INodeIndex]> = []; - await this.iNodeMgr.transact( - async (tran) => { - if (!target) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - syscall: 'readdir', - }); - } - navigatedTargetType = (await this.iNodeMgr.get(tran, target))?.type; - navigatedTargetStat = await this.iNodeMgr.statGet(tran, target); - if (navigatedTargetType !== 'Directory') { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTDIR, - path: path as string, - syscall: 'readdir', - }); - } - if (!this.checkPermissions(constants.R_OK, navigatedTargetStat)) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, - path: path as string, - syscall: 'readdir', - }); - } - for await (const dirEntry of this.iNodeMgr.dirGet(tran, target)) { - navigatedTargetEntries.push(dirEntry); - } - }, - target ? [target] : [], - ); + await this.iNodeMgr.withTransactionF(target, async (tran) => { + const navigatedTargetData = await this.iNodeMgr.get(target!, tran); + if (navigatedTargetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'readdir', + }); + } + const navigatedTargetType = navigatedTargetData.type; + if (navigatedTargetType !== 'Directory') { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOTDIR, + path: path as string, + syscall: 'readdir', + }); + } + const navigatedTargetStat = await this.iNodeMgr.statGet(target!, tran); + if (!this.checkPermissions(constants.R_OK, navigatedTargetStat)) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EACCES, + path: path as string, + syscall: 'readdir', + }); + } + for await (const dirEntry of this.iNodeMgr.dirGet(target!, tran)) { + navigatedTargetEntries.push(dirEntry); + } + }); return navigatedTargetEntries .filter(([name]) => name !== '.' && name !== '..') .map(([name]) => { @@ -2199,28 +2393,21 @@ class EncryptedFS { typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; return utils.maybeCallback(async () => { options.flag = 'r'; - let fdIndex; + let fdIndex: FdIndex | undefined; try { const buffer = Buffer.allocUnsafe(this.blockSize); let totalBuffer = Buffer.alloc(0); - let bytesRead; - if (typeof file === 'number') { - while (bytesRead !== 0) { - bytesRead = await this.read(file, buffer, 0, buffer.length); - totalBuffer = Buffer.concat([ - totalBuffer, - buffer.slice(0, bytesRead), - ]); - } - } else { - fdIndex = await this.open(file as Path, options.flag); - while (bytesRead !== 0) { - bytesRead = await this.read(fdIndex, buffer, 0, buffer.length); - totalBuffer = Buffer.concat([ - totalBuffer, - buffer.slice(0, bytesRead), - ]); - } + let bytesRead: number | undefined = undefined; + if (typeof file !== 'number') { + fdIndex = await this.open(file, options.flag); + file = fdIndex; + } + while (bytesRead !== 0) { + bytesRead = await this.read(file, buffer, 0, buffer.length); + totalBuffer = Buffer.concat([ + totalBuffer, + buffer.slice(0, bytesRead), + ]); } return options.encoding ? totalBuffer.toString(options.encoding) @@ -2261,17 +2448,25 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, false)).target; - let link; - await this.iNodeMgr.transact( + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'readlink', + }); + } + const link = await this.iNodeMgr.withTransactionF( + target, async (tran) => { - if (!target) { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, path: path as string, syscall: 'readlink', }); } - const targetType = (await this.iNodeMgr.get(tran, target))?.type; + const targetType = (await this.iNodeMgr.get(target, tran))?.type; if (!(targetType === 'Symlink')) { throw new errors.ErrorEncryptedFSError({ errno: errno.EINVAL, @@ -2279,9 +2474,8 @@ class EncryptedFS { syscall: 'readlink', }); } - link = await this.iNodeMgr.symlinkGetLink(tran, target); + return await this.iNodeMgr.symlinkGetLink(target, tran); }, - target ? [target] : [], ); if (options.encoding === 'binary') { return Buffer.from(link); @@ -2321,7 +2515,7 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const navigated = await this.navigate(path, true); - if (!navigated.target) { + if (navigated.target == null) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, path: path as string, @@ -2349,221 +2543,233 @@ class EncryptedFS { newPath = this.getPath(newPath); const navigatedSource = await this.navigate(oldPath, false); const navigatedTarget = await this.navigate(newPath, false); - await this.iNodeMgr.transact( - async (tran) => { - if (!navigatedSource.target || navigatedTarget.remaining) { + if (navigatedSource.target == null || navigatedTarget.remaining) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: oldPath as string, + dest: newPath as string, + syscall: 'rename', + }); + } + // Listing iNodes to lock + const iNodes = [ + navigatedSource.dir, + navigatedSource.target, + // Avoid duplicate dir inodes + ...(navigatedSource.dir !== navigatedTarget.dir + ? [navigatedTarget.dir] + : []), + // Locking target if it exists + ...(navigatedTarget.target != null ? [navigatedTarget.target] : []), + ]; + const sourceTarget = navigatedSource.target; + await this.iNodeMgr.withTransactionF(...iNodes, async (tran) => { + // Check path or target + const sourceTargetData = await this.iNodeMgr.get(sourceTarget, tran); + if (sourceTargetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: oldPath as string, + dest: newPath as string, + syscall: 'rename', + }); + } + if (sourceTargetData.type === 'Directory') { + // If oldPath is a directory, target must be a directory (if it exists) + if (navigatedTarget.target) { + const targetTargetType = ( + await this.iNodeMgr.get(navigatedTarget.target, tran) + )?.type; + if (!(targetTargetType === 'Directory')) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOTDIR, + path: oldPath as string, + dest: newPath as string, + syscall: 'rename', + }); + } + } + // Neither oldPath nor newPath can point to root + if ( + navigatedSource.target === this.rootIno || + navigatedTarget.target === this.rootIno + ) { throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, + errno: errno.EBUSY, path: oldPath as string, dest: newPath as string, syscall: 'rename', }); } - const sourceTarget = navigatedSource.target; - const sourceTargetType = (await this.iNodeMgr.get(tran, sourceTarget)) - ?.type; - if (sourceTargetType === 'Directory') { - // If oldPath is a directory, target must be a directory (if it exists) - if (navigatedTarget.target) { - const targetTargetType = ( - await this.iNodeMgr.get(tran, navigatedTarget.target) - )?.type; - if (!(targetTargetType === 'Directory')) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTDIR, - path: oldPath as string, - dest: newPath as string, - syscall: 'rename', - }); - } + // If the target directory contains elements this cannot be done + // this can be done without read permissions + if (navigatedTarget.target) { + const targetEntries: Array<[string, INodeIndex]> = []; + for await (const entry of this.iNodeMgr.dirGet( + navigatedTarget.target, + tran, + )) { + targetEntries.push(entry); } - // Neither oldPath nor newPath can point to root - if ( - navigatedSource.target === this.rootIno || - navigatedTarget.target === this.rootIno - ) { + if (targetEntries.length - 2) { throw new errors.ErrorEncryptedFSError({ - errno: errno.EBUSY, + errno: errno.ENOTEMPTY, path: oldPath as string, dest: newPath as string, syscall: 'rename', }); } - // If the target directory contains elements this cannot be done - // this can be done without read permissions - if (navigatedTarget.target) { - const targetEntries: Array<[string, INodeIndex]> = []; - for await (const entry of this.iNodeMgr.dirGet( - tran, - navigatedTarget.target, - )) { - targetEntries.push(entry); - } - if (targetEntries.length - 2) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTEMPTY, - path: oldPath as string, - dest: newPath as string, - syscall: 'rename', - }); + } + // If any of the paths used .. or ., then `dir` is not the parent directory + if ( + navigatedSource.name === '.' || + navigatedSource.name === '..' || + navigatedTarget.name === '.' || + navigatedTarget.name === '..' + ) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBUSY, + path: oldPath as string, + dest: newPath as string, + syscall: 'rename', + }); + } + // Cannot rename a source prefix of target + if ( + navigatedSource.pathStack.length < navigatedTarget.pathStack.length + ) { + let prefixOf = true; + for (let i = 0; i < navigatedSource.pathStack.length; ++i) { + if ( + navigatedSource.pathStack[i] !== navigatedTarget.pathStack[i] + ) { + prefixOf = false; + break; } } - // If any of the paths used .. or ., then `dir` is not the parent directory - if ( - navigatedSource.name === '.' || - navigatedSource.name === '..' || - navigatedTarget.name === '.' || - navigatedTarget.name === '..' - ) { + if (prefixOf) { throw new errors.ErrorEncryptedFSError({ - errno: errno.EBUSY, + errno: errno.EINVAL, path: oldPath as string, dest: newPath as string, syscall: 'rename', }); } - // Cannot rename a source prefix of target - if ( - navigatedSource.pathStack.length < - navigatedTarget.pathStack.length - ) { - let prefixOf = true; - for (let i = 0; i < navigatedSource.pathStack.length; ++i) { - if ( - navigatedSource.pathStack[i] !== navigatedTarget.pathStack[i] - ) { - prefixOf = false; - break; - } - } - if (prefixOf) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EINVAL, - path: oldPath as string, - dest: newPath as string, - syscall: 'rename', - }); - } + } + } else { + // If oldPath is not a directory, then newPath cannot be an existing directory + if (navigatedTarget.target) { + const targetTargetType = ( + await this.iNodeMgr.get(navigatedTarget.target, tran) + )?.type; + if (targetTargetType === 'Directory') { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EISDIR, + path: oldPath as string, + dest: newPath as string, + syscall: 'rename', + }); } - } else { - // If oldPath is not a directory, then newPath cannot be an existing directory - if (navigatedTarget.target) { - const targetTargetType = ( - await this.iNodeMgr.get(tran, navigatedTarget.target) - )?.type; - if (targetTargetType === 'Directory') { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EISDIR, + } + } + const sourceDirStat = await this.iNodeMgr.statGet( + navigatedSource.dir, + tran, + ); + const targetDirStat = await this.iNodeMgr.statGet( + navigatedTarget.dir, + tran, + ); + // Both the navigatedSource.dir and navigatedTarget.dir must support write permissions + if ( + !this.checkPermissions(constants.W_OK, sourceDirStat) || + !this.checkPermissions(constants.W_OK, targetDirStat) + ) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EACCES, + path: oldPath as string, + dest: newPath as string, + syscall: 'rename', + }); + } + // If they are in the same directory, it is simple rename + if (navigatedSource.dir === navigatedTarget.dir) { + try { + await this.iNodeMgr.dirResetEntry( + navigatedSource.dir, + navigatedSource.name, + navigatedTarget.name, + tran, + ); + } catch (e) { + if (e instanceof inodesErrors.ErrorINodesInvalidName) { + throw new errors.ErrorEncryptedFSError( + { + errno: errno.ENOENT, path: oldPath as string, dest: newPath as string, syscall: 'rename', - }); - } + }, + { cause: e }, + ); } + throw e; } - const sourceDirStat = await this.iNodeMgr.statGet( + return; + } + const index = (await this.iNodeMgr.dirGetEntry( + navigatedSource.dir, + navigatedSource.name, + tran, + )) as INodeIndex; + const now = new Date(); + if (navigatedTarget.target) { + await this.iNodeMgr.statSetProp( + navigatedTarget.target, + 'ctime', + now, tran, - navigatedSource.dir, ); - const targetDirStat = await this.iNodeMgr.statGet( + await this.iNodeMgr.dirUnsetEntry( + navigatedTarget.dir, + navigatedTarget.name, tran, + ); + await this.iNodeMgr.dirSetEntry( navigatedTarget.dir, + navigatedTarget.name, + index, + tran, ); - // Both the navigatedSource.dir and navigatedTarget.dir must support write permissions - if ( - !this.checkPermissions(constants.W_OK, sourceDirStat) || - !this.checkPermissions(constants.W_OK, targetDirStat) - ) { + await this.iNodeMgr.statSetProp( + navigatedTarget.target, + 'ctime', + now, + tran, + ); + } else { + if (targetDirStat.nlink < 2) { throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, + errno: errno.ENOENT, path: oldPath as string, dest: newPath as string, syscall: 'rename', }); } - // If they are in the same directory, it is simple rename - if (navigatedSource.dir === navigatedTarget.dir) { - try { - await this.iNodeMgr.dirResetEntry( - tran, - navigatedSource.dir, - navigatedSource.name, - navigatedTarget.name, - ); - } catch (e) { - if (e instanceof inodesErrors.ErrorINodesInvalidName) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: oldPath as string, - dest: newPath as string, - syscall: 'rename', - }); - } - throw e; - } - return; - } - const index = (await this.iNodeMgr.dirGetEntry( - tran, - navigatedSource.dir, - navigatedSource.name, - )) as INodeIndex; - const now = new Date(); - if (navigatedTarget.target) { - await this.iNodeMgr.statSetProp( - tran, - navigatedTarget.target, - 'ctime', - now, - ); - await this.iNodeMgr.dirUnsetEntry( - tran, - navigatedTarget.dir, - navigatedTarget.name, - ); - await this.iNodeMgr.dirSetEntry( - tran, - navigatedTarget.dir, - navigatedTarget.name, - index, - ); - await this.iNodeMgr.statSetProp( - tran, - navigatedTarget.target, - 'ctime', - now, - ); - } else { - if (targetDirStat.nlink < 2) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: oldPath as string, - dest: newPath as string, - syscall: 'rename', - }); - } - await this.iNodeMgr.dirSetEntry( - tran, - navigatedTarget.dir, - navigatedTarget.name, - index, - ); - } - await this.iNodeMgr.statSetProp(tran, sourceTarget, 'ctime', now); - await this.iNodeMgr.dirUnsetEntry( + await this.iNodeMgr.dirSetEntry( + navigatedTarget.dir, + navigatedTarget.name, + index, tran, - navigatedSource.dir, - navigatedSource.name, ); - }, - navigatedTarget.target - ? navigatedSource.target - ? [navigatedTarget.target, navigatedSource.target] - : [navigatedTarget.target] - : navigatedSource.target - ? [navigatedSource.target] - : [], - ); + } + await this.iNodeMgr.statSetProp(sourceTarget, 'ctime', now, tran); + await this.iNodeMgr.dirUnsetEntry( + navigatedSource.dir, + navigatedSource.name, + tran, + ); + }); }, callback); } @@ -2592,6 +2798,14 @@ class EncryptedFS { // we must trim off these trailing slashes to allow these directories to be removed path = path.replace(/(.+?)\/+$/, '$1'); const navigated = await this.navigate(path, false); + // This is for if the path resolved to root + if (!navigated.name) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EBUSY, + path: path as string, + syscall: 'rmdir', + }); + } // On linux, when .. is used, the parent directory becomes unknown // in that case, they return with ENOTEMPTY // but the directory may in fact be empty @@ -2603,156 +2817,105 @@ class EncryptedFS { syscall: 'rmdir', }); } - let dirStat; - const targetEntries: Array<[string | Buffer, INodeIndex]> = []; - const dir = navigated.dir; - await this.iNodeMgr.transact( - async (tran) => { - // This is for if the path resolved to root - if (!navigated.name) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EBUSY, - path: path as string, - syscall: 'rmdir', - }); - } - if (!navigated.target) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - syscall: 'rmdir', - }); - } - const target = navigated.target; - const targetType = (await this.iNodeMgr.get(tran, target))?.type; - dirStat = await this.iNodeMgr.statGet(tran, dir); - for await (const entry of this.iNodeMgr.dirGet(tran, target)) { - targetEntries.push(entry); - } - if (!(targetType === 'Directory')) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTDIR, - path: path as string, - syscall: 'rmdir', - }); - } - }, - navigated.target ? [navigated.target, navigated.dir] : [navigated.dir], - ); - if (targetEntries.length - 2) { + if (navigated.target == null) { + // If recursive, then this is acceptable if (options.recursive) { - for (const entry of targetEntries) { - if (entry[0] !== '.' && entry[0] !== '..') { - await this._rmdir( - utils.pathJoin(path as string, entry[0] as string), - ); - } - } - } else { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOTEMPTY, - path: path as string, - syscall: 'rmdir', - }); + return; } + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'rmdir', + }); } - await this.iNodeMgr.transact( - async (tran) => { - if (!this.checkPermissions(constants.W_OK, dirStat)) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, - path: path as string, - syscall: 'rmdir', - }); - } - await this.iNodeMgr.dirUnsetEntry(tran, dir, navigated.name); - }, - navigated.target ? [navigated.target, navigated.dir] : [navigated.dir], + const tranAcquire = this.iNodeMgr.transaction( + navigated.target, + navigated.dir, ); - }, callback); - } - - protected async _rmdir(path: Path): Promise { - path = this.getPath(path); - // If the path has trailing slashes, navigation would traverse into it - // we must trim off these trailing slashes to allow these directories to be removed - path = path.replace(/(.+?)\/+$/, '$1'); - const navigated = await this.navigate(path, false); - // On linux, when .. is used, the parent directory becomes unknown - // in that case, they return with ENOTEMPTY - // but the directory may in fact be empty - // for this edge case, we instead use EINVAL - if (navigated.name === '.' || navigated.name === '..') { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EINVAL, - path: path as string, - syscall: 'rmdir', - }); - } - let dirStat, targetType; - const targetEntries: Array<[string | Buffer, INodeIndex]> = []; - const dir = navigated.dir; - await this.iNodeMgr.transact( - async (tran) => { - // This is for if the path resolved to root - if (!navigated.name) { + const [tranRelease, tran] = await tranAcquire(); + const targetEntries: Array<[string, INodeIndex]> = []; + let e: Error | undefined; + try { + // Handle existing target + const target = navigated.target as INodeIndex; + const targetType = (await this.iNodeMgr.get(target, tran))?.type; + if (targetType == null) { throw new errors.ErrorEncryptedFSError({ - errno: errno.EBUSY, + errno: errno.ENOENT, path: path as string, syscall: 'rmdir', }); } - if (!navigated.target) { + if (targetType !== 'Directory') { throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, + errno: errno.ENOTDIR, path: path as string, syscall: 'rmdir', }); } - targetType = (await this.iNodeMgr.get(tran, navigated.target))?.type; - dirStat = await this.iNodeMgr.statGet(tran, dir); - }, - navigated.target ? [navigated.target, navigated.dir] : [navigated.dir], - ); - if (targetType === 'Directory') { - await this.iNodeMgr.transact(async (tran) => { - if (!navigated.target) { + // EACCES is thrown when write permission is denied on parent directory + const navigatedDirStat = await this.iNodeMgr.statGet( + navigated.dir, + tran, + ); + if (!this.checkPermissions(constants.W_OK, navigatedDirStat)) { throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, + errno: errno.EACCES, path: path as string, - syscall: 'rmdir', + syscall: 'mkdir', }); } - for await (const entry of this.iNodeMgr.dirGet( - tran, - navigated.target, - )) { + for await (const entry of this.iNodeMgr.dirGet(target, tran)) { targetEntries.push(entry); } - if (targetEntries.length - 2) { - for (const entry of targetEntries) { - if (entry[0] !== '.' && entry[0] !== '..') { - await this._rmdir( - utils.pathJoin(path as string, entry[0] as string), - ); - } - } + // If 2 entries (`.` and `..`), then it is an empty directory + if (targetEntries.length === 2) { + await this.iNodeMgr.dirUnsetEntry( + navigated.dir, + navigated.name, + tran, + ); + // Finished deletion + return; } - }); - } - await this.iNodeMgr.transact( - async (tran) => { - if (!this.checkPermissions(constants.W_OK, dirStat)) { + // Directory is not-empty, and if it is non-recursive, then this is an error + if (!options.recursive) { throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, + errno: errno.ENOTEMPTY, path: path as string, syscall: 'rmdir', }); } - await this.iNodeMgr.dirUnsetEntry(tran, dir, navigated.name); - }, - navigated.target ? [navigated.target, navigated.dir] : [navigated.dir], - ); + } catch (e_) { + e = e_; + throw e_; + } finally { + await tranRelease(e); + } + // Now we recursively delete our entries + // each deletion occurs within their own transaction context + for (const [entryName] of targetEntries) { + if (entryName === '.' || entryName === '..') { + continue; + } + const entryPath = utils.pathJoin(path, entryName); + try { + await this.unlink(entryPath); + } catch (e) { + if (!(e instanceof errors.ErrorEncryptedFSError)) { + throw e; + } + if (e.code === errno.EISDIR.code) { + // Is a directory, propagate recursive deletion + await this.rmdir(entryPath, options); + } else { + throw e; + } + } + } + // After deleting all entries, attempt to delete the same directory again + await this.rmdir(path, options); + }, callback); } public async stat(path: Path): Promise; @@ -2765,25 +2928,36 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, true)).target; - if (target) { - let targetStat; - await this.iNodeMgr.transact( - async (tran) => { - targetStat = await this.iNodeMgr.statGet(tran, target); - }, - [target], - ); - return new Stat({ ...targetStat }); - } else { + if (target == null) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, path: path as string, syscall: 'stat', }); } + const targetStat = await this.iNodeMgr.withTransactionF( + target, + async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'stat', + }); + } + return await this.iNodeMgr.statGet(target, tran); + }, + ); + return new Stat({ ...targetStat }); }, callback); } + /** + * Makes a symlink + * + * This call must handle concurrent races to create the symlink inode + */ public async symlink( dstPath: Path, srcPath: Path, @@ -2820,47 +2994,7 @@ class EncryptedFS { }); } const navigated = await this.navigate(srcPath, false); - if (!navigated.target) { - const symlinkINode = this.iNodeMgr.inoAllocate(); - await this.iNodeMgr.transact( - async (tran) => { - const dirStat = await this.iNodeMgr.statGet(tran, navigated.dir); - if (dirStat.nlink < 2) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: srcPath as string, - dest: dstPath as string, - syscall: 'symlink', - }); - } - if (!this.checkPermissions(constants.W_OK, dirStat)) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, - path: srcPath as string, - dest: dstPath as string, - syscall: 'symlink', - }); - } - await this.iNodeMgr.symlinkCreate( - tran, - symlinkINode, - { - mode: permissions.DEFAULT_SYMLINK_PERM, - uid: this.uid, - gid: this.gid, - }, - dstPath as string, - ); - await this.iNodeMgr.dirSetEntry( - tran, - navigated.dir, - navigated.name, - symlinkINode, - ); - }, - [navigated.dir, symlinkINode], - ); - } else { + if (navigated.target != null) { throw new errors.ErrorEncryptedFSError({ errno: errno.EEXIST, path: srcPath as string, @@ -2868,6 +3002,54 @@ class EncryptedFS { syscall: 'symlink', }); } + await this.iNodeMgr.withNewINodeTransactionF( + navigated, + navigated.dir, + async (symlinkIno, tran) => { + // INode may be created while waiting for lock + if ((await this.iNodeMgr.get(symlinkIno, tran)) != null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EEXIST, + path: srcPath as string, + dest: dstPath as string, + syscall: 'symlink', + }); + } + const dirStat = await this.iNodeMgr.statGet(navigated.dir, tran); + if (dirStat.nlink < 2) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: srcPath as string, + dest: dstPath as string, + syscall: 'symlink', + }); + } + if (!this.checkPermissions(constants.W_OK, dirStat)) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EACCES, + path: srcPath as string, + dest: dstPath as string, + syscall: 'symlink', + }); + } + await this.iNodeMgr.symlinkCreate( + symlinkIno, + { + mode: permissions.DEFAULT_SYMLINK_PERM, + uid: this.uid, + gid: this.gid, + }, + dstPath as string, + tran, + ); + await this.iNodeMgr.dirSetEntry( + navigated.dir, + navigated.name, + symlinkIno, + tran, + ); + }, + ); }, callback); } @@ -2897,7 +3079,7 @@ class EncryptedFS { await this.ftruncate(file, len); } else { file = this.getPath(file as Path); - let fdIndex; + let fdIndex: FdIndex | undefined; try { fdIndex = await this.open(file, constants.O_WRONLY); await this.ftruncate(fdIndex, len); @@ -2913,7 +3095,7 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const navigated = await this.navigate(path, false); - if (!navigated.target) { + if (navigated.target == null) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, path: path as string, @@ -2921,10 +3103,21 @@ class EncryptedFS { }); } const target = navigated.target; - await this.iNodeMgr.transact( + await this.iNodeMgr.withTransactionF( + ...(navigated.dir === navigated.target + ? [navigated.dir] + : [navigated.dir, navigated.target]), async (tran) => { - const dirStat = await this.iNodeMgr.statGet(tran, navigated.dir); - const targetType = (await this.iNodeMgr.get(tran, target))?.type; + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'unlink', + }); + } + const dirStat = await this.iNodeMgr.statGet(navigated.dir, tran); + const targetType = (await this.iNodeMgr.get(target, tran))?.type; if (!this.checkPermissions(constants.W_OK, dirStat)) { throw new errors.ErrorEncryptedFSError({ errno: errno.EACCES, @@ -2940,16 +3133,13 @@ class EncryptedFS { }); } const now = new Date(); - await this.iNodeMgr.statSetProp(tran, target, 'ctime', now); + await this.iNodeMgr.statSetProp(target, 'ctime', now, tran); await this.iNodeMgr.dirUnsetEntry( - tran, navigated.dir, navigated.name, + tran, ); }, - navigated.dir === navigated.target - ? [navigated.dir] - : [navigated.dir, navigated.target], ); }, callback); } @@ -2964,37 +3154,42 @@ class EncryptedFS { return utils.maybeCallback(async () => { path = this.getPath(path); const target = (await this.navigate(path, true)).target; - await this.iNodeMgr.transact( - async (tran) => { - if (!target) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.ENOENT, - path: path as string, - syscall: 'utimes', - }); - } - let newAtime; - let newMtime; - if (typeof atime === 'number') { - newAtime = new Date(atime * 1000); - } else if (typeof atime === 'string') { - newAtime = new Date(parseInt(atime) * 1000); - } else if (atime instanceof Date) { - newAtime = atime; - } - if (typeof mtime === 'number') { - newMtime = new Date(mtime * 1000); - } else if (typeof mtime === 'string') { - newMtime = new Date(parseInt(mtime) * 1000); - } else if (mtime instanceof Date) { - newMtime = mtime; - } - await this.iNodeMgr.statSetProp(tran, target, 'atime', newAtime); - await this.iNodeMgr.statSetProp(tran, target, 'mtime', newMtime); - await this.iNodeMgr.statSetProp(tran, target, 'ctime', new Date()); - }, - target ? [target] : [], - ); + if (target == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'utimes', + }); + } + await this.iNodeMgr.withTransactionF(target, async (tran) => { + const targetData = await this.iNodeMgr.get(target, tran); + if (targetData == null) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.ENOENT, + path: path as string, + syscall: 'utimes', + }); + } + let newAtime: Date; + let newMtime: Date; + if (typeof atime === 'number') { + newAtime = new Date(atime * 1000); + } else if (typeof atime === 'string') { + newAtime = new Date(parseInt(atime) * 1000); + } else { + newAtime = atime; + } + if (typeof mtime === 'number') { + newMtime = new Date(mtime * 1000); + } else if (typeof mtime === 'string') { + newMtime = new Date(parseInt(mtime) * 1000); + } else { + newMtime = mtime; + } + await this.iNodeMgr.statSetProp(target, 'atime', newAtime, tran); + await this.iNodeMgr.statSetProp(target, 'mtime', newMtime, tran); + await this.iNodeMgr.statSetProp(target, 'ctime', new Date(), tran); + }); }, callback); } @@ -3079,7 +3274,7 @@ class EncryptedFS { syscall: 'write', }); } - let buffer; + let buffer: Buffer; if (typeof data === 'string') { position = typeof offsetOrPos === 'number' ? offsetOrPos : undefined; lengthOrEncoding = @@ -3104,10 +3299,13 @@ class EncryptedFS { return await fd.write(buffer, position); } catch (e) { if (e instanceof RangeError) { - throw new errors.ErrorEncryptedFSError({ - errno: errno.EFBIG, - syscall: 'write', - }); + throw new errors.ErrorEncryptedFSError( + { + errno: errno.EFBIG, + syscall: 'write', + }, + { cause: e }, + ); } if (e instanceof errors.ErrorEncryptedFSError) { e.setSyscall('write'); @@ -3157,7 +3355,7 @@ class EncryptedFS { callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback; return utils.maybeCallback(async () => { - let fdIndex; + let fdIndex: FdIndex; options.flag = 'w'; const buffer = this.getBuffer(data, options.encoding); let fdCheck = false; @@ -3178,10 +3376,10 @@ class EncryptedFS { /** * Navigates the filesystem tree from root. * You can interpret the results like: - * !target => Non-existent segment - * name === '' => Target is at root - * name === '.' => dir is the same as target - * name === '..' => dir is a child directory + * target == null => Non-existent segment + * name === '' => Target is at root + * name === '.' => dir is the same as target + * name === '..' => dir is a child directory */ protected async navigate( pathS: string, @@ -3235,8 +3433,20 @@ class EncryptedFS { /** * Navigates the filesystem tree from a given directory. - * You should not use this directly unless you first call _navigate and pass the remaining path to _navigateFrom. + * You should not use this directly unless you first call _navigate + * and pass the remaining path to _navigateFrom. * Note that the pathStack is always the full path to the target. + * + * Each navigateFrom call usually has its own transaction context + * It does not preserve transaction context on recursive calls + * to `this.navigate` or `this.navigateFrom`. + * This is because navigation on from a particular inode is a + * self-contained operation. + * + * Callers must pass an existing transaction context if they are navigating + * from an inode that was just created within that transaction context, this + * is necessary because otherwise there will be a deadlock when + * `this.navigateFrom` starts their own transaction context on that inode */ protected async navigateFrom( curdir: INodeIndex, @@ -3245,51 +3455,71 @@ class EncryptedFS { activeSymlinks: Set = new Set(), pathStack: Array = [], origPathS: string = pathS, + tran?: DBTransaction, ): Promise { + // If pathS is empty, there is nothing from the curdir to navigate to if (!pathS) { throw new errors.ErrorEncryptedFSError({ errno: errno.ENOENT, path: origPathS, }); } - let curdirStat; - await this.iNodeMgr.transact( - async (tran) => { - curdirStat = await this.iNodeMgr.statGet(tran, curdir); - }, - [curdir], - ); - if (!this.checkPermissions(constants.X_OK, curdirStat)) { + // Only commit the transaction if this call created the transaction + let tranRelease: ResourceRelease | undefined; + if (tran == null) { + const tranAcquire = this.iNodeMgr.transaction(curdir); + [tranRelease, tran] = (await tranAcquire()) as [ + ResourceRelease, + DBTransaction, + ]; + } + const curDirData = this.iNodeMgr.get(curdir, tran); + if (curDirData == null) { throw new errors.ErrorEncryptedFSError({ - errno: errno.EACCES, - path: origPathS, + errno: errno.ENOENT, }); } - const parse = this.parsePath(pathS); - if (parse.segment !== '.') { - if (parse.segment === '..') { - // This is a noop if the pathStack is empty - pathStack.pop(); - } else { - pathStack.push(parse.segment); + let targetType: INodeType; + let nextDir: INodeIndex | undefined; + let nextPath: string; + let e: Error | undefined; + try { + const curdirStat = await this.iNodeMgr.statGet(curdir, tran); + if (!this.checkPermissions(constants.X_OK, curdirStat)) { + throw new errors.ErrorEncryptedFSError({ + errno: errno.EACCES, + path: origPathS, + }); } - } - let nextDir, nextPath, target, targetType; - await this.iNodeMgr.transact( - async (tran) => { - if (parse.segment === '..' && curdir === this.rootIno) { - target = curdir; + const parse = this.parsePath(pathS); + if (parse.segment !== '.') { + if (parse.segment === '..') { + // This is a noop if the pathStack is empty + pathStack.pop(); } else { - target = await this.iNodeMgr.dirGetEntry(tran, curdir, parse.segment); + pathStack.push(parse.segment); } - }, - [curdir], - ); - if (target) { - await this.iNodeMgr.transact(async (tran) => { - const targetData = await this.iNodeMgr.get(tran, target); - targetType = targetData?.type; - }); + } + let target: INodeIndex | undefined; + if (parse.segment === '..' && curdir === this.rootIno) { + // At the root directory, `..` refers back to the root inode + target = curdir; + } else { + // Acquire the target inode for the entry + target = await this.iNodeMgr.dirGetEntry(curdir, parse.segment, tran); + } + if (target == null) { + // Target does not exist, return an `undefined` target + return { + dir: curdir, + target: undefined, + name: parse.segment, + remaining: parse.rest, + pathStack: pathStack, + }; + } + // If the target exists, then the the target type must exist in the same transaction + targetType = (await this.iNodeMgr.get(target, tran))!.type; switch (targetType) { case 'File': { if (!parse.rest) { @@ -3343,50 +3573,43 @@ class EncryptedFS { activeSymlinks.add(target); } // Although symlinks should not have an empty links, it's still handled correctly here - let targetLinks; - await this.iNodeMgr.transact(async (tran) => { - targetLinks = await this.iNodeMgr.symlinkGetLink(tran, target); - }); - nextPath = utils.pathJoin(targetLinks, parse.rest); - if (nextPath[0] === '/') { - return this.navigate( - nextPath, - resolveLastLink, - activeSymlinks, - origPathS, - ); - } else { + const targetLink = await this.iNodeMgr.symlinkGetLink(target, tran); + nextPath = utils.pathJoin(targetLink, parse.rest); + if (nextPath[0] !== '/') { pathStack.pop(); nextDir = curdir; } } break; - default: - return { - dir: curdir, - target: undefined, - name: parse.segment, - remaining: parse.rest, - pathStack: pathStack, - }; } + } catch (e_) { + e = e_; + throw e_; + } finally { + if (tranRelease != null) { + await tranRelease(e); + } + } + if (targetType === 'Symlink' && nextPath[0] === '/') { + // Only symlinks can have absolute next paths + // in which case we start the navigate from the root + return this.navigate( + nextPath, + resolveLastLink, + activeSymlinks, + origPathS, + ); } else { - return { - dir: curdir, - target: undefined, - name: parse.segment, - remaining: parse.rest, - pathStack: pathStack, - }; + // Otherwise we are navigating relative to the `nextDir` + return this.navigateFrom( + nextDir!, + nextPath, + resolveLastLink, + activeSymlinks, + pathStack, + origPathS, + ); } - return this.navigateFrom( - nextDir, - nextPath, - resolveLastLink, - activeSymlinks, - pathStack, - origPathS, - ); } /** @@ -3430,7 +3653,7 @@ class EncryptedFS { if (path instanceof Buffer) { return path.toString(); } - if (typeof path === 'object' && typeof path.pathname === 'string') { + if (typeof path === 'object') { return this.getPathFromURL(path); } throw new TypeError('path must be a string or Buffer or URL'); @@ -3490,42 +3713,7 @@ class EncryptedFS { data.byteOffset + data.byteLength, ); } - if (typeof data === 'string') { - return Buffer.from(data, encoding); - } - throw new TypeError('data must be Buffer or Uint8Array or string'); - } - - /** - * Performs validation of the database key - */ - protected static async setupCanary(db: DB) { - // Uses the root level that's already available - try { - const deadbeefData: Buffer = await db.db.get('canary'); - try { - const deadbeef = await db.deserializeDecrypt( - deadbeefData, - false, - ); - if (deadbeef !== 'deadbeef') throw new errors.ErrorEncryptedFSKey(); - } catch (e) { - if (e instanceof dbErrors.ErrorDBDecrypt) { - throw new errors.ErrorEncryptedFSKey(); - } - throw e; - } - } catch (e) { - if (e.notFound) { - // If the stored value didn't exist, its a new db and so store and proceed - await db.put([], 'canary', 'deadbeef'); - } else { - // Db must be stopped otherwise the lock will persist and - // other efs instances cannot be created - await db.stop(); - throw e; - } - } + return Buffer.from(data, encoding); } } diff --git a/src/errors.ts b/src/errors.ts index 57b503fd..a2661dc8 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,42 +1,62 @@ -import { CustomError } from 'ts-custom-error'; +import type { POJO } from './types'; +import { AbstractError } from '@matrixai/errors'; -class ErrorEncryptedFS extends CustomError {} +class ErrorEncryptedFS extends AbstractError { + static description = 'EncryptedFS error'; +} + +class ErrorEncryptedFSRunning extends ErrorEncryptedFS { + static description = 'EncryptedFS is running'; +} -class ErrorEncryptedFSRunning extends ErrorEncryptedFS {} +class ErrorEncryptedFSNotRunning extends ErrorEncryptedFS { + static description = 'EncryptedFS is not running'; +} -class ErrorEncryptedFSNotRunning extends ErrorEncryptedFS {} +class ErrorEncryptedFSDestroyed extends ErrorEncryptedFS { + static description = 'EncryptedFS is destroyed'; +} -class ErrorEncryptedFSDestroyed extends ErrorEncryptedFS {} +class ErrorEncryptedFSKey extends ErrorEncryptedFS { + static description = 'EncryptedFS failed canary check'; +} -class ErrorEncryptedFSKey extends ErrorEncryptedFS {} +class ErrorEncryptedFSError extends ErrorEncryptedFS { + static description = 'EncryptedFS filesystem error'; -class ErrorEncryptedFSError extends ErrorEncryptedFS { protected _errno: number; protected _code: string; protected _description: string; protected _syscall?: string; - constructor({ - errno, - path, - dest, - syscall, - }: { - errno: { - errno: number; - code: string; - description: string; - }; - path?: string; - dest?: string; - syscall?: string; - }) { + constructor( + { + errno, + path, + dest, + syscall, + }: { + errno: { + errno: number; + code: string; + description: string; + }; + path?: string; + dest?: string; + syscall?: string; + }, + options: { + timestamp?: Date; + data?: POJO; + cause?: T; + } = {}, + ) { let message = errno.code + ': ' + errno.description; if (path != null) { message += ', ' + path; if (dest != null) message += ' -> ' + dest; } - super(message); + super(message, options); this._errno = errno.errno; this._code = errno.code; this._description = errno.description; diff --git a/src/fd/FileDescriptor.ts b/src/fd/FileDescriptor.ts index f4bd9143..9ef6d34f 100644 --- a/src/fd/FileDescriptor.ts +++ b/src/fd/FileDescriptor.ts @@ -1,7 +1,7 @@ import type { INodeType, INodeIndex } from '../inodes/types'; import type { DBTransaction } from '@matrixai/db'; - import type { INodeManager } from '../inodes'; +import { Lock } from '@matrixai/async-locks'; import * as errorsFd from './errors'; import * as constants from '../constants'; import * as utils from '../utils'; @@ -12,6 +12,10 @@ class FileDescriptor { protected _ino: INodeIndex; protected _flags: number; protected _pos: number; + /** + * Ensure mutual exclusion for this file descriptor's methods + */ + protected lock: Lock = new Lock(); constructor(iNodeMgr: INodeManager, ino: INodeIndex, flags: number) { this._iNodeMgr = iNodeMgr; @@ -54,46 +58,53 @@ class FileDescriptor { * Sets the file descriptor position. */ public async setPos( - tran: DBTransaction, pos: number, flags: number = constants.SEEK_SET, + tran?: DBTransaction, ): Promise { - let newPos; - const type = await tran.get( - this._iNodeMgr.iNodesDomain, - inodesUtils.iNodeId(this._ino), - ); - const size = await this._iNodeMgr.statGetProp(tran, this._ino, 'size'); - switch (type) { - case 'File': - case 'Directory': - { - switch (flags) { - case constants.SEEK_SET: - newPos = pos; - break; - case constants.SEEK_CUR: - newPos = this._pos + pos; - break; - case constants.SEEK_END: - newPos = size + pos; - break; - default: - newPos = this._pos; - } - if (newPos < 0) { - throw new errorsFd.ErrorFileDescriptorInvalidPosition( - `Position ${newPos} is not reachable`, - ); - } - this._pos = newPos; - } - break; - default: - throw new errorsFd.ErrorFileDescriptorInvalidINode( - `Invalid INode Type ${type}`, - ); + if (tran == null) { + return await this._iNodeMgr.withTransactionF(this._ino, async (tran) => + this.setPos(pos, flags, tran), + ); } + await this.lock.withF(async () => { + let newPos; + const type = await tran.get([ + ...this._iNodeMgr.iNodesDbPath, + inodesUtils.iNodeId(this._ino), + ]); + const size = await this._iNodeMgr.statGetProp(this._ino, 'size', tran); + switch (type) { + case 'File': + case 'Directory': + { + switch (flags) { + case constants.SEEK_SET: + newPos = pos; + break; + case constants.SEEK_CUR: + newPos = this._pos + pos; + break; + case constants.SEEK_END: + newPos = size + pos; + break; + default: + newPos = this._pos; + } + if (newPos < 0) { + throw new errorsFd.ErrorFileDescriptorInvalidPosition( + `Position ${newPos} is not reachable`, + ); + } + this._pos = newPos; + } + break; + default: + throw new errorsFd.ErrorFileDescriptorInvalidINode( + `Invalid INode Type ${type}`, + ); + } + }); } /* @@ -103,119 +114,113 @@ class FileDescriptor { * current position. The function will read up to the length of * the provided buffer. */ - public async read(buffer: Buffer, position?: number): Promise { - // Check that the iNode is a valid type (for now, only File iNodes) - let type, blkSize; - await this._iNodeMgr.transact( - async (tran) => { - type = await tran.get( - this._iNodeMgr.iNodesDomain, - inodesUtils.iNodeId(this._ino), - ); - blkSize = await this._iNodeMgr.statGetProp(tran, this._ino, 'blksize'); - }, - [this._ino], - ); - // Determine the starting position within the data - let currentPos = this._pos; - if (position != null) { - currentPos = position; + public async read( + buffer: Buffer, + position?: number, + tran?: DBTransaction, + ): Promise { + if (tran == null) { + return await this._iNodeMgr.withTransactionF(this._ino, async (tran) => + this.read(buffer, position, tran), + ); } - let bytesRead = buffer.byteLength; - switch (type) { - case 'File': - { - // Get the starting block index - const blockStartIdx = utils.blockIndexStart(blkSize, currentPos); - // Determines the offset of blocks - const blockOffset = utils.blockOffset(blkSize, currentPos); - // Determines the number of blocks - const blockLength = utils.blockLength( - blkSize, - blockOffset, - bytesRead, - ); - // Get the ending block index - const blockEndIdx = utils.blockIndexEnd(blockStartIdx, blockLength); - // Get the cursor offset for the start and end blocks - const blockCursorStart = utils.blockOffset(blkSize, currentPos); - const blockCursorEnd = utils.blockOffset( - blkSize, - currentPos + bytesRead - 1, - ); - // Initialise counters for the read buffer and block position - let retBufferPos = 0; - let blockCounter = blockStartIdx; - await this._iNodeMgr.transact( - async (tran) => { - // Iterate over the blocks in the database - for await (const block of this._iNodeMgr.fileGetBlocks( - tran, - this._ino, - blkSize, - blockStartIdx, - blockEndIdx + 1, - )) { - // Add the block to the return buffer (handle the start and end blocks) - if ( - blockCounter === blockStartIdx && - blockCounter === blockEndIdx - ) { - retBufferPos += block.copy( - buffer, - retBufferPos, - blockCursorStart, - blockCursorEnd + 1, - ); - } else if (blockCounter === blockStartIdx) { - retBufferPos += block.copy( - buffer, - retBufferPos, - blockCursorStart, - ); - } else if (blockCounter === blockEndIdx) { - retBufferPos += block.copy( - buffer, - retBufferPos, - 0, - blockCursorEnd + 1, - ); - } else { - retBufferPos += block.copy(buffer, retBufferPos); - } - - // Increment the block counter - blockCounter++; + return await this.lock.withF(async () => { + // Check that the iNode is a valid type (for now, only File iNodes) + const type = await tran.get([ + ...this._iNodeMgr.iNodesDbPath, + inodesUtils.iNodeId(this._ino), + ]); + const blkSize = await this._iNodeMgr.statGetProp( + this._ino, + 'blksize', + tran, + ); + // Determine the starting position within the data + let currentPos = this._pos; + if (position != null) { + currentPos = position; + } + let bytesRead = buffer.byteLength; + switch (type) { + case 'File': + { + // Get the starting block index + const blockStartIdx = utils.blockIndexStart(blkSize, currentPos); + // Determines the offset of blocks + const blockOffset = utils.blockOffset(blkSize, currentPos); + // Determines the number of blocks + const blockLength = utils.blockLength( + blkSize, + blockOffset, + bytesRead, + ); + // Get the ending block index + const blockEndIdx = utils.blockIndexEnd(blockStartIdx, blockLength); + // Get the cursor offset for the start and end blocks + const blockCursorStart = utils.blockOffset(blkSize, currentPos); + const blockCursorEnd = utils.blockOffset( + blkSize, + currentPos + bytesRead - 1, + ); + // Initialise counters for the read buffer and block position + let retBufferPos = 0; + let blockCounter = blockStartIdx; + // Iterate over the blocks in the database + for await (const block of this._iNodeMgr.fileGetBlocks( + this._ino, + blkSize, + blockStartIdx, + blockEndIdx + 1, + tran, + )) { + // Add the block to the return buffer (handle the start and end blocks) + if ( + blockCounter === blockStartIdx && + blockCounter === blockEndIdx + ) { + retBufferPos += block.copy( + buffer, + retBufferPos, + blockCursorStart, + blockCursorEnd + 1, + ); + } else if (blockCounter === blockStartIdx) { + retBufferPos += block.copy( + buffer, + retBufferPos, + blockCursorStart, + ); + } else if (blockCounter === blockEndIdx) { + retBufferPos += block.copy( + buffer, + retBufferPos, + 0, + blockCursorEnd + 1, + ); + } else { + retBufferPos += block.copy(buffer, retBufferPos); } - }, - [this._ino], - ); - - // Set the access time in the metadata - await this._iNodeMgr.transact( - async (tran) => { - const now = new Date(); - await this._iNodeMgr.statSetProp(tran, this._ino, 'atime', now); - }, - [this._ino], + // Increment the block counter + blockCounter++; + } + // Set the access time in the metadata + const now = new Date(); + await this._iNodeMgr.statSetProp(this._ino, 'atime', now, tran); + bytesRead = retBufferPos; + } + break; + default: + throw new errorsFd.ErrorFileDescriptorInvalidINode( + `Invalid INode Type ${type}`, ); - - bytesRead = retBufferPos; - } - break; - default: - throw new errorsFd.ErrorFileDescriptorInvalidINode( - `Invalid INode Type ${type}`, - ); - } - - // If the default position used, increment by the bytes read in - if (position == null) { - this._pos = currentPos + bytesRead; - } - - // Return the number of bytes read in - return bytesRead; + } + // If the default position used, increment by the bytes read in + if (position == null) { + this._pos = currentPos + bytesRead; + } + // Return the number of bytes read in + return bytesRead; + }); } /** @@ -226,215 +231,200 @@ class FileDescriptor { buffer: Buffer, position?: number, extraFlags: number = 0, + tran?: DBTransaction, ): Promise { - // Check that the iNode is a valid type - let type, blkSize; - await this._iNodeMgr.transact( - async (tran) => { - type = await tran.get( - this._iNodeMgr.iNodesDomain, - inodesUtils.iNodeId(this._ino), - ); - blkSize = await this._iNodeMgr.statGetProp(tran, this._ino, 'blksize'); - }, - [this._ino], - ); - - // Determine the starting position within the data - let currentPos = this._pos; - if (position != null) { - currentPos = position; + if (tran == null) { + return await this._iNodeMgr.withTransactionF(this._ino, async (tran) => + this.write(buffer, position, extraFlags, tran), + ); } - - let bytesWritten = 0; - switch (type) { - case 'File': - { - if ((this._flags | extraFlags) & constants.O_APPEND) { - let idx, value; - // To append we check the idx and length of the last block - await this._iNodeMgr.transact( - async (tran) => { - [idx, value] = await this._iNodeMgr.fileGetLastBlock( + return await this.lock.withF(async () => { + // Check that the iNode is a valid type + const type = await tran.get([ + ...this._iNodeMgr.iNodesDbPath, + inodesUtils.iNodeId(this._ino), + ]); + const blkSize = await this._iNodeMgr.statGetProp( + this._ino, + 'blksize', + tran, + ); + // Determine the starting position within the data + let currentPos = this._pos; + if (position != null) { + currentPos = position; + } + let bytesWritten = 0; + switch (type) { + case 'File': + { + if ((this._flags | extraFlags) & constants.O_APPEND) { + // To append we check the idx and length of the last block + const [idx, value] = await this._iNodeMgr.fileGetLastBlock( + this._ino, + tran, + ); + if (value.byteLength === blkSize) { + // If the last block is full, begin writing from the next block index + await this._iNodeMgr.fileSetBlocks( + this._ino, + buffer, + blkSize, + idx + 1, tran, + ); + } else if (value.byteLength + buffer.byteLength > blkSize) { + // If the last block is not full and additional data will exceed block size + // Copy the bytes until block size is reached and write into the last block at offset + const startBuffer = Buffer.alloc(blkSize - value.byteLength); + buffer.copy(startBuffer); + const writeBytes = await this._iNodeMgr.fileWriteBlock( this._ino, + startBuffer, + idx, + value.byteLength, + tran, ); - if (value.byteLength === blkSize) { - // If the last block is full, begin writing from the next block index - await this._iNodeMgr.fileSetBlocks( - tran, + // Copy the remaining bytes and write this into the next block(s) + const endBuffer = Buffer.alloc(buffer.byteLength - writeBytes); + buffer.copy(endBuffer, 0, writeBytes); + await this._iNodeMgr.fileSetBlocks( + this._ino, + endBuffer, + blkSize, + idx + 1, + tran, + ); + } else { + // If the last block is not full and additional data will not exceed block size + // Write the data into this block at the offset + await this._iNodeMgr.fileWriteBlock( + this._ino, + buffer, + idx, + value.byteLength, + tran, + ); + } + bytesWritten = buffer.byteLength; + // Move the cursor to the end of the existing data + currentPos = idx * blkSize + value.byteLength; + } else { + // Get the starting block index + const blockStartIdx = utils.blockIndexStart(blkSize, currentPos); + // Determines the offset of blocks + const blockOffset = utils.blockOffset(blkSize, currentPos); + // Determines the number of blocks + const blockLength = utils.blockLength( + blkSize, + blockOffset, + buffer.byteLength, + ); + // Get the ending block index + const blockEndIdx = utils.blockIndexEnd( + blockStartIdx, + blockLength, + ); + // Get the cursors for the start and end blocks + const blockCursorStart = utils.blockOffset(blkSize, currentPos); + const blockCursorEnd = utils.blockOffset( + blkSize, + currentPos + buffer.byteLength - 1, + ); + // Initialise write buffer and block position counters + let writeBufferPos = 0; + let blockCounter = blockStartIdx; + for (const idx of utils.range(blockStartIdx, blockEndIdx + 1)) { + // For each data segment write the data to the index in the database + if ( + blockCounter === blockStartIdx && + blockCounter === blockEndIdx + ) { + // If this block is both the start and end block, write the data in at the offset + writeBufferPos += await this._iNodeMgr.fileWriteBlock( this._ino, buffer, - blkSize, - idx + 1, - ); - } else if (value.byteLength + buffer.byteLength > blkSize) { - // If the last block is not full and additional data will exceed block size - // Copy the bytes until block size is reached and write into the last block at offset - const startBuffer = Buffer.alloc(blkSize - value.byteLength); - buffer.copy(startBuffer); - const writeBytes = await this._iNodeMgr.fileWriteBlock( + idx, + blockCursorStart, tran, + ); + } else if (blockCounter === blockStartIdx) { + // If this block is only the start block, copy the relevant bytes from the data to + // satisfy the offset and write these to the block at the offset + const copyBuffer = Buffer.alloc(blkSize - blockCursorStart); + buffer.copy(copyBuffer); + writeBufferPos += await this._iNodeMgr.fileWriteBlock( this._ino, - startBuffer, + copyBuffer, idx, - value.byteLength, - ); - // Copy the remaining bytes and write this into the next block(s) - const endBuffer = Buffer.alloc( - buffer.byteLength - writeBytes, - ); - buffer.copy(endBuffer, 0, writeBytes); - await this._iNodeMgr.fileSetBlocks( + blockCursorStart, tran, + ); + } else if (blockCounter === blockEndIdx) { + // If this block is only the end block, copy the relevant bytes from the data to + // satisfy the offset and write these to the block + const copyBuffer = Buffer.alloc(blockCursorEnd + 1); + buffer.copy(copyBuffer, 0, writeBufferPos); + writeBufferPos += await this._iNodeMgr.fileWriteBlock( this._ino, - endBuffer, - blkSize, - idx + 1, + copyBuffer, + idx, + undefined, + tran, ); } else { - // If the last block is not full and additional data will not exceed block size - // Write the data into this block at the offset - await this._iNodeMgr.fileWriteBlock( - tran, + // If the block is a middle block, overwrite the whole block with the relevant bytes + const copyBuffer = Buffer.alloc(blkSize); + buffer.copy(copyBuffer, 0, writeBufferPos); + writeBufferPos += await this._iNodeMgr.fileWriteBlock( this._ino, - buffer, + copyBuffer, idx, - value.byteLength, + undefined, + tran, ); } - bytesWritten = buffer.byteLength; - }, - [this._ino], - ); - // Move the cursor to the end of the existing data - currentPos = idx * blkSize + value.byteLength; - } else { - // Get the starting block index - const blockStartIdx = utils.blockIndexStart(blkSize, currentPos); - // Determines the offset of blocks - const blockOffset = utils.blockOffset(blkSize, currentPos); - // Determines the number of blocks - const blockLength = utils.blockLength( - blkSize, - blockOffset, - buffer.byteLength, - ); - // Get the ending block index - const blockEndIdx = utils.blockIndexEnd(blockStartIdx, blockLength); - - // Get the cursors for the start and end blocks - const blockCursorStart = utils.blockOffset(blkSize, currentPos); - const blockCursorEnd = utils.blockOffset( - blkSize, - currentPos + buffer.byteLength - 1, + // Increment the block counter + blockCounter++; + } + // Set the amount of bytes written + bytesWritten = writeBufferPos; + } + // Set the modified time, changed time, size and blocks of the file iNode + const now = new Date(); + await this._iNodeMgr.statSetProp(this._ino, 'mtime', now, tran); + await this._iNodeMgr.statSetProp(this._ino, 'ctime', now, tran); + // Calculate the size of the new data + let size = await this._iNodeMgr.statGetProp( + this._ino, + 'size', + tran, ); - - // Initialise write buffer and block position counters - let writeBufferPos = 0; - let blockCounter = blockStartIdx; - - await this._iNodeMgr.transact( - async (tran) => { - for (const idx of utils.range(blockStartIdx, blockEndIdx + 1)) { - // For each data segment write the data to the index in the database - if ( - blockCounter === blockStartIdx && - blockCounter === blockEndIdx - ) { - // If this block is both the start and end block, write the data in at the offset - writeBufferPos += await this._iNodeMgr.fileWriteBlock( - tran, - this._ino, - buffer, - idx, - blockCursorStart, - ); - } else if (blockCounter === blockStartIdx) { - // If this block is only the start block, copy the relevant bytes from the data to - // satisfy the offset and write these to the block at the offset - const copyBuffer = Buffer.alloc(blkSize - blockCursorStart); - buffer.copy(copyBuffer); - writeBufferPos += await this._iNodeMgr.fileWriteBlock( - tran, - this._ino, - copyBuffer, - idx, - blockCursorStart, - ); - } else if (blockCounter === blockEndIdx) { - // If this block is only the end block, copy the relevant bytes from the data to - // satisfy the offset and write these to the block - const copyBuffer = Buffer.alloc(blockCursorEnd + 1); - buffer.copy(copyBuffer, 0, writeBufferPos); - writeBufferPos += await this._iNodeMgr.fileWriteBlock( - tran, - this._ino, - copyBuffer, - idx, - ); - } else { - // If the block is a middle block, overwrite the whole block with the relevant bytes - const copyBuffer = Buffer.alloc(blkSize); - buffer.copy(copyBuffer, 0, writeBufferPos); - writeBufferPos += await this._iNodeMgr.fileWriteBlock( - tran, - this._ino, - copyBuffer, - idx, - ); - } - - // Increment the block counter - blockCounter++; - } - }, - [this._ino], + size = + currentPos + buffer.byteLength > size + ? currentPos + buffer.byteLength + : size; + await this._iNodeMgr.statSetProp(this._ino, 'size', size, tran); + await this._iNodeMgr.statSetProp( + this._ino, + 'blocks', + Math.ceil(size / blkSize), + tran, ); - // Set the amount of bytes written - bytesWritten = writeBufferPos; } - - // Set the modified time, changed time, size and blocks of the file iNode - await this._iNodeMgr.transact( - async (tran) => { - const now = new Date(); - await this._iNodeMgr.statSetProp(tran, this._ino, 'mtime', now); - await this._iNodeMgr.statSetProp(tran, this._ino, 'ctime', now); - // Calculate the size of the new data - let size = await this._iNodeMgr.statGetProp( - tran, - this._ino, - 'size', - ); - size = - currentPos + buffer.byteLength > size - ? currentPos + buffer.byteLength - : size; - await this._iNodeMgr.statSetProp(tran, this._ino, 'size', size); - await this._iNodeMgr.statSetProp( - tran, - this._ino, - 'blocks', - Math.ceil(size / blkSize), - ); - }, - [this._ino], + break; + default: + throw new errorsFd.ErrorFileDescriptorInvalidINode( + `Invalid INode Type ${type}`, ); - } - break; - default: - throw new errorsFd.ErrorFileDescriptorInvalidINode( - `Invalid INode Type ${type}`, - ); - } - - // If the default position used, increment by the bytes read in - if (position == null) { - this._pos = currentPos + bytesWritten; - } - // Return the number of bytes written - return bytesWritten; + } + // If the default position used, increment by the bytes read in + if (position == null) { + this._pos = currentPos + bytesWritten; + } + // Return the number of bytes written + return bytesWritten; + }); } } diff --git a/src/fd/FileDescriptorManager.ts b/src/fd/FileDescriptorManager.ts index 53107ce5..207fd8e0 100644 --- a/src/fd/FileDescriptorManager.ts +++ b/src/fd/FileDescriptorManager.ts @@ -72,12 +72,9 @@ class FileDescriptorManager { if (fd) { this._fds.delete(fdIndex); this._counter.deallocate(fdIndex); - await this._iNodeMgr.transact( - async (tran) => { - await this._iNodeMgr.unref(tran, fd.ino); - }, - [fd.ino], - ); + await this._iNodeMgr.withTransactionF(fd.ino, async (tran) => { + await this._iNodeMgr.unref(fd.ino, tran); + }); } return; } diff --git a/src/fd/errors.ts b/src/fd/errors.ts index dd91df42..bf84b747 100644 --- a/src/fd/errors.ts +++ b/src/fd/errors.ts @@ -1,16 +1,19 @@ -import { CustomError } from 'ts-custom-error'; +import { AbstractError } from '@matrixai/errors'; -class ErrorFileDescriptor extends CustomError {} +class ErrorFileDescriptor extends AbstractError { + static description = 'File descriptor error'; +} -class ErrorFileDescriptorMissingINode extends ErrorFileDescriptor {} +class ErrorFileDescriptorInvalidPosition extends ErrorFileDescriptor { + static description = 'File descriptor position cannot be less than 0'; +} -class ErrorFileDescriptorInvalidPosition extends ErrorFileDescriptor {} - -class ErrorFileDescriptorInvalidINode extends ErrorFileDescriptor {} +class ErrorFileDescriptorInvalidINode extends ErrorFileDescriptor { + static description = 'File descriptor cannot handle unknown INode type'; +} export { ErrorFileDescriptor, - ErrorFileDescriptorMissingINode, ErrorFileDescriptorInvalidPosition, ErrorFileDescriptorInvalidINode, }; diff --git a/src/inodes/INodeManager.ts b/src/inodes/INodeManager.ts index 0c7fb7d0..8acc65f7 100644 --- a/src/inodes/INodeManager.ts +++ b/src/inodes/INodeManager.ts @@ -1,15 +1,23 @@ -import type { MutexInterface } from 'async-mutex'; -import type { INodeIndex, INodeId, INodeType, INodeData } from './types'; -import type { DB, DBDomain, DBLevel, DBTransaction } from '@matrixai/db'; +import type { DB, DBTransaction, LevelPath } from '@matrixai/db'; +import type { ResourceAcquire } from '@matrixai/resources'; +import type { + INodeIndex, + INodeId, + INodeType, + INodeData, + BufferId, +} from './types'; +import type { Ref } from '../types'; import type { StatProps } from '../Stat'; - -import { Mutex } from 'async-mutex'; -import Counter from 'resource-counter'; import Logger from '@matrixai/logger'; import { CreateDestroyStartStop, ready, } from '@matrixai/async-init/dist/CreateDestroyStartStop'; +import { Lock, LockBox } from '@matrixai/async-locks'; +import { withF, withG } from '@matrixai/resources'; +import { utils as dbUtils } from '@matrixai/db'; +import Counter from 'resource-counter'; import * as inodesUtils from './utils'; import * as inodesErrors from './errors'; import Stat from '../Stat'; @@ -24,8 +32,8 @@ type SymlinkParams = Partial>; interface INodeManager extends CreateDestroyStartStop {} @CreateDestroyStartStop( - new inodesErrors.ErrorINodesRunning(), - new inodesErrors.ErrorINodesDestroyed(), + new inodesErrors.ErrorINodeManagerRunning(), + new inodesErrors.ErrorINodeManagerDestroyed(), ) class INodeManager { public static async createINodeManager({ @@ -48,69 +56,38 @@ class INodeManager { return iNodeMgr; } - public mgrDomain: DBDomain; - public iNodesDomain: DBDomain; - public statsDomain: DBDomain; - public dataDomain: DBDomain; - public dirsDomain: DBDomain; - public linkDomain: DBDomain; - public gcDomain: DBDomain; + public readonly mgrDbPath: LevelPath = [this.constructor.name]; + public readonly iNodesDbPath: LevelPath = [this.constructor.name, 'inodes']; + public readonly statsDbPath: LevelPath = [this.constructor.name, 'stats']; + public readonly dataDbPath: LevelPath = [this.constructor.name, 'data']; + public readonly dirsDbPath: LevelPath = [this.constructor.name, 'dir']; + public readonly linkDbPath: LevelPath = [this.constructor.name, 'link']; + public readonly gcDbPath: LevelPath = [this.constructor.name, 'gc']; protected logger: Logger; - protected _db: DB; - protected mgrDb: DBLevel; - protected iNodesDb: DBLevel; - protected statsDb: DBLevel; - protected dataDb: DBLevel; - protected dirsDb: DBLevel; - protected linkDb: DBLevel; - protected gcDb: DBLevel; - protected counter: Counter = new Counter(1); + protected db: DB; + protected iNodeCounter: Counter = new Counter(1); + protected iNodeAllocations: Map> = new Map(); protected refs: Map = new Map(); - protected locks: Map = new Map(); + protected locks: LockBox = new LockBox(); constructor({ db, logger }: { db: DB; logger: Logger }) { this.logger = logger; - this._db = db; + this.db = db; } public async start({ fresh = false }: { fresh?: boolean }): Promise { this.logger.info(`Starting ${this.constructor.name}`); - const mgrDomain: DBDomain = [INodeManager.name]; - const iNodesDomain: DBDomain = [mgrDomain[0], 'inodes']; - const statsDomain: DBDomain = [mgrDomain[0], 'stat']; - const dataDomain: DBDomain = [mgrDomain[0], 'data']; - const dirsDomain: DBDomain = [mgrDomain[0], 'dir']; - const linkDomain: DBDomain = [mgrDomain[0], 'link']; - const gcDomain: DBDomain = [mgrDomain[0], 'gc']; - const mgrDb = await this.db.level(mgrDomain[0]); - const iNodesDb = await this.db.level(iNodesDomain[1], mgrDb); - const statsDb = await this.db.level(statsDomain[1], mgrDb); - const dataDb = await this.db.level(dataDomain[1], mgrDb); - const dirsDb = await this.db.level(dirsDomain[1], mgrDb); - const linkDb = await this.db.level(linkDomain[1], mgrDb); - const gcDb = await this.db.level(gcDomain[1], mgrDb); if (fresh) { - await mgrDb.clear(); + await this.db.clear(this.mgrDbPath); } // Populate the inode counter with pre-existing inodes - for await (const k of iNodesDb.createKeyStream()) { - this.counter.allocate(inodesUtils.uniNodeId(k as INodeId)); - } - this.mgrDomain = mgrDomain; - this.iNodesDomain = iNodesDomain; - this.statsDomain = statsDomain; - this.dataDomain = dataDomain; - this.dirsDomain = dirsDomain; - this.linkDomain = linkDomain; - this.gcDomain = gcDomain; - this.mgrDb = mgrDb; - this.iNodesDb = iNodesDb; - this.statsDb = statsDb; - this.dataDb = dataDb; - this.dirsDb = dirsDb; - this.linkDb = linkDb; - this.gcDb = gcDb; + for await (const [k] of this.db.iterator( + { values: false }, + this.iNodesDbPath, + )) { + this.iNodeCounter.allocate(inodesUtils.uniNodeId(k as INodeId)); + } // Clean up all dangling inodes that could not be removed due to references // This only has effect when `this.stop` was not called during a prior instance await this.gcAll(); @@ -126,172 +103,360 @@ class INodeManager { // Clean up all dangling inodes that could not be removed due to references await this.gcAll(); // Reset the inode counter, it will be repopulated on start - this.counter = new Counter(1); + this.iNodeCounter = new Counter(1); // Reset the references this.refs.clear(); - // Reset the locks - this.locks.clear(); this.logger.info(`Stopped ${this.constructor.name}`); } public async destroy(): Promise { this.logger.info(`Destroying ${this.constructor.name}`); - // If the DB was stopped, the existing sublevel `this.mgrDb` will not be valid - // Therefore we recreate the sublevel here - const mgrDb = await this.db.level(this.mgrDomain[0]); - await mgrDb.clear(); + await this.db.clear(this.mgrDbPath); this.logger.info(`Destroyed ${this.constructor.name}`); } /** * Delete iNodes that were scheduled for deletion * These iNodes could not be deleted because of an existing reference - * This is used during `this.start` and `this.stop` + * This is used during `this.start` and `this.stop`, and thus does not use any locks * This must only be called when there are no active `this.refs` or `this.locks` */ protected async gcAll(): Promise { - for await (const k of this.gcDb.createKeyStream()) { - await this.db.transact(async (tran) => { + await withF([this.db.transaction()], async ([tran]) => { + for await (const [k] of tran.iterator({ values: false }, this.gcDbPath)) { const ino = inodesUtils.uniNodeId(k as INodeId); - const type = (await tran.get( - this.iNodesDomain, + // Snapshot doesn't need to be used because `this.gcAll` is only executed at `this.stop` + const type = (await tran.get([ + ...this.iNodesDbPath, inodesUtils.iNodeId(ino), - ))!; + ]))!; // Delete the on-disk state switch (type) { case 'File': - await this._fileDestroy(tran, ino); + await this._fileDestroy(ino, tran); break; case 'Directory': - await this._dirDestroy(tran, ino); + await this._dirDestroy(ino, tran); break; case 'Symlink': - await this._symlinkDestroy(tran, ino); + await this._symlinkDestroy(ino, tran); break; } tran.queueSuccess(() => { this.refs.delete(ino); - this.locks.delete(ino); this.inoDeallocate(ino); }); - }); - } - } - - get db(): DB { - return this._db; + } + }); } public inoAllocate(): INodeIndex { - return this.counter.allocate(); + return this.iNodeCounter.allocate(); } public inoDeallocate(ino: INodeIndex): void { - return this.counter.deallocate(ino); + return this.iNodeCounter.deallocate(ino); } /** - * By default will not lock anything + * INodeIndex allocation resource + * This resource represents potentially INodeIndex being allocated + * The navigated parameter tells us where the first hardlink for this INodeIndex will be set + * If left to be undefined, it is assumed that you are allocating the root INodeIndex + * Concurrent call with same navigated parameter will result in the same INodeIndex result + * This is essential to enable mutual-exclusion */ - @ready(new inodesErrors.ErrorINodesNotRunning()) - public async transact( - f: (t: DBTransaction) => Promise, - inos: Array = [], - ) { - // Will lock nothing by default - return await this.db.transact(f, inos.map(this.getLock.bind(this))); + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public inoAllocation( + navigated?: Readonly<{ dir: INodeIndex; name: string }>, + ): ResourceAcquire { + let key: string; + if (navigated == null) { + key = ''; + } else { + key = navigated.dir + navigated.name; + } + return async () => { + let inoRef = this.iNodeAllocations.get(key); + if (inoRef != null) { + inoRef.count++; + } else { + inoRef = { + value: this.inoAllocate(), + count: 1, + }; + this.iNodeAllocations.set(key, inoRef); + } + return [ + async (e) => { + // Only deallocate if there was an error while using inode allocation + if (e != null) { + this.iNodeCounter.deallocate(inoRef!.value); + } + // Remove the inode allocations entry if the count reaches 0 + if (--inoRef!.count <= 0) { + this.iNodeAllocations.delete(key); + } + }, + inoRef.value, + ]; + }; + } + + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public transaction( + ...inos: Array + ): ResourceAcquire { + return async () => { + const locksAcquire = this.locks.lock( + ...inos.map<[INodeIndex, typeof Lock]>((ino) => [ino, Lock]), + ); + const transactionAcquire = this.db.transaction(); + const [locksRelease] = await locksAcquire(); + let transactionRelease, tran; + try { + [transactionRelease, tran] = await transactionAcquire(); + } catch (e) { + await locksRelease(); + throw e; + } + return [ + async () => { + await transactionRelease(); + await locksRelease(); + }, + tran, + ]; + }; } - protected getLock(ino: INodeIndex): MutexInterface { - let lock = this.locks.get(ino); - if (lock != null) return lock; - lock = new Mutex(); - this.locks.set(ino, lock); - return lock; + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public async withTransactionF( + ...params: [ + ...inos: Array, + f: (tran: DBTransaction) => Promise, + ] + ): Promise { + const f = params.pop() as (tran: DBTransaction) => Promise; + const lockRequests = (params as Array).map< + [INodeIndex, typeof Lock] + >((ino) => [ino, Lock]); + return withF( + [this.db.transaction(), this.locks.lock(...lockRequests)], + ([tran]) => f(tran), + ); + } + + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public withTransactionG( + ...params: [ + ...inos: Array, + g: (tran: DBTransaction) => AsyncGenerator, + ] + ): AsyncGenerator { + const g = params.pop() as ( + tran: DBTransaction, + ) => AsyncGenerator; + const lockRequests = (params as Array).map< + [INodeIndex, typeof Lock] + >((ino) => [ino, Lock]); + return withG( + [this.db.transaction(), this.locks.lock(...lockRequests)], + ([tran]) => g(tran), + ); + } + + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public async withNewINodeTransactionF( + ...params: + | [ + ...inos: Array, + f: (ino: INodeIndex, tran: DBTransaction) => Promise, + ] + | [ + navigated: Readonly<{ dir: INodeIndex; name: string }>, + ...inos: Array, + f: (ino: INodeIndex, tran: DBTransaction) => Promise, + ] + ): Promise { + const f = params.pop() as ( + ino: INodeIndex, + tran: DBTransaction, + ) => Promise; + let navigated: Readonly<{ dir: INodeIndex; name: string }> | undefined; + if (typeof params[0] !== 'number') { + navigated = params.shift() as Readonly<{ dir: INodeIndex; name: string }>; + } + const lockRequests = (params as Array).map< + [INodeIndex, typeof Lock] + >((ino) => [ino, Lock]); + return withF( + [ + this.inoAllocation(navigated), + ([ino]: [INodeIndex]) => + this.locks.lock([ino, Lock], ...lockRequests)(), + this.db.transaction(), + ], + ([ino, _, tran]) => f(ino, tran), + ); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public withNewINodeTransactionG( + ...params: + | [ + ...inos: Array, + g: ( + ino: INodeIndex, + tran: DBTransaction, + ) => AsyncGenerator, + ] + | [ + navigated: Readonly<{ dir: INodeIndex; name: string }>, + ...inos: Array, + g: ( + ino: INodeIndex, + tran: DBTransaction, + ) => AsyncGenerator, + ] + ): AsyncGenerator { + const g = params.pop() as ( + ino: INodeIndex, + tran: DBTransaction, + ) => AsyncGenerator; + let navigated: Readonly<{ dir: INodeIndex; name: string } | undefined>; + if (typeof params[0] !== 'number') { + navigated = params.shift() as Readonly<{ dir: INodeIndex; name: string }>; + } + const lockRequests = (params as Array).map< + [INodeIndex, typeof Lock] + >((ino) => [ino, Lock]); + return withG( + [ + this.inoAllocation(navigated), + ([ino]: [INodeIndex]) => + this.locks.lock([ino, Lock], ...lockRequests)(), + this.db.transaction(), + ], + ([ino, _, tran]) => g(ino, tran), + ); + } + + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async fileCreate( - tran: DBTransaction, ino: INodeIndex, params: FileParams, blkSize: number, data?: Buffer, + tran?: DBTransaction, ): Promise { - const statDomain = [...this.statsDomain, ino.toString()]; + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.fileCreate(ino, params, blkSize, data, tran), + ); + } + const statPath = [...this.statsDbPath, ino.toString()]; const mode = constants.S_IFREG | ((params.mode ?? 0) & ~constants.S_IFMT); - await this.iNodeCreate(tran, 'File', { - ...params, - ino, - mode, - }); - await this.statSetProp(tran, ino, 'blksize', blkSize); + await this.iNodeCreate( + 'File', + { + ...params, + ino, + mode, + }, + tran, + ); + await this.statSetProp(ino, 'blksize', blkSize, tran); if (data) { - await this.fileSetBlocks(tran, ino, data, blkSize); - await tran.put(statDomain, 'size', data.length); - await tran.put(statDomain, 'blocks', Math.ceil(data.length / blkSize)); + await this.fileSetBlocks(ino, data, blkSize, 0, tran); + await tran.put([...statPath, 'size'], data.length); + await tran.put([...statPath, 'blocks'], Math.ceil(data.length / blkSize)); } } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async dirCreate( - tran: DBTransaction, ino: INodeIndex, params: DirectoryParams, parent?: INodeIndex, + tran?: DBTransaction, ): Promise { + if (tran == null) { + return this.withTransactionF( + ino, + ...(parent != null ? [parent] : []), + async (tran) => this.dirCreate(ino, params, parent, tran), + ); + } const mode = constants.S_IFDIR | ((params.mode ?? 0) & ~constants.S_IFMT); - const dirDomain = [...this.dirsDomain, ino.toString()]; + const dirPath = [...this.dirsDbPath, ino.toString()]; let nlink: number; if (parent == null) { // Root can never be garbage collected nlink = 2; parent = ino; if ((await this.dirGetRoot(tran)) != null) { - throw new inodesErrors.ErrorINodesDuplicateRoot(); + throw new inodesErrors.ErrorINodesDuplicateRoot( + `Cannot create directory INode ${ino} as the root INode`, + ); } - await this.dirSetRoot(tran, ino); + await this.dirSetRoot(ino, tran); } else { - if ((await this.get(tran, parent)) == null) { - throw new inodesErrors.ErrorINodesParentMissing(); + if ((await this.get(parent, tran)) == null) { + throw new inodesErrors.ErrorINodesParentMissing( + `Cannot create directory INode ${ino} with missing parent INode ${parent}`, + ); } nlink = 1; } - await this.iNodeCreate(tran, 'Directory', { - ...params, - ino, - mode, - nlink, - }); + await this.iNodeCreate( + 'Directory', + { + ...params, + ino, + mode, + nlink, + }, + tran, + ); if (nlink === 1) { - await this.link(tran, parent!); + await this.link(parent!, tran); } - await tran.put(dirDomain, '.', ino); - await tran.put(dirDomain, '..', parent); + await tran.put([...dirPath, '.'], ino); + await tran.put([...dirPath, '..'], parent); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async symlinkCreate( - tran: DBTransaction, ino: INodeIndex, params: SymlinkParams, link: string, + tran?: DBTransaction, ): Promise { + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.symlinkCreate(ino, params, link, tran), + ); + } const mode = constants.S_IFLNK | ((params.mode ?? 0) & ~constants.S_IFMT); - await this.iNodeCreate(tran, 'Symlink', { - ...params, - ino, - mode, - }); - await tran.put(this.linkDomain, inodesUtils.iNodeId(ino), link); + await this.iNodeCreate( + 'Symlink', + { + ...params, + ino, + mode, + }, + tran, + ); + await tran.put([...this.linkDbPath, inodesUtils.iNodeId(ino)], link); } protected async iNodeCreate( - tran: DBTransaction, type: INodeType, params: INodeParams, + tran: DBTransaction, ): Promise { - const statDomain = [...this.statsDomain, params.ino.toString()]; + const statPath = [...this.statsDbPath, params.ino.toString()]; params.dev = params.dev ?? 0; params.nlink = params.nlink ?? 0; params.uid = params.uid ?? permissions.DEFAULT_ROOT_UID; @@ -306,11 +471,10 @@ class INodeManager { params.ctime = params.ctime ?? now; params.birthtime = params.birthtime ?? now; await tran.put( - this.iNodesDomain, - inodesUtils.iNodeId(params.ino as INodeIndex), + [...this.iNodesDbPath, inodesUtils.iNodeId(params.ino as INodeIndex)], type, ); - await tran.put(statDomain, 'ino', params.ino); + await tran.put([...statPath, 'ino'], params.ino); for (const [key, value] of Object.entries(params)) { switch (key) { case 'dev': @@ -322,82 +486,98 @@ class INodeManager { case 'size': case 'blksize': case 'blocks': - await tran.put(statDomain, key, value); + await tran.put([...statPath, key], value); break; case 'atime': case 'mtime': case 'ctime': case 'birthtime': - await tran.put(statDomain, key, (value as Date).getTime()); + await tran.put([...statPath, key], (value as Date).getTime()); break; } } } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async fileDestroy( - tran: DBTransaction, ino: INodeIndex, + tran?: DBTransaction, ): Promise { - return this._fileDestroy(tran, ino); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.fileDestroy(ino, tran), + ); + } + return this._fileDestroy(ino, tran); } protected async _fileDestroy( - tran: DBTransaction, ino: INodeIndex, + tran: DBTransaction, ): Promise { - const dataDomain = [...this.dataDomain, ino.toString()]; - const dataDb = await this.db.level(ino.toString(), this.dataDb); - for await (const k of dataDb.createKeyStream()) { - await tran.del(dataDomain, k); + const dataPath = [...this.dataDbPath, ino.toString()]; + for await (const [k] of tran.iterator({ value: false }, dataPath)) { + await tran.del([...dataPath, k]); } - await this.iNodeDestroy(tran, ino); + await this.iNodeDestroy(ino, tran); } - @ready(new inodesErrors.ErrorINodesNotRunning()) - public async dirDestroy(tran: DBTransaction, ino: INodeIndex): Promise { - return this._dirDestroy(tran, ino); + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public async dirDestroy( + ino: INodeIndex, + tran?: DBTransaction, + ): Promise { + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.dirDestroy(ino, tran), + ); + } + return this._dirDestroy(ino, tran); } protected async _dirDestroy( - tran: DBTransaction, ino: INodeIndex, + tran: DBTransaction, ): Promise { - const dirDomain = [...this.dirsDomain, ino.toString()]; - const dirDb = await this.db.level(ino.toString(), this.dirsDb); - const parent = (await tran.get(dirDomain, '..'))!; + const dirPath = [...this.dirsDbPath, ino.toString()]; + const parent = (await tran.get([...dirPath, '..']))!; if (parent !== ino) { - await this._unlink(tran, parent); + await this._unlink(parent, tran); } else { await this.dirUnsetRoot(tran); } - for await (const k of dirDb.createKeyStream()) { - await tran.del(dirDomain, k); + for await (const [k] of tran.iterator({ values: false }, dirPath)) { + await tran.del([...dirPath, k]); } - await this.iNodeDestroy(tran, ino); + await this.iNodeDestroy(ino, tran); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async symlinkDestroy( - tran: DBTransaction, ino: INodeIndex, + tran?: DBTransaction, ): Promise { - return this._symlinkDestroy(tran, ino); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.symlinkDestroy(ino, tran), + ); + } + return this._symlinkDestroy(ino, tran); } protected async _symlinkDestroy( - tran: DBTransaction, ino: INodeIndex, + tran: DBTransaction, ): Promise { - await tran.del(this.linkDomain, inodesUtils.iNodeId(ino)); - await this.iNodeDestroy(tran, ino); + await tran.del([...this.linkDbPath, inodesUtils.iNodeId(ino)]); + await this.iNodeDestroy(ino, tran); } protected async iNodeDestroy( - tran: DBTransaction, ino: INodeIndex, + tran: DBTransaction, ): Promise { - const statDomain = [...this.statsDomain, ino.toString()]; + const statPath = [...this.statsDbPath, ino.toString()]; const keys = [ 'dev', 'ino', @@ -415,10 +595,10 @@ class INodeManager { 'birthtime', ]; for (const k of keys) { - await tran.del(statDomain, k); + await tran.del([...statPath, k]); } - await tran.del(this.iNodesDomain, inodesUtils.iNodeId(ino)); - await tran.del(this.gcDomain, inodesUtils.iNodeId(ino)); + await tran.del([...this.iNodesDbPath, inodesUtils.iNodeId(ino)]); + await tran.del([...this.gcDbPath, inodesUtils.iNodeId(ino)]); } /** @@ -426,115 +606,160 @@ class INodeManager { * Use this to test if an ino number exists * You can use the returned ino for subsequent operations */ - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async get( - tran: DBTransaction, - ino: number, + ino: INodeIndex, + tran?: DBTransaction, ): Promise { - const type = await tran.get( - this.iNodesDomain, + if (tran == null) { + return this.withTransactionF(ino, async (tran) => this.get(ino, tran)); + } + const type = await tran.get([ + ...this.iNodesDbPath, inodesUtils.iNodeId(ino as INodeIndex), - ); + ]); if (type == null) { return; } - const gc = await tran.get( - this.gcDomain, + const gc = await tran.get([ + ...this.gcDbPath, inodesUtils.iNodeId(ino as INodeIndex), - ); + ]); return { - ino: ino as INodeIndex, + ino, type, gc: gc !== undefined, }; } - @ready(new inodesErrors.ErrorINodesNotRunning()) - public async link(tran: DBTransaction, ino: INodeIndex): Promise { - const nlink = await this.statGetProp(tran, ino, 'nlink'); - await this.statSetProp(tran, ino, 'nlink', nlink + 1); + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public async *getAll(tran?: DBTransaction): AsyncGenerator { + if (tran == null) { + return yield* this.withTransactionG((tran) => this.getAll(tran)); + } + // Consistent iteration on iNodesDbPath and gcDbPath + const gcIterator = tran.iterator(undefined, this.gcDbPath); + try { + for await (const [inoData, typeData] of tran.iterator( + undefined, + this.iNodesDbPath, + )) { + const ino = inodesUtils.uniNodeId(inoData as INodeId); + const type = dbUtils.deserialize(typeData); + gcIterator.seek(inoData); + const gcData = (await gcIterator.next())?.[1]; + const gc = + gcData != null ? dbUtils.deserialize(gcData) : undefined; + yield { + ino, + type, + gc: gc !== undefined, + }; + } + } finally { + await gcIterator.end(); + } } - @ready(new inodesErrors.ErrorINodesNotRunning()) - public async unlink(tran: DBTransaction, ino: INodeIndex): Promise { - return this._unlink(tran, ino); + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public async link(ino: INodeIndex, tran?: DBTransaction): Promise { + if (tran == null) { + return this.withTransactionF(ino, async (tran) => this.link(ino, tran)); + } + const nlink = await this.statGetProp(ino, 'nlink', tran); + await this.statSetProp(ino, 'nlink', nlink + 1, tran); } - protected async _unlink(tran: DBTransaction, ino: INodeIndex): Promise { - const nlink = await this._statGetProp(tran, ino, 'nlink'); - await this._statSetProp(tran, ino, 'nlink', Math.max(nlink - 1, 0)); - await this.gc(tran, ino); + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public async unlink(ino: INodeIndex, tran?: DBTransaction): Promise { + if (tran == null) { + return this.withTransactionF(ino, async (tran) => this.unlink(ino, tran)); + } + return this._unlink(ino, tran); + } + + protected async _unlink(ino: INodeIndex, tran: DBTransaction): Promise { + const nlink = await this._statGetProp(ino, 'nlink', tran); + await this._statSetProp(ino, 'nlink', Math.max(nlink - 1, 0), tran); + await this.gc(ino, tran); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public ref(ino: INodeIndex) { const refCount = this.refs.get(ino) ?? 0; this.refs.set(ino, refCount + 1); } - @ready(new inodesErrors.ErrorINodesNotRunning()) - public async unref(tran: DBTransaction, ino: INodeIndex) { + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public async unref(ino: INodeIndex, tran?: DBTransaction) { + if (tran == null) { + return this.withTransactionF(ino, async (tran) => this.unref(ino, tran)); + } const refCount = this.refs.get(ino); if (refCount == null) { return; } this.refs.set(ino, Math.max(refCount - 1, 0)); - await this.gc(tran, ino); + await this.gc(ino, tran); } - protected async gc(tran: DBTransaction, ino: INodeIndex): Promise { + protected async gc(ino: INodeIndex, tran: DBTransaction): Promise { const refs = this.refs.get(ino) ?? 0; - const nlink = await this._statGetProp(tran, ino, 'nlink'); - const type = (await tran.get( - this.iNodesDomain, + const nlink = await this._statGetProp(ino, 'nlink', tran); + const type = (await tran.get([ + ...this.iNodesDbPath, inodesUtils.iNodeId(ino), - ))!; + ]))!; // The root directory will never be deleted if (nlink === 0 || (nlink === 1 && type === 'Directory')) { if (refs === 0) { // Delete the on-disk and in-memory state switch (type) { case 'File': - await this._fileDestroy(tran, ino); + await this._fileDestroy(ino, tran); break; case 'Directory': - await this._dirDestroy(tran, ino); + await this._dirDestroy(ino, tran); break; case 'Symlink': - await this._symlinkDestroy(tran, ino); + await this._symlinkDestroy(ino, tran); break; } tran.queueSuccess(() => { this.refs.delete(ino); - this.locks.delete(ino); this.inoDeallocate(ino); }); } else { // Schedule for deletion // when scheduled for deletion // it is not allowed for mutation of the directory to occur - await tran.put(this.gcDomain, inodesUtils.iNodeId(ino), null); + await tran.put([...this.gcDbPath, inodesUtils.iNodeId(ino)], null); } } } - @ready(new inodesErrors.ErrorINodesNotRunning()) - public async statGet(tran: DBTransaction, ino: INodeIndex): Promise { - const statDomain = [...this.statsDomain, ino.toString()]; + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) + public async statGet(ino: INodeIndex, tran?: DBTransaction): Promise { + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.statGet(ino, tran), + ); + } + const statPath = [...this.statsDbPath, ino.toString()]; const props: Array = await Promise.all([ - tran.get(statDomain, 'dev'), - tran.get(statDomain, 'mode'), - tran.get(statDomain, 'nlink'), - tran.get(statDomain, 'uid'), - tran.get(statDomain, 'gid'), - tran.get(statDomain, 'rdev'), - tran.get(statDomain, 'size'), - tran.get(statDomain, 'blksize'), - tran.get(statDomain, 'blocks'), - tran.get(statDomain, 'atime'), - tran.get(statDomain, 'mtime'), - tran.get(statDomain, 'ctime'), - tran.get(statDomain, 'birthtime'), + tran.get([...statPath, 'dev']), + tran.get([...statPath, 'mode']), + tran.get([...statPath, 'nlink']), + tran.get([...statPath, 'uid']), + tran.get([...statPath, 'gid']), + tran.get([...statPath, 'rdev']), + tran.get([...statPath, 'size']), + tran.get([...statPath, 'blksize']), + tran.get([...statPath, 'blocks']), + tran.get([...statPath, 'atime']), + tran.get([...statPath, 'mtime']), + tran.get([...statPath, 'ctime']), + tran.get([...statPath, 'birthtime']), ]); const [ dev, @@ -569,21 +794,26 @@ class INodeManager { }); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async statGetProp( - tran: DBTransaction, ino: INodeIndex, key: Key, + tran?: DBTransaction, ): Promise { - return this._statGetProp(tran, ino, key); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.statGetProp(ino, key, tran), + ); + } + return this._statGetProp(ino, key, tran); } protected async _statGetProp( - tran: DBTransaction, ino: INodeIndex, key: Key, + tran: DBTransaction, ): Promise { - const statDomain = [...this.statsDomain, ino.toString()]; + const statPath = [...this.statsDbPath, ino.toString()]; let value; switch (key) { case 'dev': @@ -596,35 +826,40 @@ class INodeManager { case 'size': case 'blksize': case 'blocks': - value = (await tran.get(statDomain, key))!; + value = (await tran.get([...statPath, key]))!; break; case 'atime': case 'mtime': case 'ctime': case 'birthtime': - value = new Date((await tran.get(statDomain, key))!); + value = new Date((await tran.get([...statPath, key]))!); break; } return value; } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async statSetProp( - tran: DBTransaction, ino: INodeIndex, key: Key, value: StatProps[Key], + tran?: DBTransaction, ): Promise { - return this._statSetProp(tran, ino, key, value); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.statSetProp(ino, key, value, tran), + ); + } + return this._statSetProp(ino, key, value, tran); } protected async _statSetProp( - tran: DBTransaction, ino: INodeIndex, key: Key, value: StatProps[Key], + tran: DBTransaction, ): Promise { - const statDomain = [...this.statsDomain, ino.toString()]; + const statPath = [...this.statsDbPath, ino.toString()]; switch (key) { case 'dev': case 'ino': @@ -636,152 +871,190 @@ class INodeManager { case 'size': case 'blksize': case 'blocks': - await tran.put(statDomain, key, value); + await tran.put([...statPath, key], value); break; case 'atime': case 'mtime': case 'ctime': case 'birthtime': - await tran.put(statDomain, key, (value as Date).getTime()); + await tran.put([...statPath, key], (value as Date).getTime()); break; } } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async statUnsetProp( - tran: DBTransaction, ino: INodeIndex, key: Key, + tran?: DBTransaction, ): Promise { - const statDomain = [...this.statsDomain, ino.toString()]; - await tran.del(statDomain, key); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.statUnsetProp(ino, key, tran), + ); + } + const statPath = [...this.statsDbPath, ino.toString()]; + await tran.del([...statPath, key]); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async dirGetRoot( - tran: DBTransaction, + tran?: DBTransaction, ): Promise { - return tran.get(this.mgrDomain, 'root'); + if (tran == null) { + return this.withTransactionF(async (tran) => this.dirGetRoot(tran)); + } + return tran.get([...this.mgrDbPath, 'root']); } protected async dirSetRoot( - tran: DBTransaction, ino: INodeIndex, + tran: DBTransaction, ): Promise { - await tran.put(this.mgrDomain, 'root', ino); + await tran.put([...this.mgrDbPath, 'root'], ino); } protected async dirUnsetRoot(tran: DBTransaction): Promise { - await tran.del(this.mgrDomain, 'root'); + await tran.del([...this.mgrDbPath, 'root']); } - /** - * Iterators are not part of our snapshot yet - */ - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async *dirGet( - tran: DBTransaction, ino: INodeIndex, + tran?: DBTransaction, ): AsyncGenerator<[string, INodeIndex]> { - const dirDb = await this.db.level(ino.toString(), this.dirsDb); - for await (const o of dirDb.createReadStream()) { - const name = (o as any).key.toString('utf-8') as string; - const value = await this.db.deserializeDecrypt( - (o as any).value, - false, + if (tran == null) { + return yield* this.withTransactionG(ino, (tran) => + this.dirGet(ino, tran), ); + } + for await (const [k, v] of tran.iterator(undefined, [ + ...this.dirsDbPath, + ino.toString(), + ])) { + const name = k.toString('utf-8'); + const value = dbUtils.deserialize(v); yield [name, value]; } } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async dirGetEntry( - tran: DBTransaction, ino: INodeIndex, name: string, + tran?: DBTransaction, ): Promise { - const dirDomain = [...this.dirsDomain, ino.toString()]; - return tran.get(dirDomain, name); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.dirGetEntry(ino, name, tran), + ); + } + const dirPath = [...this.dirsDbPath, ino.toString()]; + return tran.get([...dirPath, name]); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async dirSetEntry( - tran: DBTransaction, ino: INodeIndex, name: string, value: INodeIndex, + tran?: DBTransaction, ): Promise { - const dirDomain = [...this.dirsDomain, ino.toString()]; - if ((await this.get(tran, value)) == null) { - throw new inodesErrors.ErrorINodesIndexMissing(); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.dirSetEntry(ino, name, value, tran), + ); } - const existingValue = await tran.get(dirDomain, name); + const dirPath = [...this.dirsDbPath, ino.toString()]; + if ((await this.get(value, tran)) == null) { + throw new inodesErrors.ErrorINodesIndexMissing( + `Cannot set directory entry ${name} to missing INode ${ino}`, + ); + } + const existingValue = await tran.get([...dirPath, name]); if (existingValue === value) { return; } const now = new Date(); - await tran.put(dirDomain, name, value); - await this.statSetProp(tran, ino, 'mtime', now); - await this.statSetProp(tran, ino, 'ctime', now); - await this.link(tran, value); + await tran.put([...dirPath, name], value); + await this.statSetProp(ino, 'mtime', now, tran); + await this.statSetProp(ino, 'ctime', now, tran); + await this.link(value, tran); if (existingValue != null) { - await this.unlink(tran, existingValue); + await this.unlink(existingValue, tran); } } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async dirUnsetEntry( - tran: DBTransaction, ino: INodeIndex, name: string, + tran?: DBTransaction, ): Promise { - const dirDomain = [...this.dirsDomain, ino.toString()]; + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.dirUnsetEntry(ino, name, tran), + ); + } + const dirPath = [...this.dirsDbPath, ino.toString()]; const now = new Date(); - const existingValue = await tran.get(dirDomain, name); + const existingValue = await tran.get([...dirPath, name]); if (existingValue == null) { return; } - await tran.del(dirDomain, name); - await this.statSetProp(tran, ino, 'mtime', now); - await this.statSetProp(tran, ino, 'ctime', now); - await this.unlink(tran, existingValue); + await tran.del([...dirPath, name]); + await this.statSetProp(ino, 'mtime', now, tran); + await this.statSetProp(ino, 'ctime', now, tran); + await this.unlink(existingValue, tran); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async dirResetEntry( - tran: DBTransaction, ino: INodeIndex, nameOld: string, nameNew: string, + tran?: DBTransaction, ): Promise { - const dirDomain = [...this.dirsDomain, ino.toString()]; - const inoOld = await tran.get(dirDomain, nameOld); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.dirResetEntry(ino, nameOld, nameNew, tran), + ); + } + const dirPath = [...this.dirsDbPath, ino.toString()]; + const inoOld = await tran.get([...dirPath, nameOld]); if (inoOld == null) { - throw new inodesErrors.ErrorINodesInvalidName(); + throw new inodesErrors.ErrorINodesInvalidName( + `Cannot set missing directory entry ${nameOld} to ${nameNew}`, + ); } const now = new Date(); - await this.statSetProp(tran, ino, 'ctime', now); - await this.statSetProp(tran, ino, 'mtime', now); - await this.statSetProp(tran, inoOld, 'ctime', now); - const inoReplace = await this.dirGetEntry(tran, ino, nameNew); + await this.statSetProp(ino, 'ctime', now, tran); + await this.statSetProp(ino, 'mtime', now, tran); + await this.statSetProp(inoOld, 'ctime', now, tran); + const inoReplace = await this.dirGetEntry(ino, nameNew, tran); if (inoReplace) { - await this.statSetProp(tran, inoReplace, 'ctime', now); + await this.statSetProp(inoReplace, 'ctime', now, tran); } // The order must be set then unset // it cannot work if unset then set, the old inode may get garbage collected - await this.dirSetEntry(tran, ino, nameNew, inoOld); - await this.dirUnsetEntry(tran, ino, nameOld); + await this.dirSetEntry(ino, nameNew, inoOld, tran); + await this.dirUnsetEntry(ino, nameOld, tran); } - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async symlinkGetLink( - tran: DBTransaction, ino: INodeIndex, + tran?: DBTransaction, ): Promise { - const link = await tran.get( - this.linkDomain, + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.symlinkGetLink(ino, tran), + ); + } + const link = await tran.get([ + ...this.linkDbPath, inodesUtils.iNodeId(ino), - ); + ]); return link!; } @@ -789,34 +1062,37 @@ class INodeManager { * Modified and Change Time are both updated here as this is * exposed to the EFS functions to be used */ - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async fileClearData( - tran: DBTransaction, ino: INodeIndex, + tran?: DBTransaction, ): Promise { - // To set the data we must first clear all existing data, which is - // how VFS handles it - const dataDb = await this.db.level(ino.toString(), this.dataDb); - const dataDomain = [...this.dataDomain, ino.toString()]; - for await (const key of dataDb.createKeyStream()) { - await tran.del(dataDomain, key); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.fileClearData(ino, tran), + ); } + const dataPath = [...this.dataDbPath, ino.toString()]; + await tran.clear(dataPath); } /** - * Iterators are not part of our snapshot yet * Access time not updated here, handled at higher level as this is only * accessed by fds and and other INodeMgr functions */ - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async *fileGetBlocks( - tran: DBTransaction, ino: INodeIndex, blockSize: number, - startIdx = 0, + startIdx: number = 0, endIdx?: number, + tran?: DBTransaction, ): AsyncGenerator { - const dataDb = await this.db.level(ino.toString(), this.dataDb); + if (tran == null) { + return yield* this.withTransactionG(ino, (tran) => + this.fileGetBlocks(ino, blockSize, startIdx, endIdx, tran), + ); + } const options = endIdx ? { gte: inodesUtils.bufferId(startIdx), @@ -824,40 +1100,43 @@ class INodeManager { } : { gte: inodesUtils.bufferId(startIdx) }; let blockCount = startIdx; - for await (const data of dataDb.createReadStream(options)) { + for await (const [k, v] of tran.iterator(options, [ + ...this.dataDbPath, + ino.toString(), + ])) { // This is to account for the case where a some blocks are missing in a database // i.e. blocks 0 -> 3 have data and a write operation was performed on blocks 7 -> 8 - while (inodesUtils.unbufferId((data as any).key) !== blockCount) { + while (inodesUtils.unbufferId(k as BufferId) !== blockCount) { yield Buffer.alloc(blockSize); + blockCount++; } - const plainTextData = await this.db.deserializeDecrypt( - (data as any).value as Buffer, - true, - ); - yield plainTextData; + yield v; blockCount++; } } /** - * Iterators are not part of our snapshot yet * Access time not updated here, handled at higher level as this is only * accessed by fds and and other INodeMgr functions */ - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async fileGetLastBlock( - tran: DBTransaction, ino: INodeIndex, + tran?: DBTransaction, ): Promise<[number, Buffer]> { - const dataDb = await this.db.level(ino.toString(), this.dataDb); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.fileGetLastBlock(ino, tran), + ); + } const options = { limit: 1, reverse: true }; let key, value; - for await (const data of dataDb.createReadStream(options)) { - key = inodesUtils.unbufferId((data as any).key); - value = await this.db.deserializeDecrypt( - (data as any).value as Buffer, - true, - ); + for await (const [k, v] of tran.iterator(options, [ + ...this.dataDbPath, + ino.toString(), + ])) { + key = inodesUtils.unbufferId(k as BufferId); + value = v; } if (value == null || key == null) { return [0, Buffer.alloc(0)]; @@ -870,13 +1149,13 @@ class INodeManager { * accessed by fds and and other INodeMgr functions */ protected async fileGetBlock( - tran: DBTransaction, ino: INodeIndex, idx: number, + tran: DBTransaction, ): Promise { - const dataDomain = [...this.dataDomain, ino.toString()]; + const dataPath = [...this.dataDbPath, ino.toString()]; const key = inodesUtils.bufferId(idx); - const buffer = await tran.get(dataDomain, key, true); + const buffer = await tran.get([...dataPath, key], true); if (!buffer) { return undefined; } @@ -887,18 +1166,23 @@ class INodeManager { * Modified and Change time not updated here, handled at higher level as this * is only accessed by fds and and other INodeMgr functions */ - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async fileSetBlocks( - tran: DBTransaction, ino: INodeIndex, data: Buffer, blockSize: number, - startIdx = 0, + startIdx: number = 0, + tran?: DBTransaction, ): Promise { + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.fileSetBlocks(ino, data, blockSize, startIdx, tran), + ); + } const bufferSegments = utils.segmentBuffer(blockSize, data); let blockIdx = startIdx; for (const dataSegment of bufferSegments) { - await this.fileWriteBlock(tran, ino, dataSegment, blockIdx); + await this.fileWriteBlock(ino, dataSegment, blockIdx, 0, tran); blockIdx++; } } @@ -907,20 +1191,27 @@ class INodeManager { * Modified and Change time not updated here, handled at higher level as this * is only accessed by fds and other INodeMgr functions */ - @ready(new inodesErrors.ErrorINodesNotRunning()) + @ready(new inodesErrors.ErrorINodeManagerNotRunning()) public async fileWriteBlock( - tran: DBTransaction, ino: INodeIndex, data: Buffer, idx: number, - offset = 0, + offset: number = 0, + tran?: DBTransaction, ): Promise { - const dataDomain = [...this.dataDomain, ino.toString()]; - let block = await this.fileGetBlock(tran, ino, idx); + if (tran == null) { + return this.withTransactionF(ino, async (tran) => + this.fileWriteBlock(ino, data, idx, offset, tran), + ); + } + const dataPath = [...this.dataDbPath, ino.toString()]; + let block = await this.fileGetBlock(ino, idx, tran); const key = inodesUtils.bufferId(idx); let bytesWritten; if (!block) { - await tran.put(dataDomain, key, data, true); + const newBlock = Buffer.alloc(offset + data.length); + data.copy(newBlock, offset); + await tran.put([...dataPath, key], newBlock, true); bytesWritten = data.length; } else { if (offset >= block.length) { @@ -928,7 +1219,7 @@ class INodeManager { const newBlock = Buffer.alloc(offset + data.length); block.copy(newBlock); bytesWritten = data.copy(newBlock, offset); - await tran.put(dataDomain, key, newBlock, true); + await tran.put([...dataPath, key], newBlock, true); } else { // In this case we are overwriting if (offset + data.length > block.length) { @@ -938,7 +1229,7 @@ class INodeManager { ]); } bytesWritten = data.copy(block, offset); - await tran.put(dataDomain, key, block, true); + await tran.put([...dataPath, key], block, true); } } return bytesWritten; diff --git a/src/inodes/errors.ts b/src/inodes/errors.ts index 86337954..0bf7a94d 100644 --- a/src/inodes/errors.ts +++ b/src/inodes/errors.ts @@ -1,26 +1,43 @@ -import { CustomError } from 'ts-custom-error'; +import { AbstractError } from '@matrixai/errors'; -class ErrorINodes extends CustomError {} +class ErrorINodes extends AbstractError { + static description = 'INodes error'; +} -class ErrorINodesRunning extends ErrorINodes {} +class ErrorINodeManagerRunning extends ErrorINodes { + static description = 'INodeManager is running'; +} -class ErrorINodesNotRunning extends ErrorINodes {} +class ErrorINodeManagerNotRunning extends ErrorINodes { + static description = 'INodeManager is not running'; +} -class ErrorINodesDestroyed extends ErrorINodes {} +class ErrorINodeManagerDestroyed extends ErrorINodes { + static description = 'INodeManager is destroyed'; +} -class ErrorINodesDuplicateRoot extends ErrorINodes {} +class ErrorINodesDuplicateRoot extends ErrorINodes { + static description = 'Only a single root INode is allowed'; +} -class ErrorINodesIndexMissing extends ErrorINodes {} +class ErrorINodesIndexMissing extends ErrorINodes { + static description = 'INode cannot be found'; +} -class ErrorINodesParentMissing extends ErrorINodes {} +class ErrorINodesParentMissing extends ErrorINodes { + static description = 'Parent INode cannot be found during directory creation'; +} -class ErrorINodesInvalidName extends ErrorINodes {} +class ErrorINodesInvalidName extends ErrorINodes { + static description = + 'Old entry cannot be found during directory entry renaming'; +} export { ErrorINodes, - ErrorINodesRunning, - ErrorINodesNotRunning, - ErrorINodesDestroyed, + ErrorINodeManagerRunning, + ErrorINodeManagerNotRunning, + ErrorINodeManagerDestroyed, ErrorINodesDuplicateRoot, ErrorINodesIndexMissing, ErrorINodesParentMissing, diff --git a/src/inodes/utils.ts b/src/inodes/utils.ts index 4a5d2dc8..872ff356 100644 --- a/src/inodes/utils.ts +++ b/src/inodes/utils.ts @@ -1,5 +1,4 @@ import type { INodeIndex, INodeId, BufferIndex, BufferId } from './types'; - import lexi from 'lexicographic-integer'; function iNodeId(index: INodeIndex): INodeId { diff --git a/src/types.ts b/src/types.ts index 251db6a4..8dbca72c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,11 @@ type POJO = { [key: string]: any }; */ type Opaque = T & { __TYPE__: K }; +/** + * Non-empty array + */ +type NonEmptyArray = [T, ...T[]]; + /** * Any type that can be turned into a string */ @@ -21,6 +26,15 @@ interface ToString { toString(): string; } +/** + * Wrap a type to be reference counted + * Useful for when we need to garbage collect data + */ +type Ref = { + count: number; + value: T; +}; + /** * Generic callback */ @@ -78,7 +92,9 @@ type EFSWorkerManagerInterface = WorkerManagerInterface; export type { POJO, Opaque, + NonEmptyArray, ToString, + Ref, Callback, FunctionProperties, NonFunctionProperties, diff --git a/src/utils.ts b/src/utils.ts index 9d7b831f..0424322e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,5 @@ import type { Callback } from './types'; -import type { Stat } from '.'; - +import type Stat from './Stat'; import pathNode from 'path'; import { md, random, pkcs5, cipher, util as forgeUtil } from 'node-forge'; import callbackify from 'util-callbackify'; @@ -31,7 +30,16 @@ function fromArrayBuffer( } async function getRandomBytes(size: number): Promise { - return Buffer.from(await random.getBytes(size), 'binary'); + const p = new Promise((resolve, reject) => { + random.getBytes(size, (e, bytes) => { + if (e != null) { + reject(e); + } else { + resolve(bytes); + } + }); + }); + return Buffer.from(await p, 'binary'); } function getRandomBytesSync(size: number): Buffer { diff --git a/tests/EncryptedFS.concurrent.test.ts b/tests/EncryptedFS.concurrent.test.ts index c4fb308a..7d8a1576 100644 --- a/tests/EncryptedFS.concurrent.test.ts +++ b/tests/EncryptedFS.concurrent.test.ts @@ -1,934 +1,4048 @@ import type { FdIndex } from '@/fd/types'; -import type { WriteStream } from '@/streams'; -import path from 'path'; +import type { INodeData } from '@/inodes/types'; import fs from 'fs'; -import pathNode from 'path'; import os from 'os'; +import path from 'path'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { code as errno } from 'errno'; +import { DB } from '@matrixai/db'; +import EncryptedFS from '@/EncryptedFS'; +import { ErrorEncryptedFSError } from '@/errors'; import * as utils from '@/utils'; -import { EncryptedFS, constants } from '@'; -import { expectError, sleep } from './utils'; +import * as constants from '@/constants'; +import INodeManager from '@/inodes/INodeManager'; +import { promise } from '@/utils'; +import { expectReason, sleep } from './utils'; -describe('EncryptedFS Concurrency', () => { - const logger = new Logger('EncryptedFS Directories', LogLevel.WARN, [ +describe(`${EncryptedFS.name} Concurrency`, () => { + const logger = new Logger(`${EncryptedFS.name} Concurrency`, LogLevel.WARN, [ new StreamHandler(), ]); - let dataDir: string; - let dbPath: string; const dbKey: Buffer = utils.generateKeySync(256); + let dataDir: string; + let db: DB; + let iNodeMgr: INodeManager; let efs: EncryptedFS; - const flags = constants; beforeEach(async () => { dataDir = await fs.promises.mkdtemp( - pathNode.join(os.tmpdir(), 'encryptedfs-test-'), + path.join(os.tmpdir(), 'encryptedfs-test-'), ); - dbPath = `${dataDir}/db`; + db = await DB.createDB({ + dbPath: dataDir, + crypto: { + key: dbKey!, + ops: { + encrypt: utils.encrypt, + decrypt: utils.decrypt, + }, + }, + logger: logger.getChild(DB.name), + }); + iNodeMgr = await INodeManager.createINodeManager({ + db, + logger: logger.getChild(INodeManager.name), + }); efs = await EncryptedFS.createEncryptedFS({ - dbKey, - dbPath, - umask: 0o022, + db, + iNodeMgr, logger, }); }); afterEach(async () => { + await efs.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, }); }); - test('Renaming a directory at the same time with two different calls', async () => { - await efs.mkdir('test'); - try { + describe('concurrent inode creation', () => { + test('EncryptedFS.open', async () => { + // Only one call wins the race to create the file await Promise.all([ - efs.rename('test', 'one'), - efs.rename('test', 'two'), - efs.rename('test', 'three'), - efs.rename('test', 'four'), - efs.rename('test', 'five'), - efs.rename('test', 'six'), + efs.open('test', constants.O_RDWR | constants.O_CREAT), + efs.open('test', constants.O_RDWR | constants.O_CREAT), + efs.open('test', constants.O_RDWR | constants.O_CREAT), + efs.open('test', constants.O_RDWR | constants.O_CREAT), ]); - } catch (err) { - // Do nothing - } - - // Right now only the first rename works. the rest fail. this is expected. - expect(await efs.readdir('.')).toContain('one'); - }); - test('Reading a directory while adding/removing entries in the directory', async () => { - await efs.mkdir('dir'); - const file1 = path.join('dir', 'file1'); - - const results1 = await Promise.all([ - efs.writeFile(file1, 'test1'), - efs.readdir('dir'), - ]); - // Readdir seems to return the directory before the changes happen. - expect(results1[1]).not.toContain('file1'); - expect(await efs.readdir('dir')).toContain('file1'); - - const results2 = await Promise.all([efs.unlink(file1), efs.readdir('dir')]); - // Readdir seems to return the directory before the changes happen. - expect(results2[1]).toContain('file1'); - expect(await efs.readdir('dir')).not.toContain('file1'); - }); - test('Reading a directory while removing the directory', async () => { - await efs.mkdir('dir'); - - const results1 = await Promise.all([efs.readdir('dir'), efs.rmdir('dir')]); - // Readdir seems to return the directory before the changes happen. - expect(results1[0]).toEqual([]); - await expectError(efs.readdir('dir'), errno.ENOENT); - - // If after rmdir still completes after readdir - await efs.mkdir('dir'); - const results2 = await Promise.all([efs.rmdir('dir'), efs.readdir('dir')]); - expect(results2[1]).toEqual([]); - }); - test('Reading a directory while renaming entries', async () => { - await efs.mkdir('dir'); - await efs.writeFile(path.join('dir', 'file1')); - - const results1 = await Promise.all([ - efs.readdir('dir'), - efs.rename(path.join('dir', 'file1'), path.join('dir', 'file2')), - ]); - // Readdir seems to return the directory before the changes happen. - expect(results1[0]).toContain('file1'); - expect(await efs.readdir('dir')).toContain('file2'); - - const results2 = await Promise.all([ - efs.rename(path.join('dir', 'file2'), path.join('dir', 'file1')), - efs.readdir('dir'), - ]); - // Readdir seems to return the directory before the changes happen. - expect(results2[1]).toContain('file2'); - expect(await efs.readdir('dir')).toContain('file1'); - }); - describe('concurrent file writes', () => { - test('10 short writes with efs.writeFile.', async () => { - const contents = [ - 'one', - 'two', - 'three', - 'four', - 'five', - 'six', - 'seven', - 'eight', - 'nine', - 'ten', - ]; - // Here we want to write to a file at the same time and sus out the behaviour. - const promises: Array = []; - for (const content of contents) { - promises.push(efs.writeFile('test', content)); + expect(await efs.readFile('test', { encoding: 'utf-8' })).toBe(''); + const inodeDatas: Array = []; + for await (const inodeData of iNodeMgr.getAll()) { + inodeDatas.push(inodeData); } - await Promise.all(promises); + expect(inodeDatas).toStrictEqual([ + { ino: 1, type: 'Directory', gc: false }, + { ino: 2, type: 'File', gc: false }, + ]); }); - test('10 long writes with efs.writeFile.', async () => { - const blockSize = 4096; - const blocks = 100; - const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']; - let divisor = 0; - const contents = letters.map((letter) => { - divisor++; - return letter.repeat((blockSize * blocks) / divisor); - }); - const promises: Array = []; - for (const content of contents) { - promises.push(efs.writeFile('test', content, {})); + test('EncryptedFS.mknod and EncryptedFS.mknod', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.mknod(path1, constants.S_IFREG, 0, 0); + })(), + (async () => { + return await efs.mknod(path1, constants.S_IFREG, 0, 0); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.mknod(path1, constants.S_IFREG, 0, 0); + })(), + (async () => { + return await efs.mknod(path1, constants.S_IFREG, 0, 0); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); } - await Promise.all(promises); }); - test('10 short writes with efs.write.', async () => { - const contents = [ - 'one', - 'two', - 'three', - 'four', - 'five', - 'six', - 'seven', - 'eight', - 'nine', - 'ten', - ]; - // Here we want to write to a file at the same time and sus out the behaviour. - const fds: Array = []; - for (let i = 0; i < 10; i++) { - fds.push(await efs.open('test', flags.O_RDWR | flags.O_CREAT)); + test('EncryptedFS.mkdir and EncryptedFS.mknod', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.mkdir(path1); + })(), + (async () => { + return await efs.mknod(path1, constants.S_IFREG, 0, 0); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); } - const promises: Array = []; - for (let i = 0; i < 10; i++) { - promises.push(efs.write(fds[i], contents[i])); + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.mkdir(path1); + })(), + (async () => { + return await efs.mknod(path1, constants.S_IFREG, 0, 0); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); } - await Promise.all(promises); }); - test('10 long writes with efs.write.', async () => { - const blockSize = 4096; - const blocks = 100; - const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']; - let divisor = 0; - const contents = letters.map((letter) => { - divisor++; - return letter.repeat((blockSize * blocks) / divisor); - }); - let fds: Array = []; - for (let i = 0; i < 10; i++) { - fds.push(await efs.open('test', flags.O_RDWR | flags.O_CREAT)); + test('EncryptedFS.open and EncryptedFS.mkdir', async () => { + const path1 = path.join('dir', 'dir1'); + await efs.mkdir('dir'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.mkdir(path1); + })(), + (async () => { + const fd = await efs.open(path1, 'wx'); + await efs.close(fd); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); } - let promises: Array = []; - for (let i = 0; i < 10; i++) { + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.mkdir(path1); + })(), + (async () => { + const fd = await efs.open(path1, 'wx'); + await efs.close(fd); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + }); + // Test inode creation as well + }); + describe('concurrent file writes', () => { + test('EncryptedFS.write on multiple file descriptors', async () => { + // Concurrent writes of the same length results in "last write wins" + let fds: Array = [ + await efs.open('test', constants.O_RDWR | constants.O_CREAT), + await efs.open('test', constants.O_RDWR | constants.O_CREAT), + ]; + let contents = ['one', 'two']; + let promises: Array>; + promises = []; + for (let i = 0; i < 2; i++) { promises.push(efs.write(fds[i], contents[i])); } await Promise.all(promises); - const fileContent = (await efs.readFile('test')).toString(); - - for (const letter of letters) { - expect(fileContent).toContain(letter); - } - - // Now reverse order. - await efs.unlink('test'); + expect(['one', 'two']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); for (const fd of fds) { await efs.close(fd); } - fds = []; - for (let i = 9; i >= 0; i--) { - fds.push(await efs.open('test', flags.O_RDWR | flags.O_CREAT)); - } + // Concurrent writes of different length results in "last write wins" or a merge + fds = [ + await efs.open('test', constants.O_RDWR | constants.O_CREAT), + await efs.open('test', constants.O_RDWR | constants.O_CREAT), + ]; + contents = ['one1', 'two']; promises = []; - for (let i = 9; i >= 0; i--) { + for (let i = 0; i < 2; i++) { promises.push(efs.write(fds[i], contents[i])); } + expect(['one1', 'two', 'two1']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + for (const fd of fds) { + await efs.close(fd); + } + }); + test('EncryptedFS.write on the same file descriptor', async () => { + await efs.writeFile('test', ''); + const fd = await efs.open('test', 'w'); + await Promise.all([ + efs.write(fd, Buffer.from('aaa')), + efs.write(fd, Buffer.from('bbb')), + ]); + expect(['aaabbb', 'bbbaaa']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + await efs.close(fd); + }); + test('EncryptedFS.writeFile', async () => { + let promises: Array>; + // Concurrent writes of the same length results in "last write wins" + promises = []; + for (const data of ['one', 'two']) { + promises.push(efs.writeFile('test', data)); + } await Promise.all(promises); - const fileContent2 = (await efs.readFile('test')).toString(); - - expect(fileContent2).toContain('A'); + expect(['one', 'two']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + // Concurrent writes of different length results in "last write wins" or a merge + for (let i = 0; i < 10; i++) { + promises = []; + for (const data of ['one1', 'two']) { + promises.push(efs.writeFile('test', data)); + } + await Promise.all(promises); + expect(['one1', 'two', 'two1']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + } + // Explicit last write wins + promises = [ + (async () => { + // One is written last + await sleep(0); + return efs.writeFile('test', 'one'); + })(), + efs.writeFile('test', 'two'), + ]; + await Promise.all(promises); + expect(['one']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + promises = [ + efs.writeFile('test', 'one'), + (async () => { + // Two1 is written last + await sleep(0); + return efs.writeFile('test', 'two1'); + })(), + ]; + await Promise.all(promises); + expect(['two1']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + const inodeDatas: Array = []; + for await (const inodeData of iNodeMgr.getAll()) { + inodeDatas.push(inodeData); + } + expect(inodeDatas).toStrictEqual([ + { ino: 1, type: 'Directory', gc: false }, + { ino: 2, type: 'File', gc: false }, + ]); }); - }); - describe('Allocating/truncating a file while writing (stream or fd)', () => { - test('Allocating while writing to fd', async () => { - const fd = await efs.open('file', flags.O_WRONLY | flags.O_CREAT); + test('EncryptedFS.appendFile', async () => { + await efs.writeFile('test', 'original'); + // Concurrent appends results in mutually exclusive writes + const promises = [ + efs.appendFile('test', 'one'), + efs.appendFile('test', 'two'), + ]; + await Promise.all(promises); + // Either order of appending is acceptable + expect(['originalonetwo', 'originaltwoone']).toContainEqual( + await efs.readFile('test', { encoding: 'utf-8' }), + ); + }); + test('EncryptedFS.fallocate and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(4); + } - const content = 'A'.repeat(4096 * 2); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(4); + } - await Promise.all([ - efs.write(fd, Buffer.from(content)), - efs.fallocate(fd, 0, 4096 * 3), + // WriteFile with FdIndex + // cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + return await efs.writeFile(fd, 'test'); + })(), ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(4); + } - // Both operations complete, order makes no diference. - const fileContents = await efs.readFile('file'); - expect(fileContents.length).toBeGreaterThan(4096 * 2); - expect(fileContents.toString()).toContain('A'); - expect(fileContents).toContain(0x00); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + return await efs.writeFile(fd, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(4); + } }); - test('Truncating while writing to fd', async () => { - const fd1 = await efs.open('file', flags.O_WRONLY | flags.O_CREAT); + test('EncryptedFS.fallocate and EncryptedFS.write', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + return await efs.write(fd, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 4 }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(4); + } - const content = 'A'.repeat(4096 * 2); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + return await efs.write(fd, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 4 }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(4); + } + }); + test('EncryptedFS.fallocate and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(i.toString()); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(10); + } - await Promise.all([ - efs.write(fd1, Buffer.from(content)), - efs.ftruncate(fd1, 4096), + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(i.toString()); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(10); + } - // Both operations complete, order makes no difference. Truncate doesn't do anything? - const fileContents1 = await efs.readFile('file'); - expect(fileContents1.length).toBe(4096 * 2); - expect(fileContents1.toString()).toContain('A'); - expect(fileContents1).not.toContain(0x00); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + // With a slow streaming write, + // fallocate should happen in the middle of the stream + results = await Promise.allSettled([ + (async () => { + await sleep(50); + return await efs.fallocate(fd, 0, 100); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(i.toString()); + await sleep(10); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length === 100) { + // Fallocate happened after write + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(100); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(100); + expect(contents.length).toEqual(10); + } + }); + test('EncryptedFS.truncate and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.truncate(fd, 27); + })(), + (async () => { + return await efs.writeFile( + path1, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length < 30) { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } else { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } - await efs.unlink('file'); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.truncate(fd, 27); + })(), + (async () => { + return await efs.writeFile( + path1, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length < 30) { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } else { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } - const fd2 = await efs.open('file', flags.O_WRONLY | flags.O_CREAT); + // WriteFile with FdIndex + // cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + return await efs.truncate(fd, 27); + })(), + (async () => { + return await efs.writeFile( + fd, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length < 30) { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } else { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } - await Promise.all([ - efs.ftruncate(fd2, 4096), - efs.write(fd2, Buffer.from(content)), + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.truncate(fd, 27); + })(), + (async () => { + return await efs.writeFile( + fd, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length < 30) { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } else { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } + }); + test('EncryptedFS.truncate and EncryptedFS.write', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.truncate(fd, 27); + })(), + (async () => { + return await efs.write( + fd, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 44 }, ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length > 30) { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } - // Both operations complete, order makes no difference. Truncate doesn't do anything? - const fileContents2 = await efs.readFile('file'); - expect(fileContents2.length).toBe(4096 * 2); - expect(fileContents2.toString()).toContain('A'); - expect(fileContents2).not.toContain(0x00); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.truncate(fd, 27); + })(), + (async () => { + return await efs.write( + fd, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 44 }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length > 30) { + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } else { + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } }); - test('Allocating while writing to stream', async () => { - await efs.writeFile('file', ''); - const writeStream = await efs.createWriteStream('file'); - const content = 'A'.repeat(4096); - const fd = await efs.open('file', 'w'); + test('EncryptedFS.truncate and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + const phrase = 'The quick brown fox jumped over the lazy dog'.split(' '); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.truncate(fd, 27); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (const i of phrase) { + writeStream.write(i + ' '); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length > 30) { + expect(stat.size).toEqual(45); + expect(contents.length).toEqual(45); + } else { + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } - await Promise.all([ - new Promise((res) => { - writeStream.write(content, () => { - res(null); - }); - }), - efs.fallocate(fd, 0, 4096 * 2), + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.truncate(fd, 27); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (const i of phrase) { + writeStream.write(i + ' '); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), ]); - await new Promise((res) => { - writeStream.end(() => { - res(null); - }); - }); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length > 30) { + expect(stat.size).toEqual(45); + expect(contents.length).toEqual(45); + } else { + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } - // Both operations complete, order makes no difference. - const fileContents = await efs.readFile('file'); - expect(fileContents.length).toEqual(4096 * 2); - expect(fileContents.toString()).toContain('A'); - expect(fileContents).toContain(0x00); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(50); + return await efs.truncate(fd, 27); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (const i of phrase) { + writeStream.write(i + ' '); + await sleep(10); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length > 30) { + expect(stat.size).toEqual(45); + expect(contents.length).toEqual(45); + } else { + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } }); - test('Truncating while writing to stream', async () => { - await efs.writeFile('file', ''); - const writeStream = await efs.createWriteStream('file'); - const content = 'A'.repeat(4096 * 2); - const promise1 = new Promise((res) => { - writeStream.write(content, () => { - res(null); - }); - }); + test('EncryptedFS.ftruncate and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.ftruncate(fd, 27); + })(), + (async () => { + return await efs.writeFile( + path1, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length < 30) { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } else { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } - await Promise.all([promise1, efs.truncate('file', 4096)]); - await new Promise((res) => { - writeStream.end(() => { - res(null); - }); - }); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.ftruncate(fd, 27); + })(), + (async () => { + return await efs.writeFile( + path1, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length < 30) { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } else { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } + + // WriteFile with FdIndex + // cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + return await efs.ftruncate(fd, 27); + })(), + (async () => { + return await efs.writeFile( + fd, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length < 30) { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } else { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } - // Both operations complete, order makes no difference. Truncate doesn't do anything? - const fileContents = await efs.readFile('file'); - expect(fileContents.length).toEqual(4096 * 2); - expect(fileContents.toString()).toContain('A'); - expect(fileContents).not.toContain(0x00); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.ftruncate(fd, 27); + })(), + (async () => { + return await efs.writeFile( + fd, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length < 30) { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } else { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } }); - }); - test('File metadata changes while reading/writing a file.', async () => { - const fd1 = await efs.promises.open('file', flags.O_WRONLY | flags.O_CREAT); - const content = 'A'.repeat(2); - await Promise.all([ - efs.promises.writeFile(fd1, Buffer.from(content)), - efs.promises.utimes('file', 0, 0), - ]); - let stat = await efs.promises.stat('file'); - expect(stat.atime.getMilliseconds()).toBe(0); - expect(stat.mtime.getMilliseconds()).toBe(0); - await efs.close(fd1); - await efs.unlink('file'); - - const fd2 = await efs.promises.open('file', flags.O_WRONLY | flags.O_CREAT); - await Promise.all([ - efs.promises.utimes('file', 0, 0), - efs.promises.writeFile(fd2, Buffer.from(content)), - ]); - stat = await efs.promises.stat('file'); - expect(stat.atime.getMilliseconds()).toBe(0); - expect(stat.mtime.getMilliseconds()).toBeGreaterThan(0); - await efs.close(fd2); - }); - test('Dir metadata changes while reading/writing a file.', async () => { - const dir = 'directory'; - const PUT = path.join(dir, 'file'); - await efs.mkdir(dir); - const content = 'A'.repeat(2); - await Promise.all([ - efs.promises.writeFile(PUT, Buffer.from(content)), - efs.promises.utimes(dir, 0, 0), - ]); - let stat = await efs.promises.stat(dir); - expect(stat.atime.getMilliseconds()).toBe(0); - await efs.unlink(PUT); - await efs.rmdir(dir); - await efs.mkdir(dir); - await Promise.all([ - efs.promises.utimes(dir, 0, 0), - efs.promises.writeFile(PUT, Buffer.from(content)), - ]); - stat = await efs.promises.stat(dir); - expect(stat.atime.getMilliseconds()).toBe(0); - }); - describe('Changing fd location in a file (lseek) while writing/reading (and updating) fd pos', () => { - let fd; - beforeEach(async () => { - fd = await efs.open('file', flags.O_RDWR | flags.O_CREAT); - await efs.fallocate(fd, 0, 200); + test('EncryptedFS.ftruncate and EncryptedFS.write', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.ftruncate(fd, 27); + })(), + (async () => { + return await efs.write( + fd, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 44 }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length > 30) { + // Fallocate happened after write + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } else { + // Write happened after fallocate + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.ftruncate(fd, 27); + })(), + (async () => { + return await efs.write( + fd, + 'The quick brown fox jumped over the lazy dog', + ); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 44 }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length > 30) { + expect(stat.size).toEqual(44); + expect(contents.length).toEqual(44); + } else { + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } }); + test('EncryptedFS.ftruncate and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + const phrase = 'The quick brown fox jumped over the lazy dog'.split(' '); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + // WriteFile with path + let results = await Promise.allSettled([ + (async () => { + return await efs.ftruncate(fd, 27); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (const i of phrase) { + writeStream.write(i + ' '); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length > 30) { + expect(stat.size).toEqual(45); + expect(contents.length).toEqual(45); + } else { + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } - test('Seeking while writing to file.', async () => { - await efs.lseek(fd, 0, flags.SEEK_SET); - // Seeking before. - await Promise.all([ - efs.lseek(fd, 10, flags.SEEK_CUR), - efs.write(fd, Buffer.from('A'.repeat(10))), + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.ftruncate(fd, 27); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (const i of phrase) { + writeStream.write(i + ' '); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, ]); - let pos = await efs.lseek(fd, 0, flags.SEEK_CUR); - expect(pos).toEqual(20); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length > 30) { + expect(stat.size).toEqual(45); + expect(contents.length).toEqual(45); + } else { + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } - await efs.lseek(fd, 0, flags.SEEK_SET); - // Seeking after. - await Promise.all([ - efs.write(fd, Buffer.from('A'.repeat(10))), - efs.lseek(fd, 10, flags.SEEK_CUR), + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(50); + return await efs.ftruncate(fd, 27); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (const i of phrase) { + writeStream.write(i + ' '); + await sleep(10); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, ]); - pos = await efs.lseek(fd, 0, flags.SEEK_CUR); - expect(pos).toEqual(10); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length > 30) { + expect(stat.size).toEqual(45); + expect(contents.length).toEqual(45); + } else { + expect(stat.size).toEqual(27); + expect(contents.length).toEqual(27); + } }); - test('Seeking while reading a file.', async () => { - await efs.write(fd, Buffer.from('AAAAAAAAAABBBBBBBBBBCCCCCCCCCC')); - await efs.lseek(fd, 0, flags.SEEK_SET); - // Seeking before. - const buf = Buffer.alloc(10); - await Promise.all([ - efs.lseek(fd, 10, flags.SEEK_CUR), - efs.read(fd, buf, undefined, 10), + test('EncryptedFS.utimes and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + const nowTime = Date.now(); + await sleep(10); + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.utimes(path1, 0, 0); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), ]); - const pos = await efs.lseek(fd, 0, flags.SEEK_CUR); - expect(pos).toEqual(20); - expect(buf.toString()).toContain('B'); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + if (stat.mtime.getTime() === 0) { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toEqual(0); + } else { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toBeGreaterThan(nowTime); + } - await efs.lseek(fd, 0, flags.SEEK_SET); - // Seeking after. - const buf2 = Buffer.alloc(10); - await Promise.all([ - efs.read(fd, buf2, undefined, 10), - efs.lseek(fd, 10, flags.SEEK_CUR), + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.utimes(path1, 0, 0); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, ]); - const pos2 = await efs.lseek(fd, 0, flags.SEEK_CUR); - expect(pos2).toEqual(20); - expect(buf2.toString()).toContain('B'); + stat = await efs.stat(path1); + if (stat.mtime.getTime() === 0) { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toEqual(0); + } else { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toBeGreaterThan(nowTime); + } }); - test('Seeking while updating fd pos.', async () => { - await efs.lseek(fd, 0, flags.SEEK_SET); - // Seeking before. - await Promise.all([ - efs.lseek(fd, 10, flags.SEEK_CUR), - efs.lseek(fd, 20, flags.SEEK_SET), + test('EncryptedFS.futimes and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + const nowTime = Date.now(); + await sleep(10); + await efs.mkdir('dir'); + const fd = await efs.open(path1, 'wx+'); + await efs.writeFile(fd, 'test'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.futimes(fd, 0, 0); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), ]); - const pos = await efs.lseek(fd, 0, flags.SEEK_CUR); - expect(pos).toEqual(20); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + if (stat.mtime.getTime() === 0) { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toEqual(0); + } else { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toBeGreaterThan(nowTime); + } - await efs.lseek(fd, 0, flags.SEEK_SET); - // Seeking after. - await Promise.all([ - efs.lseek(fd, 20, flags.SEEK_SET), - efs.lseek(fd, 10, flags.SEEK_CUR), + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.futimes(fd, 0, 0); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), ]); - const pos2 = await efs.lseek(fd, 0, flags.SEEK_CUR); - expect(pos2).toEqual(30); - }); - }); - describe('checking if nlinks gets clobbered.', () => { - test('when creating and removing the file.', async () => { - // Need a way to check if only one inode was created in the end. - // otherwise do we have dangling inodes that are not going to get collected? - await Promise.all([ - efs.writeFile('file', ''), - efs.writeFile('file', ''), - efs.writeFile('file', ''), - efs.writeFile('file', ''), - efs.writeFile('file', ''), - ]); - const stat = await efs.stat('file'); - expect(stat.nlink).toEqual(1); - - const fd = await efs.open('file', 'r'); - try { - await Promise.all([ - efs.unlink('file'), - efs.unlink('file'), - efs.unlink('file'), - efs.unlink('file'), - efs.unlink('file'), - ]); - } catch (err) { - // Do nothing - } - const stat2 = await efs.fstat(fd); - expect(stat2.nlink).toEqual(0); - await efs.close(fd); - }); - test('when creating and removing links.', async () => { - await efs.writeFile('file', ''); - - // One link to a file multiple times. - try { - await Promise.all([ - efs.link('file', 'link'), - efs.link('file', 'link'), - efs.link('file', 'link'), - efs.link('file', 'link'), - efs.link('file', 'link'), - ]); - } catch (e) { - // Do nothing - } - const stat = await efs.stat('file'); - expect(stat.nlink).toEqual(2); - - // Removing one link multiple times. - try { - await Promise.all([ - efs.unlink('link'), - efs.unlink('link'), - efs.unlink('link'), - efs.unlink('link'), - efs.unlink('link'), - ]); - } catch (e) { - // Do nothing - } - const stat2 = await efs.stat('file'); - expect(stat2.nlink).toEqual(1); - - // Multiple links to a file. - await Promise.all([ - efs.link('file', 'link1'), - efs.link('file', 'link2'), - efs.link('file', 'link3'), - efs.link('file', 'link4'), - efs.link('file', 'link5'), - ]); - const stat3 = await efs.stat('file'); - expect(stat3.nlink).toEqual(6); - - // Removing one link multiple times. - try { - await Promise.all([ - efs.unlink('link1'), - efs.unlink('link2'), - efs.unlink('link3'), - efs.unlink('link4'), - efs.unlink('link5'), - ]); - } catch (e) { - // Do nothing - } - const stat4 = await efs.stat('file'); - expect(stat4.nlink).toEqual(1); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + if (stat.mtime.getTime() === 0) { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toEqual(0); + } else { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toBeGreaterThan(nowTime); + } + await efs.close(fd); }); - }); - test('Read stream and write stream to same file', async (done) => { - await efs.writeFile('file', ''); - const readStream = await efs.createReadStream('file'); - const writeStream = await efs.createWriteStream('file', { flags: 'w+' }); - const contents = 'A'.repeat(4096); - - // Write two blocks. - writeStream.write(Buffer.from(contents)); - // WriteStream.end(); - await sleep(1000); - let readString = ''; - for await (const data of readStream) { - readString += data; - } - expect(readString.length).toEqual(4096); - writeStream.end(async () => { - await sleep(100); - done(); - }); - - // WriteStream.write(Buffer.from(contents)); - // await sleep(1000); - // - // for await (const data of readStream) { - // readString += data; - // } - // expect(readString.length).toEqual(4096); - }); - test('One write stream and one fd writing to the same file', async () => { - await efs.writeFile('file', ''); - const fd = await efs.open('file', flags.O_RDWR); - const writeStream = await efs.createWriteStream('file'); - - await Promise.all([ - new Promise((res) => { - writeStream.write(Buffer.from('A'.repeat(10)), () => { - res(null); - }); - }), - efs.write(fd, Buffer.from('B'.repeat(10))), - new Promise((res) => { - writeStream.write(Buffer.from('C'.repeat(10)), () => { - res(null); - }); - }), - new Promise((res) => { - writeStream.end(); - writeStream.on('finish', () => { - res(null); - }); - }), - ]); - - // The writeStream overwrites the file. likely because it finishes last and writes everything at once. - const fileContents = (await efs.readFile('file')).toString(); - expect(fileContents).toContain('A'); - expect(fileContents).not.toContain('B'); - expect(fileContents).toContain('C'); - }); - test('One read stream and one fd writing to the same file', async () => { - await efs.writeFile('file', ''); - const fd = await efs.open('file', flags.O_RDWR); - const readStream = await efs.createReadStream('file'); - let readData = ''; + test('EncryptedFS.utimes a directory and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'dir1'); + const path2 = path.join(path1, 'file1'); + const nowTime = Date.now(); + await sleep(10); + await efs.mkdir('dir'); + await efs.mkdir(path1); + await efs.writeFile(path2, 'test'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.utimes(path1, 0, 0); + })(), + (async () => { + return await efs.writeFile(path2, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + if (stat.mtime.getTime() === 0) { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toEqual(0); + } else { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toBeGreaterThan(nowTime); + } - readStream.on('data', (data) => { - readData += data; - }); - const streamEnd = new Promise((res) => { - readStream.on('end', () => { - res(null); - }); + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.utimes(path1, 0, 0); + })(), + (async () => { + return await efs.writeFile(path2, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + if (stat.mtime.getTime() === 0) { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toEqual(0); + } else { + expect(stat.atime.getTime()).toEqual(0); + expect(stat.mtime.getTime()).toBeGreaterThan(nowTime); + } }); + test('EncryptedFS.lseek and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + expect(stat.size).toEqual(4); + expect(contents.length).toEqual(4); - await Promise.all([ - efs.write(fd, Buffer.from('A'.repeat(10))), - efs.write(fd, Buffer.from('B'.repeat(10))), - streamEnd, - ]); - - await sleep(100); - - // Only the last write data gets read. - expect(readData).not.toContain('A'); - expect(readData).toContain('B'); - expect(readData).not.toContain('C'); - }); - test('One write stream and one fd reading to the same file', async () => { - await efs.writeFile('file', ''); - const fd = await efs.open('file', flags.O_RDWR); - const writeStream = await efs.createWriteStream('file'); - const buf1 = Buffer.alloc(20); - const buf2 = Buffer.alloc(20); - const buf3 = Buffer.alloc(20); - - await Promise.all([ - new Promise((res) => { - writeStream.write(Buffer.from('A'.repeat(10)), () => { - res(null); - }); - }), - efs.read(fd, buf1, 0, 20), - new Promise((res) => { - writeStream.write(Buffer.from('B'.repeat(10)), () => { - res(null); - }); - }), - efs.read(fd, buf2, 0, 20), - new Promise((res) => { - writeStream.end(); - writeStream.on('finish', () => { - res(null); - }); - }), - ]); - await efs.read(fd, buf3, 0, 20); - - // Efs.read only reads data after the write stream finishes. - expect(buf1.toString()).not.toContain('AB'); - expect(buf2.toString()).not.toContain('AB'); - expect(buf3.toString()).toContain('AB'); - }); - test('One read stream and one fd reading to the same file', async () => { - await efs.writeFile('file', 'AAAAAAAAAABBBBBBBBBB'); - const fd = await efs.open('file', flags.O_RDONLY); - const readStream = await efs.createReadStream('file'); - let readData = ''; - - readStream.on('data', (data) => { - readData += data; - }); - const streamEnd = new Promise((res) => { - readStream.on('end', () => { - res(null); - }); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + expect(stat.size).toEqual(4); + expect(contents.length).toEqual(4); }); - const buf = Buffer.alloc(20); + test('EncryptedFS.lseek and EncryptedFS.writeFile with fd', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return await efs.writeFile(fd, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + expect(stat.size).toEqual(4); + expect(contents.length).toEqual(4); - await Promise.all([efs.read(fd, buf, 0, 20), streamEnd]); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return await efs.writeFile(fd, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + expect(stat.size).toEqual(4); + expect(contents.length).toEqual(4); + }); + test('EncryptedFS.lseek and EncryptedFS.write', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return await efs.write(fd, 'test'); + })(), + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + if (contents.length > 15) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: 4 }, + ]); + expect(stat.size).toEqual(24); + expect(contents.length).toEqual(24); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 24 }, + { status: 'fulfilled', value: 4 }, + ]); + expect(stat.size).toEqual(4); + expect(contents.length).toEqual(4); + } - await sleep(100); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return await efs.write(fd, 'test'); + })(), + ]); - // Ok, is efs.read() broken? - expect(readData).toContain('AB'); - expect(buf.toString()).toContain('AB'); - }); - test('Two write streams to the same file', async () => { - const contentSize = 4096 * 3; - const contents = [ - 'A'.repeat(contentSize), - 'B'.repeat(contentSize), - 'C'.repeat(contentSize), - ]; - let streams: Array = []; - - // Each stream sequentially. - for (let i = 0; i < contents.length; i++) { - streams.push(await efs.createWriteStream('file')); - } - for (let i = 0; i < streams.length; i++) { - streams[i].write(Buffer.from(contents[i])); - } - for (const stream of streams) { - stream.end(); - } - - await sleep(1000); - const fileContents = (await efs.readFile('file')).toString(); - expect(fileContents).not.toContain('A'); - expect(fileContents).not.toContain('B'); - expect(fileContents).toContain('C'); - - await efs.unlink('file'); - - // Each stream interlaced. - const contents2 = ['A'.repeat(4096), 'B'.repeat(4096), 'C'.repeat(4096)]; - streams = []; - for (let i = 0; i < contents2.length; i++) { - streams.push(await efs.createWriteStream('file')); - } - for (let j = 0; j < 3; j++) { - for (let i = 0; i < streams.length; i++) { - // Order we write to changes. - streams[(j + i) % 3].write(Buffer.from(contents2[(j + i) % 3])); - } - } - for (const stream of streams) { - stream.end(); - } - await sleep(1000); - const fileContents2 = (await efs.readFile('file')).toString(); - expect(fileContents2).not.toContain('A'); - expect(fileContents2).not.toContain('B'); - expect(fileContents2).toContain('C'); - // Conclusion. the last stream to close writes the whole contents of it's buffer to the file. - }); - test('Writing a file and deleting the file at the same time using writeFile', async () => { - await efs.writeFile('file', ''); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + if (contents.length > 15) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: 4 }, + ]); + expect(stat.size).toEqual(24); + expect(contents.length).toEqual(24); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 24 }, + { status: 'fulfilled', value: 4 }, + ]); + expect(stat.size).toEqual(4); + expect(contents.length).toEqual(4); + } + }); + test('EncryptedFS.lseek and EncryptedFS.readFile', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + await efs.writeFile(path1, 'test'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return (await efs.readFile(path1)).toString(); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: 'test' }, + ]); + let stat = await efs.stat(path1); + let contents = await efs.readFile(path1); + expect(stat.size).toEqual(4); + expect(contents.length).toEqual(4); - // Odd error, needs fixing. - await Promise.all([efs.writeFile('file', 'CONTENT!'), efs.unlink('file')]); - await expectError(efs.readFile('file'), errno.ENOENT); - }); - test('opening a file and deleting the file at the same time', async () => { - await efs.writeFile('file', ''); - - // Odd error, needs fixing. - const results = await Promise.all([ - efs.open('file', flags.O_WRONLY), - efs.unlink('file'), - ]); - const fd = results[0]; - await efs.write(fd, 'yooo'); - }); - test('Writing a file and deleting the file at the same time for fd', async () => { - await efs.writeFile('file', ''); - - const fd1 = await efs.open('file', flags.O_WRONLY); - await Promise.all([ - efs.write(fd1, Buffer.from('TESTING WOOo')), - efs.unlink('file'), - ]); - await efs.close(fd1); - expect(await efs.readdir('.')).toEqual([]); - - await efs.writeFile('file', ''); - const fd2 = await efs.open('file', flags.O_WRONLY); - await Promise.all([ - efs.unlink('file'), - efs.write(fd2, Buffer.from('TESTING TWOOo')), - ]); - await efs.close(fd2); - expect(await efs.readdir('.')).toEqual([]); - }); - test('Writing a file and deleting the file at the same time for stream', async () => { - await efs.writeFile('file', ''); - - const writeStream1 = await efs.createWriteStream('file'); - await Promise.all([ - new Promise((res) => { - writeStream1.write(Buffer.from('AAAAAAAAAA'), () => { - writeStream1.end(() => { - res(null); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + await efs.writeFile(path1, 'test'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return (await efs.readFile(path1)).toString(); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: 'test' }, + ]); + stat = await efs.stat(path1); + contents = await efs.readFile(path1); + expect(stat.size).toEqual(4); + expect(contents.length).toEqual(4); + }); + test('EncryptedFS.lseek and EncryptedFS.read', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + await efs.writeFile( + path1, + 'The quick brown fox jumped over the lazy dog', + ); + let fd = await efs.open(path1, 'r+'); + + const buffer = Buffer.alloc(45); + let results = await Promise.allSettled([ + (async () => { + return await efs.lseek(fd, 27, constants.SEEK_CUR); + })(), + (async () => { + return await efs.read(fd, buffer, undefined, 44); + })(), + ]); + if (results[1].status === 'fulfilled' && results[1].value > 30) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 71 }, + { status: 'fulfilled', value: 44 }, + ]); + expect(buffer.toString()).toContain('The quick brown fox'); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 27 }, + { status: 'fulfilled', value: 17 }, + ]); + expect(buffer.toString()).not.toContain('The quick brown fox'); + } + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile( + path1, + 'The quick brown fox jumped over the lazy dog', + ); + fd = await efs.open(path1, 'r+'); + buffer.fill(0); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.lseek(fd, 27, constants.SEEK_CUR); + })(), + (async () => { + return await efs.read(fd, buffer, undefined, 44); + })(), + ]); + if (results[1].status === 'fulfilled' && results[1].value > 30) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 71 }, + { status: 'fulfilled', value: 44 }, + ]); + expect(buffer.toString()).toContain('The quick brown fox'); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 27 }, + { status: 'fulfilled', value: 17 }, + ]); + expect(buffer.toString()).not.toContain('The quick brown fox'); + } + }); + test('EncryptedFS.lseek and EncryptedFS.lseek setting position', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + let fd = await efs.open(path1, 'wx+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return await efs.lseek(fd, 15, constants.SEEK_SET); + })(), + ]); + let pos = await efs.lseek(fd, 0, constants.SEEK_CUR); + if (pos > 30) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 35 }, + { status: 'fulfilled', value: 15 }, + ]); + expect(pos).toEqual(35); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: 15 }, + ]); + expect(pos).toEqual(15); + } + + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + fd = await efs.open(path1, 'wx+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.lseek(fd, 20, constants.SEEK_CUR); + })(), + (async () => { + return await efs.lseek(fd, 15, constants.SEEK_SET); + })(), + ]); + pos = await efs.lseek(fd, 0, constants.SEEK_CUR); + if (pos > 30) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 35 }, + { status: 'fulfilled', value: 15 }, + ]); + expect(pos).toEqual(35); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 20 }, + { status: 'fulfilled', value: 15 }, + ]); + expect(pos).toEqual(15); + } + }); + test('EncryptedFS.createReadStream and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'.repeat(5); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataB); + + let results = await Promise.allSettled([ + (async () => { + const readProm = new Promise((resolve, reject) => { + const readStream = efs.createReadStream(path1); + let readData = ''; + readStream.on('data', (data) => { + readData += data.toString(); + }); + readStream.on('end', () => { + resolve(readData); + }); + readStream.on('error', (e) => { + reject(e); + }); }); - }); - }), - efs.unlink('file'), - ]); - expect(await efs.readdir('.')).toEqual([]); - - await efs.writeFile('file', ''); - const writeStream2 = await efs.createWriteStream('file'); - await Promise.all([ - efs.unlink('file'), - new Promise((res) => { - writeStream2.write(Buffer.from('BBBBBBBBBB'), () => { - writeStream2.end(() => { - res(null); + return await readProm; + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + + if (results[0].status === 'fulfilled' && results[0].value[0] === 'A') { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataA.repeat(10) }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB }, + { status: 'fulfilled', value: undefined }, + ]); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataB); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + const readProm = new Promise((resolve, reject) => { + const readStream = efs.createReadStream(path1); + let readData = ''; + readStream.on('data', (data) => { + readData += data.toString(); + }); + readStream.on('end', () => { + resolve(readData); + }); + readStream.on('error', (e) => { + reject(e); + }); }); - }); - }), - ]); - expect(await efs.readdir('.')).toEqual([]); - }); - test('Appending to a file that is being written to for fd ', async () => { - await efs.writeFile('file', ''); - const fd1 = await efs.open('file', flags.O_WRONLY); - - await Promise.all([ - efs.write(fd1, Buffer.from('AAAAAAAAAA')), - efs.appendFile('file', 'BBBBBBBBBB'), - ]); - - const fileContents = (await efs.readFile('file')).toString(); - expect(fileContents).toContain('A'); - expect(fileContents).toContain('B'); - expect(fileContents).toContain('AB'); - await efs.close(fd1); - - await efs.writeFile('file', ''); - const fd2 = await efs.open('file', flags.O_WRONLY); - await Promise.all([ - efs.appendFile('file', 'BBBBBBBBBB'), - efs.write(fd2, Buffer.from('AAAAAAAAAA')), - ]); - - // The append seems to happen after the write. - const fileContents2 = (await efs.readFile('file')).toString(); - expect(fileContents2).toContain('A'); - expect(fileContents2).toContain('B'); - expect(fileContents2).toContain('AB'); - await sleep(1000); - await efs.close(fd2); - }); - test('Appending to a file that is being written for stream', async () => { - await efs.writeFile('file', ''); - const writeStream = await efs.createWriteStream('file'); - await Promise.all([ - new Promise((res) => { - writeStream.write(Buffer.from('AAAAAAAAAA'), () => { - writeStream.end(() => { - res(null); + return await readProm; + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + + if (results[0].status === 'fulfilled' && results[0].value[0] === 'A') { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataA.repeat(10) }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB }, + { status: 'fulfilled', value: undefined }, + ]); + } + }); + test('EncryptedFS.write and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'.repeat(5); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + return efs.write(fd, dataB); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 25 }, + { status: 'fulfilled', value: undefined }, + ]); + let stat = await efs.stat(path1); + let contents = (await efs.readFile(path1)).toString(); + + if (contents[0] === 'A') { + expect(contents).toEqual(dataA.repeat(10)); + expect(contents).toHaveLength(50); + expect(stat.size).toEqual(50); + } else { + expect(contents).toEqual(dataB + dataA.repeat(5)); + expect(contents).toHaveLength(50); + expect(stat.size).toEqual(50); + } + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return efs.write(fd, dataB); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 25 }, + { status: 'fulfilled', value: undefined }, + ]); + stat = await efs.stat(path1); + contents = (await efs.readFile(path1)).toString(); + + if (contents[0] === 'A') { + expect(contents).toEqual(dataA.repeat(10)); + expect(contents).toHaveLength(50); + expect(stat.size).toEqual(50); + } else { + expect(contents).toEqual(dataB + dataA.repeat(5)); + expect(contents).toHaveLength(50); + expect(stat.size).toEqual(50); + } + }); + test('EncryptedFS.createReadStream and EncryptedFS.write', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'.repeat(5); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataB); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + const readProm = new Promise((resolve, reject) => { + const readStream = efs.createReadStream(path1); + let readData = ''; + readStream.on('data', (data) => { + readData += data.toString(); + }); + readStream.on('end', () => { + resolve(readData); + }); + readStream.on('error', (e) => { + reject(e); + }); }); - }); - }), - efs.appendFile('file', 'BBBBBBBBBB'), - ]); - - const fileContents = (await efs.readFile('file')).toString(); - expect(fileContents).toContain('A'); - expect(fileContents).toContain('B'); - expect(fileContents).toContain('AB'); - - await efs.writeFile('file', ''); - const writeStream2 = await efs.createWriteStream('file'); - await Promise.all([ - efs.appendFile('file', 'BBBBBBBBBB'), - new Promise((res) => { - writeStream2.write(Buffer.from('AAAAAAAAAA'), () => { - writeStream2.end(() => { - res(null); + return await readProm; + })(), + (async () => { + await efs.write(fd, dataA); + })(), + ]); + + if (results[0].status === 'fulfilled' && results[0].value[0] === 'A') { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataA + dataB.slice(dataA.length) }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB }, + { status: 'fulfilled', value: undefined }, + ]); + } + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataB); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + const readProm = new Promise((resolve, reject) => { + const readStream = efs.createReadStream(path1); + let readData = ''; + readStream.on('data', (data) => { + readData += data.toString(); + }); + readStream.on('end', () => { + resolve(readData); + }); + readStream.on('error', (e) => { + reject(e); + }); + }); + return await readProm; + })(), + (async () => { + await efs.write(fd, dataA); + })(), + ]); + + if (results[0].status === 'fulfilled' && results[0].value[0] === 'A') { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataA + dataB.slice(dataA.length) }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB }, + { status: 'fulfilled', value: undefined }, + ]); + } + }); + test('EncryptedFS.read and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + let fd = await efs.open(path1, 'r+'); + const buffer = Buffer.alloc(100); + + let results = await Promise.allSettled([ + (async () => { + return efs.read(fd, buffer, undefined, 100); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + let contents = buffer.toString(); + + if (results[0].status === 'fulfilled' && results[0].value === 50) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 50 }, + { status: 'fulfilled', value: undefined }, + ]); + expect(contents).toEqual(dataA.repeat(10) + '\0'.repeat(50)); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 0 }, + { status: 'fulfilled', value: undefined }, + ]); + expect(contents).toEqual('\0'.repeat(100)); + } + + // Cleaning up + buffer.fill(0); + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return efs.read(fd, buffer, undefined, 100); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + contents = buffer.toString(); + + if (results[0].status === 'fulfilled' && results[0].value === 50) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 50 }, + { status: 'fulfilled', value: undefined }, + ]); + expect(contents).toEqual(dataA.repeat(10) + '\0'.repeat(50)); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 0 }, + { status: 'fulfilled', value: undefined }, + ]); + expect(contents).toEqual('\0'.repeat(100)); + } + }); + test('EncryptedFS.createReadStream and EncryptedFS.read', async () => { + const path1 = path.join('dir', 'file1'); + const dataB = 'BBBBB'.repeat(5); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataB); + const buffer = Buffer.alloc(100); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + const readProm = new Promise((resolve, reject) => { + const readStream = efs.createReadStream(path1); + let readData = ''; + readStream.on('data', (data) => { + readData += data.toString(); + }); + readStream.on('end', () => { + resolve(readData); + }); + readStream.on('error', (e) => { + reject(e); + }); + }); + return await readProm; + })(), + (async () => { + return efs.read(fd, buffer, undefined, 100); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB }, + { status: 'fulfilled', value: 25 }, + ]); + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataB); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + const readProm = new Promise((resolve, reject) => { + const readStream = efs.createReadStream(path1); + let readData = ''; + readStream.on('data', (data) => { + readData += data.toString(); + }); + readStream.on('end', () => { + resolve(readData); + }); + readStream.on('error', (e) => { + reject(e); + }); }); + return await readProm; + })(), + (async () => { + return efs.read(fd, buffer, undefined, 100); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB }, + { status: 'fulfilled', value: 25 }, + ]); + }); + test('EncryptedFS.createWriteStream and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'; + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataB); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + let stat = await efs.stat(path1); + let contents = (await efs.readFile(path1)).toString(); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA.repeat(10)); + expect(contents).toHaveLength(50); + expect(stat.size).toEqual(50); + } else { + expect(contents).toEqual(dataB.repeat(10)); + expect(contents).toHaveLength(50); + expect(stat.size).toEqual(50); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataB); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + stat = await efs.stat(path1); + contents = (await efs.readFile(path1)).toString(); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA.repeat(10)); + expect(contents).toHaveLength(50); + expect(stat.size).toEqual(50); + } else { + expect(contents).toEqual(dataB.repeat(10)); + expect(contents).toHaveLength(50); + expect(stat.size).toEqual(50); + } + }); + test('EncryptedFS.unlink and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + return await efs.unlink(path1); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if ((await efs.exists(path1)) === true) { + expect(await efs.exists(path1)).toEqual(true); + expect((await efs.readFile(path1)).toString()).toEqual('test'); + } else { + expect(await efs.exists(path1)).toEqual(false); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.unlink(path1); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if ((await efs.exists(path1)) === true) { + expect(await efs.exists(path1)).toEqual(true); + expect((await efs.readFile(path1)).toString()).toEqual('test'); + } else { + expect(await efs.exists(path1)).toEqual(false); + } + }); + test('EncryptedFS.unlink and EncryptedFS.open', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + return await efs.unlink(path1); + })(), + (async () => { + const fd = await efs.open(path1, 'r+'); + await efs.close(fd); + return fd; + })(), + ]); + if ( + results[0].status === 'fulfilled' && + results[1].status === 'rejected' + ) { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.ENOENT); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 0 }, + ]); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.unlink(path1); + })(), + (async () => { + const fd = await efs.open(path1, 'r+'); + await efs.close(fd); + return fd; + })(), + ]); + if ( + results[0].status === 'fulfilled' && + results[1].status === 'rejected' + ) { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, }); - }), - ]); + expectReason(results[1], ErrorEncryptedFSError, errno.ENOENT); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 0 }, + ]); + } + }); + test('EncryptedFS.unlink and EncryptedFS.write', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.unlink(path1); + })(), + (async () => { + return await efs.write(fd, 'test'); + })(), + ]); + await efs.close(fd); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 4 }, + ]); + expect(await efs.exists(path1)).toEqual(false); + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.unlink(path1); + })(), + (async () => { + return await efs.write(fd, 'test'); + })(), + ]); + await efs.close(fd); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 4 }, + ]); + expect(await efs.exists(path1)).toEqual(false); + }); + test('EncryptedFS.unlink and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + return await efs.unlink(path1); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (await efs.exists(path1)) { + const stat = await efs.stat(path1); + const contents = (await efs.readFile(path1)).toString(); + expect(contents).toEqual(dataA.repeat(10)); + expect(stat.size).toEqual(50); + } else { + expect(await efs.exists(path1)).toEqual(false); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.unlink(path1); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (await efs.exists(path1)) { + const stat = await efs.stat(path1); + const contents = (await efs.readFile(path1)).toString(); + expect(contents).toEqual(dataA.repeat(10)); + expect(stat.size).toEqual(50); + } else { + expect(await efs.exists(path1)).toEqual(false); + } + }); + test('EncryptedFS.appendFIle and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'A'.repeat(10); + const dataB = 'B'.repeat(10); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + return await efs.appendFile(path1, dataA); + })(), + (async () => { + return await efs.writeFile(path1, dataB); + })(), + ]); + let stat = await efs.stat(path1); + let contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (stat.size > 15) { + expect(contents).toEqual(dataB + dataA); + expect(stat.size).toEqual(20); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.appendFile(path1, dataA); + })(), + (async () => { + return await efs.writeFile(path1, dataB); + })(), + ]); + stat = await efs.stat(path1); + contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (stat.size > 15) { + // Expected BB..BBAA..AA + expect(contents).toEqual(dataB + dataA); + expect(stat.size).toEqual(20); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + }); + test('EncryptedFS.appendFIle and EncryptedFS.writeFile with fd', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'A'.repeat(10); + const dataB = 'B'.repeat(10); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.appendFile(fd, dataA); + })(), + (async () => { + return await efs.writeFile(fd, dataB); + })(), + ]); + let stat = await efs.stat(path1); + let contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (stat.size > 15) { + expect(contents).toEqual(dataB + dataA); + expect(stat.size).toEqual(20); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.appendFile(fd, dataA); + })(), + (async () => { + return await efs.writeFile(fd, dataB); + })(), + ]); + stat = await efs.stat(path1); + contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (stat.size > 15) { + expect(contents).toEqual(dataB + dataA); + expect(stat.size).toEqual(20); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + }); + test('EncryptedFS.appendFIle and EncryptedFS.write', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'A'.repeat(10); + const dataB = 'B'.repeat(10); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.appendFile(fd, dataA); + })(), + (async () => { + return await efs.write(fd, dataB); + })(), + ]); + let stat = await efs.stat(path1); + let contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 10 }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA + dataB); + expect(stat.size).toEqual(20); + } else { + expect(contents).toEqual(dataB + dataA); + expect(stat.size).toEqual(20); + } + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.appendFile(fd, dataA); + })(), + (async () => { + return await efs.write(fd, dataB); + })(), + ]); + stat = await efs.stat(path1); + contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 10 }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA + dataB); + expect(stat.size).toEqual(20); + } else { + expect(contents).toEqual(dataB + dataA); + expect(stat.size).toEqual(20); + } + }); + test('EncryptedFS.appendFIle and EncryptedFS.createReadStream', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBBBBBBBBBBBBBBBBB'; + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + return await efs.appendFile(path1, dataB); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + let stat = await efs.stat(path1); + let contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents.length > 55) { + expect(contents).toEqual(dataA.repeat(10) + dataB); + expect(stat.size).toEqual(70); + } else { + expect(contents).toEqual(dataA.repeat(10)); + expect(stat.size).toEqual(50); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.appendFile(path1, dataB); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + stat = await efs.stat(path1); + contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents.length > 55) { + expect(contents).toEqual(dataA.repeat(10) + dataB); + expect(stat.size).toEqual(70); + } else { + expect(contents).toEqual(dataA.repeat(10)); + expect(stat.size).toEqual(50); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(50); + return await efs.appendFile(path1, dataB); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + await sleep(10); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + stat = await efs.stat(path1); + contents = (await efs.readFile(path1)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents.length > 51) { + expect(contents).toContain(dataA[0]); + expect(contents).toContain(dataB[0]); + expect(stat.size).toBeGreaterThanOrEqual(50); + } else { + expect(contents).toContain(dataA[0]); + expect(contents).not.toContain(dataB[0]); + expect(stat.size).toEqual(50); + } + }); + test('EncryptedFS.copyFile and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + const dataA = 'A'.repeat(10); + const dataB = 'B'.repeat(10); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + + let results = await Promise.allSettled([ + (async () => { + return await efs.copyFile(path1, path2); + })(), + (async () => { + return await efs.writeFile(path1, dataB); + })(), + ]); + let stat = await efs.stat(path2); + let contents = (await efs.readFile(path2)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA); + expect(stat.size).toEqual(10); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.copyFile(path1, path2); + })(), + (async () => { + return await efs.writeFile(path1, dataB); + })(), + ]); + stat = await efs.stat(path2); + contents = (await efs.readFile(path2)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA); + expect(stat.size).toEqual(10); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + }); + test('EncryptedFS.copyFile and EncryptedFS.writeFile with fd', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + const dataA = 'A'.repeat(10); + const dataB = 'B'.repeat(10); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.copyFile(path1, path2); + })(), + (async () => { + return await efs.writeFile(fd, dataB); + })(), + ]); + let stat = await efs.stat(path2); + let contents = (await efs.readFile(path2)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA); + expect(stat.size).toEqual(10); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.copyFile(path1, path2); + })(), + (async () => { + return await efs.writeFile(fd, dataB); + })(), + ]); + stat = await efs.stat(path2); + contents = (await efs.readFile(path2)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA); + expect(stat.size).toEqual(10); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + }); + test('EncryptedFS.copyFile and EncryptedFS.write', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + const dataA = 'A'.repeat(10); + const dataB = 'B'.repeat(10); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.copyFile(path1, path2); + })(), + (async () => { + return await efs.write(fd, dataB); + })(), + ]); + let stat = await efs.stat(path2); + let contents = (await efs.readFile(path2)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 10 }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA); + expect(stat.size).toEqual(10); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.copyFile(path1, path2); + })(), + (async () => { + return await efs.write(fd, dataB); + })(), + ]); + stat = await efs.stat(path2); + contents = (await efs.readFile(path2)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: 10 }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA); + expect(stat.size).toEqual(10); + } else { + expect(contents).toEqual(dataB); + expect(stat.size).toEqual(10); + } + }); + test('EncryptedFS.copyFile and EncryptedFS.createWriteStream', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + const dataA = 'AAAAA'; + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + return await efs.copyFile(path1, path2); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + let stat = await efs.stat(path2); + let contents = (await efs.readFile(path2)).toString(); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA.repeat(10)); + expect(stat.size).toEqual(50); + } else { + expect(contents).toEqual(''); + expect(stat.size).toEqual(0); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.copyFile(path1, path2); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + stat = await efs.stat(path2); + contents = (await efs.readFile(path2)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA.repeat(10)); + expect(stat.size).toEqual(50); + } else { + expect(contents).toEqual(''); + expect(stat.size).toEqual(0); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(50); + return await efs.copyFile(path1, path2); + })(), + (async () => { + const writeStream = efs.createWriteStream(path1); + for (let i = 0; i < 10; i++) { + writeStream.write(dataA); + await sleep(10); + } + writeStream.end(); + const endProm = promise(); + writeStream.on('finish', () => endProm.resolveP()); + await endProm.p; + })(), + ]); + stat = await efs.stat(path2); + contents = (await efs.readFile(path2)).toString(); + + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: undefined }, + ]); + if (contents[0] === 'A') { + expect(contents).toEqual(dataA.repeat(10)); + expect(stat.size).toEqual(50); + } else { + expect(contents).toEqual(''); + expect(stat.size).toEqual(0); + } + }); + test('EncryptedFS.readFile and EncryptedFS.writeFile', async () => { + const path1 = path.join('dir', 'file1'); + await efs.mkdir('dir'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'; + await efs.writeFile(path1, dataA); + + let results = await Promise.allSettled([ + (async () => { + return await efs.writeFile(path1, dataB); + })(), + (async () => { + return (await efs.readFile(path1)).toString(); + })(), + ]); + + if (results[1].status === 'fulfilled' && results[1].value[0] === 'A') { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: dataA }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: dataB }, + ]); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.writeFile(path1, dataB); + })(), + (async () => { + return (await efs.readFile(path1)).toString(); + })(), + ]); + + if (results[1].status === 'fulfilled' && results[1].value[0] === 'A') { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: dataA }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: undefined }, + { status: 'fulfilled', value: dataB }, + ]); + } + }); + test('EncryptedFS.read and EncryptedFS.write with different fd', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'; + const buffer = Buffer.alloc(100); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + let fd1 = await efs.open(path1, 'r+'); + let fd2 = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + // Await sleep(100); + return await efs.write(fd1, dataB); + })(), + (async () => { + return await efs.read(fd2, buffer, undefined, 100); + })(), + ]); + if (buffer.toString()[0] === 'A') { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataA.length }, + { status: 'fulfilled', value: dataB.length }, + ]); + expect(buffer.toString()).toContain('A'); + expect(buffer.toString()).not.toContain('B'); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB.length }, + { status: 'fulfilled', value: dataB.length }, + ]); + expect(buffer.toString()).not.toContain('A'); + expect(buffer.toString()).toContain('B'); + } + + // Cleaning up + await efs.close(fd1); + await efs.close(fd2); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + fd1 = await efs.open(path1, 'r+'); + fd2 = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.write(fd1, dataB); + })(), + (async () => { + return await efs.read(fd2, buffer, undefined, 100); + })(), + ]); + if (buffer.toString()[0] === 'A') { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB.length }, + { status: 'fulfilled', value: dataA.length }, + ]); + expect(buffer.toString()).toContain('A'); + expect(buffer.toString()).not.toContain('B'); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB.length }, + { status: 'fulfilled', value: dataB.length }, + ]); + expect(buffer.toString()).not.toContain('A'); + expect(buffer.toString()).toContain('B'); + } + }); + test('EncryptedFS.read and EncryptedFS.write with same fd', async () => { + const path1 = path.join('dir', 'file1'); + const dataA = 'AAAAA'; + const dataB = 'BBBBB'; + const buffer = Buffer.alloc(100); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + // Await sleep(100); + return await efs.write(fd, dataB); + })(), + (async () => { + return await efs.read(fd, buffer, undefined, 100); + })(), + ]); + let stat = await efs.stat(path1); + if (stat.size === 5) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB.length }, + { status: 'fulfilled', value: 0 }, + ]); + expect(buffer.toString()).not.toContain('A'); + expect(buffer.toString()).not.toContain('B'); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB.length }, + { status: 'fulfilled', value: dataB.length }, + ]); + expect(buffer.toString()).toContain('A'); + expect(buffer.toString()).not.toContain('B'); + } - // Append seems to happen after stream. - const fileContents2 = (await efs.readFile('file')).toString(); - expect(fileContents2).toContain('A'); - expect(fileContents2).toContain('B'); + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, dataA); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.write(fd, dataB); + })(), + (async () => { + return await efs.read(fd, buffer, undefined, 100); + })(), + ]); + stat = await efs.stat(path1); + if (stat.size === 5) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB.length }, + { status: 'fulfilled', value: 0 }, + ]); + expect(buffer.toString()).not.toContain('A'); + expect(buffer.toString()).not.toContain('B'); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: dataB.length }, + { status: 'fulfilled', value: dataB.length }, + ]); + expect(buffer.toString()).toContain('A'); + expect(buffer.toString()).not.toContain('B'); + } + }); }); - test('Copying a file that is being written to for fd', async () => { - await efs.writeFile('file', 'AAAAAAAAAA'); - const fd1 = await efs.open('file', flags.O_WRONLY); - - await Promise.all([ - efs.write(fd1, Buffer.from('BBBBBBBBBB')), - efs.copyFile('file', 'fileCopy'), - ]); - - // Gets overwritten before copy. - const fileContents = (await efs.readFile('fileCopy')).toString(); - expect(fileContents).not.toContain('A'); - expect(fileContents).toContain('B'); - - await efs.close(fd1); - await efs.writeFile('file', 'AAAAAAAAAA'); - const fd2 = await efs.open('file', flags.O_WRONLY); - await efs.unlink('fileCopy'); - - await Promise.all([ - efs.copyFile('file', 'fileCopy'), - efs.write(fd2, Buffer.from('BBBBBBBBBB')), - ]); - - // Also gets overwritten before copy. - const fileContents2 = (await efs.readFile('fileCopy')).toString(); - expect(fileContents2).not.toContain('A'); - expect(fileContents2).toContain('B'); + describe('concurrent directory manipulation', () => { + test('EncryptedFS.mkdir', async () => { + const results = await Promise.allSettled([ + efs.mkdir('dir'), + efs.mkdir('dir'), + ]); + expect( + results.some((result) => { + return result.status === 'fulfilled'; + }), + ).toBe(true); + expect( + results.some((result) => { + const status = result.status === 'rejected'; + if (status) { + expect(result.reason).toBeInstanceOf(ErrorEncryptedFSError); + expect(result.reason).toHaveProperty('code', errno.EEXIST.code); + expect(result.reason).toHaveProperty('errno', errno.EEXIST.errno); + expect(result.reason).toHaveProperty( + 'description', + errno.EEXIST.description, + ); + } + return status; + }), + ).toBe(true); + }); + test('EncryptedFS.mkdir with recursive creation', async () => { + await Promise.all([ + efs.mkdir('1/dira/dirb', { recursive: true }), + efs.mkdir('1/dira/dirb', { recursive: true }), + ]); + expect(await efs.readdir('1/dira')).toStrictEqual(['dirb']); + expect(await efs.readdir('1/dira/dirb')).toStrictEqual([]); + // Asymmetric directory creation + // the first promise will create dira and dira/dirb + // the second promise will create dira/dirb/dirc + await Promise.all([ + efs.mkdir('2/dira/dirb', { recursive: true }), + efs.mkdir('2/dira/dirb/dirc', { recursive: true }), + ]); + expect(await efs.readdir('2/dira/dirb')).toStrictEqual(['dirc']); + expect(await efs.readdir('2/dira/dirb/dirc')).toStrictEqual([]); + }); + test('EncryptedFS.rename', async () => { + // Only the first rename works, the rest fail + await efs.mkdir('test'); + const results = await Promise.allSettled([ + efs.rename('test', 'one'), + efs.rename('test', 'two'), + efs.rename('test', 'three'), + efs.rename('test', 'four'), + efs.rename('test', 'five'), + efs.rename('test', 'six'), + ]); + const i = results.findIndex((result) => result.status === 'fulfilled'); + results.splice(i, 1); + expect( + results.every((result: PromiseRejectedResult) => { + const status = result.status === 'rejected'; + if (status) { + expect(result.reason).toBeInstanceOf(ErrorEncryptedFSError); + expect(result.reason).toHaveProperty('code', errno.ENOENT.code); + expect(result.reason).toHaveProperty('errno', errno.ENOENT.errno); + expect(result.reason).toHaveProperty( + 'description', + errno.ENOENT.description, + ); + } + return status; + }), + ).toBe(true); + expect(await efs.readdir('.')).toContain('one'); + }); + test('EncryptedFS.readdir and EncryptedFS.rmdir', async () => { + await efs.mkdir('dir'); + // It is possible for only one to succeed or both can succeed + let results = await Promise.allSettled([ + (async () => { + // Await sleep(10); + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.rmdir('dir'); + })(), + ]); + if (results.every((result) => result.status === 'fulfilled')) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: [] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + // Has to be readdir that fails if rmdir quickly + const result = results[0] as PromiseRejectedResult; + expect(result.status).toBe('rejected'); + expect(result.reason).toBeInstanceOf(ErrorEncryptedFSError); + expect(result.reason).toHaveProperty('code', errno.ENOENT.code); + expect(result.reason).toHaveProperty('errno', errno.ENOENT.errno); + expect(result.reason).toHaveProperty( + 'description', + errno.ENOENT.description, + ); + } + results = await Promise.allSettled([ + (async () => { + await sleep(0); + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.rmdir('dir'); + })(), + ]); + if (results.every((result) => result.status === 'fulfilled')) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: [] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + // Has to be readdir that fails if rmdir quickly + const result = results[0] as PromiseRejectedResult; + expect(result.status).toBe('rejected'); + expect(result.reason).toBeInstanceOf(ErrorEncryptedFSError); + expect(result.reason).toHaveProperty('code', errno.ENOENT.code); + expect(result.reason).toHaveProperty('errno', errno.ENOENT.errno); + expect(result.reason).toHaveProperty( + 'description', + errno.ENOENT.description, + ); + } + }); + test('EncryptedFS.readdir and EncryptedFS.mkdir', async () => { + await efs.mkdir('dir'); + const path1 = path.join('dir', 'file1'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.mkdir(path1); + })(), + ]); + if ( + results.every((result) => { + if ( + result.status === 'fulfilled' && + typeof result.value === 'object' && + result.value.length === 0 + ) + return true; + return result.status === 'fulfilled' && result.value === undefined; + }) + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: [] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file1'] }, + { status: 'fulfilled', value: undefined }, + ]); + } + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + + results = await Promise.allSettled([ + (async () => { + await sleep(0); + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.mkdir(path1); + })(), + ]); + if ( + results.every((result) => { + return ( + result.status === 'fulfilled' && + (result.value === [] || result.value === undefined) + ); + }) + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: [] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file1'] }, + { status: 'fulfilled', value: undefined }, + ]); + } + }); + test('EncryptedFS.readdir and EncryptedFS.writeFile', async () => { + await efs.mkdir('dir'); + const path1 = path.join('dir', 'file1'); + let results = await Promise.allSettled([ + (async () => { + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + if ( + results.every((result) => { + if ( + result.status === 'fulfilled' && + typeof result.value === 'object' && + result.value.length === 0 + ) + return true; + return result.status === 'fulfilled' && result.value === undefined; + }) + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: [] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file1'] }, + { status: 'fulfilled', value: undefined }, + ]); + } + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + + results = await Promise.allSettled([ + (async () => { + await sleep(0); + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.writeFile(path1, 'test'); + })(), + ]); + if ( + results.every((result) => { + if ( + result.status === 'fulfilled' && + typeof result.value === 'object' && + result.value.length === 0 + ) + return true; + return result.status === 'fulfilled' && result.value === undefined; + }) + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: [] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file1'] }, + { status: 'fulfilled', value: undefined }, + ]); + } + }); + test('EncryptedFS.readdir and EncryptedFS.rename', async () => { + const PATH1 = path.join('dir', 'file1'); + const PATH2 = path.join('dir', 'file2'); + await efs.mkdir('dir'); + await efs.writeFile(PATH1, 'test'); + + // With files + let results = await Promise.allSettled([ + (async () => { + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.rename(PATH1, PATH2); + })(), + ]); + if ( + results.every((result) => { + if ( + result.status === 'fulfilled' && + typeof result.value === 'object' && + result.value.includes('file1') + ) + return true; + return result.status === 'fulfilled' && result.value === undefined; + }) + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file1'] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file2'] }, + { status: 'fulfilled', value: undefined }, + ]); + } + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(PATH1, 'test'); + + results = await Promise.allSettled([ + (async () => { + await sleep(10); + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.rename(PATH1, PATH2); + })(), + ]); + if ( + results.every((result) => { + if ( + result.status === 'fulfilled' && + typeof result.value === 'object' && + result.value.includes('file1') + ) + return true; + return result.status === 'fulfilled' && result.value === undefined; + }) + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file1'] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file2'] }, + { status: 'fulfilled', value: undefined }, + ]); + } + + // With directories + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.mkdir(PATH1); + results = await Promise.allSettled([ + (async () => { + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.rename(PATH1, PATH2); + })(), + ]); + if ( + results.every((result) => { + if ( + result.status === 'fulfilled' && + typeof result.value === 'object' && + result.value.includes('file1') + ) + return true; + return result.status === 'fulfilled' && result.value === undefined; + }) + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file1'] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file2'] }, + { status: 'fulfilled', value: undefined }, + ]); + } + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.mkdir(PATH1); + + results = await Promise.allSettled([ + (async () => { + await sleep(10); + return await efs.readdir('dir'); + })(), + (async () => { + return await efs.rename(PATH1, PATH2); + })(), + ]); + if ( + results.every((result) => { + if ( + result.status === 'fulfilled' && + typeof result.value === 'object' && + result.value.includes('file1') + ) + return true; + return result.status === 'fulfilled' && result.value === undefined; + }) + ) { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file1'] }, + { status: 'fulfilled', value: undefined }, + ]); + } else { + expect(results).toStrictEqual([ + { status: 'fulfilled', value: ['file2'] }, + { status: 'fulfilled', value: undefined }, + ]); + } + }); + test('EncryptedFS.rmdir and EncryptedFS.rename', async () => { + const PATH1 = path.join('dir', 'p1'); + const PATH2 = path.join('dir', 'p2'); + await efs.mkdir('dir'); + await efs.mkdir(PATH1); + + // Directories + let results = await Promise.allSettled([ + (async () => { + return await efs.rmdir(PATH1); + })(), + (async () => { + return await efs.rename(PATH1, PATH2); + })(), + ]); + if ( + results[0].status === 'fulfilled' && + results[1].status === 'rejected' + ) { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.ENOENT); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.ENOENT); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.mkdir(PATH1); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.rmdir(PATH1); + })(), + (async () => { + return await efs.rename(PATH1, PATH2); + })(), + ]); + if ( + results[0].status === 'fulfilled' && + results[1].status === 'rejected' + ) { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.ENOENT); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.ENOENT); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + }); }); - test('Copying a file that is being written to for stream', async () => { - await efs.writeFile('file', 'AAAAAAAAAA'); - const writeStream = await efs.createWriteStream('file'); - - await Promise.all([ - new Promise((res) => { - writeStream.write(Buffer.from('BBBBBBBBBB'), () => { - writeStream.end(() => { - res(null); - }); + describe('concurrent symlinking', () => { + test('EncryptedFS.symlink and EncryptedFS.symlink', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.symlink(path1, path2); + })(), + (async () => { + return await efs.symlink(path1, path2); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, }); - }), - efs.copyFile('file', 'fileCopy'), - ]); - - // Write happens first. - const fileContents = (await efs.readFile('fileCopy')).toString(); - expect(fileContents).not.toContain('A'); - expect(fileContents).toContain('B'); - - await efs.writeFile('file', 'AAAAAAAAAA'); - await efs.unlink('fileCopy'); - const writeStream2 = await efs.createWriteStream('file'); - - await Promise.all([ - efs.copyFile('file', 'fileCopy'), - new Promise((res) => { - writeStream2.write(Buffer.from('BBBBBBBBBB'), () => { - writeStream2.end(() => { - res(null); - }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, }); - }), - ]); + } - // Copy happens after stream. - const fileContents2 = (await efs.readFile('fileCopy')).toString(); - expect(fileContents2).not.toContain('A'); - expect(fileContents2).toContain('B'); - await sleep(100); + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.symlink(path1, path2); + })(), + (async () => { + return await efs.symlink(path1, path2); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + }); + test('EncryptedFS.symlink and EncryptedFS.mknod', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + return await efs.symlink(path1, path2); + })(), + (async () => { + return await efs.mknod(path2, constants.S_IFREG, 0, 0); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.symlink(path1, path2); + })(), + (async () => { + return await efs.mknod(path2, constants.S_IFREG, 0, 0); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + }); + test('EncryptedFS.mkdir and EncryptedFS.symlink', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + let results = await Promise.allSettled([ + (async () => { + return await efs.symlink(path1, path2); + })(), + (async () => { + return await efs.mkdir(path2); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.symlink(path1, path2); + })(), + (async () => { + return await efs.mkdir(path2); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + }); + test('EncryptedFS.write and EncryptedFS.symlink', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + let fd = await efs.open(path1, 'r+'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.write(fd, 'test'); + })(), + (async () => { + return await efs.symlink(path1, path2); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 4 }, + { status: 'fulfilled', value: undefined }, + ]); + + // Cleaning up + await efs.close(fd); + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, ''); + fd = await efs.open(path1, 'r+'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.write(fd, 'test'); + })(), + (async () => { + return await efs.symlink(path1, path2); + })(), + ]); + expect(results).toStrictEqual([ + { status: 'fulfilled', value: 4 }, + { status: 'fulfilled', value: undefined }, + ]); + }); }); - test('removing a dir while renaming it.', async () => { - // Create the directory - await efs.mkdir('dir'); - // Removing and renaming. - await Promise.all([ - efs.rmdir('dir'), - expectError(efs.rename('dir', 'renamedDir'), errno.ENOENT), - ]); - let list = await efs.readdir('.'); - expect(list).toEqual([]); - - // Reverse order. - await efs.mkdir('dir2'); - await Promise.all([ - expectError(efs.rename('dir2', 'renamedDir2'), errno.ENOENT), - efs.rmdir('dir2'), - ]); - list = await efs.readdir('.'); - expect(list).toEqual([]); + describe('concurrent inode linking and unlinking', () => { + test('EncryptedFS.link and EncryptedFS.link', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.link(path1, path2); + })(), + (async () => { + return await efs.link(path1, path2); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.link(path1, path2); + })(), + (async () => { + return await efs.link(path1, path2); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + }); + test('EncryptedFS.link and EncryptedFS.symlink', async () => { + const path1 = path.join('dir', 'file1'); + const path2 = path.join('dir', 'file2'); + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + + let results = await Promise.allSettled([ + (async () => { + return await efs.link(path1, path2); + })(), + (async () => { + return await efs.symlink(path1, path2); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + + // Cleaning up + await efs.rmdir('dir', { recursive: true }); + await efs.mkdir('dir'); + await efs.writeFile(path1, 'test'); + + results = await Promise.allSettled([ + (async () => { + await sleep(100); + return await efs.link(path1, path2); + })(), + (async () => { + return await efs.symlink(path1, path2); + })(), + ]); + if (results[0].status === 'fulfilled') { + expect(results[0]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + expectReason(results[1], ErrorEncryptedFSError, errno.EEXIST); + } else { + expectReason(results[0], ErrorEncryptedFSError, errno.EEXIST); + expect(results[1]).toStrictEqual({ + status: 'fulfilled', + value: undefined, + }); + } + }); }); }); diff --git a/tests/EncryptedFS.dirs.test.ts b/tests/EncryptedFS.dirs.test.ts index d9e300f2..92df5b4f 100644 --- a/tests/EncryptedFS.dirs.test.ts +++ b/tests/EncryptedFS.dirs.test.ts @@ -5,12 +5,14 @@ import pathNode from 'path'; import path from 'path'; import Logger, { StreamHandler, LogLevel } from '@matrixai/logger'; import { code as errno } from 'errno'; +import EncryptedFS from '@/EncryptedFS'; +import { ErrorEncryptedFSError } from '@/errors'; import * as utils from '@/utils'; -import { EncryptedFS, constants } from '@'; +import * as constants from '@/constants'; import { expectError, createFile, setId, sleep } from './utils'; -describe('EncryptedFS Directories', () => { - const logger = new Logger('EncryptedFS Directories', LogLevel.WARN, [ +describe(`${EncryptedFS.name} Directories`, () => { + const logger = new Logger(`${EncryptedFS.name} Directories`, LogLevel.WARN, [ new StreamHandler(), ]); let dataDir: string; @@ -39,13 +41,14 @@ describe('EncryptedFS Directories', () => { }); }); afterEach(async () => { + await efs.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, }); }); - test('Directory stat makes sense', async () => { - await efs.mkdir(`dir`); + test('directory stat makes sense', async () => { + await efs.mkdir('dir'); const stat = await efs.stat(`dir`); expect(stat.isFile()).toStrictEqual(false); expect(stat.isDirectory()).toStrictEqual(true); @@ -53,7 +56,7 @@ describe('EncryptedFS Directories', () => { expect(stat.isSymbolicLink()).toStrictEqual(false); expect(stat.isFIFO()).toStrictEqual(false); }); - test('Empty root directory at startup', async () => { + test('empty root directory at startup', async () => { await expect(efs.readdir('/')).resolves.toEqual([]); const stat = await efs.stat('/'); expect(stat.isFile()).toStrictEqual(false); @@ -85,11 +88,31 @@ describe('EncryptedFS Directories', () => { constants.O_RDONLY | constants.O_DIRECTORY, ); const buffer = Buffer.alloc(10); - await expectError(efs.ftruncate(dirfd), errno.EINVAL); - await expectError(efs.read(dirfd, buffer, 0, 10), errno.EISDIR); - await expectError(efs.write(dirfd, buffer), errno.EBADF); - await expectError(efs.readFile(dirfd), errno.EISDIR); - await expectError(efs.writeFile(dirfd, `test`), errno.EBADF); + await expectError( + efs.ftruncate(dirfd), + ErrorEncryptedFSError, + errno.EINVAL, + ); + await expectError( + efs.read(dirfd, buffer, 0, 10), + ErrorEncryptedFSError, + errno.EISDIR, + ); + await expectError( + efs.write(dirfd, buffer), + ErrorEncryptedFSError, + errno.EBADF, + ); + await expectError( + efs.readFile(dirfd), + ErrorEncryptedFSError, + errno.EISDIR, + ); + await expectError( + efs.writeFile(dirfd, `test`), + ErrorEncryptedFSError, + errno.EBADF, + ); await efs.close(dirfd); }); test('inode nlink becomes 0 after deletion of the directory', async () => { @@ -113,44 +136,72 @@ describe('EncryptedFS Directories', () => { await efs.rmdir('first/sub2'); await efs.rmdir('first'); await expect(efs.exists('first')).resolves.toBeFalsy(); - await expectError(efs.access('first'), errno.ENOENT); - await expectError(efs.readdir('first'), errno.ENOENT); + await expectError( + efs.access('first'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.readdir('first'), + ErrorEncryptedFSError, + errno.ENOENT, + ); const rootlist = await efs.readdir('.'); expect(rootlist).toEqual(['backslash\\dir']); }); test('cannot delete current directory using .', async () => { await efs.mkdir('/removed'); await efs.chdir('/removed'); - await expectError(efs.rmdir('.'), errno.EINVAL); + await expectError(efs.rmdir('.'), ErrorEncryptedFSError, errno.EINVAL); }); test('cannot delete parent directory using .. even when current directory is deleted', async () => { await efs.mkdir('/removeda/removedb', { recursive: true }); await efs.chdir('/removeda/removedb'); await efs.rmdir('../removedb'); await efs.rmdir('../../removeda'); - await expectError(efs.rmdir('..'), errno.EINVAL); + await expectError(efs.rmdir('..'), ErrorEncryptedFSError, errno.EINVAL); }); test('cannot create inodes within a deleted current directory', async () => { await efs.writeFile('/dummy', 'hello'); await efs.mkdir('/removed'); await efs.chdir('/removed'); await efs.rmdir('../removed'); - await expectError(efs.writeFile('./a', 'abc'), errno.ENOENT); - await expectError(efs.mkdir('./b'), errno.ENOENT); - await expectError(efs.symlink('../dummy', 'c'), errno.ENOENT); - await expectError(efs.link('../dummy', 'd'), errno.ENOENT); + await expectError( + efs.writeFile('./a', 'abc'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError(efs.mkdir('./b'), ErrorEncryptedFSError, errno.ENOENT); + await expectError( + efs.symlink('../dummy', 'c'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.link('../dummy', 'd'), + ErrorEncryptedFSError, + errno.ENOENT, + ); }); test('returns ENOENT if the named directory does not exist (04)', async () => { await efs.mkdir(n0, 0o0755); await efs.rmdir(n0); - await expectError(efs.rmdir(n0), errno.ENOENT); - await expectError(efs.rmdir(n1), errno.ENOENT); + await expectError(efs.rmdir(n0), ErrorEncryptedFSError, errno.ENOENT); + await expectError(efs.rmdir(n1), ErrorEncryptedFSError, errno.ENOENT); }); test('returns ELOOP if too many symbolic links were encountered in translating the pathname', async () => { await efs.symlink(n0, n1); await efs.symlink(n1, n0); - await expectError(efs.rmdir(path.join(n0, 'test')), errno.ELOOP); - await expectError(efs.rmdir(path.join(n1, 'test')), errno.ELOOP); + await expectError( + efs.rmdir(path.join(n0, 'test')), + ErrorEncryptedFSError, + errno.ELOOP, + ); + await expectError( + efs.rmdir(path.join(n1, 'test')), + ErrorEncryptedFSError, + errno.ELOOP, + ); await efs.unlink(n0); await efs.unlink(n1); }); @@ -159,7 +210,11 @@ describe('EncryptedFS Directories', () => { async (type) => { await efs.mkdir(n0, 0o0755); await createFile(efs, type as FileTypes, path.join(n0, n1)); - await expectError(efs.rmdir(n0), errno.ENOTEMPTY); + await expectError( + efs.rmdir(n0), + ErrorEncryptedFSError, + errno.ENOTEMPTY, + ); }, ); test('returns EACCES when search permission is denied for a component of the path prefix', async () => { @@ -171,7 +226,11 @@ describe('EncryptedFS Directories', () => { await efs.chmod(path.join(n0, n1), 0o0644); efs.gid = 0o65534; efs.uid = 0o65534; - await expectError(efs.rmdir(path.join(n0, n1, n2)), errno.EACCES); + await expectError( + efs.rmdir(path.join(n0, n1, n2)), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test('returns EACCES when write permission is denied on the directory containing the link to be removed', async () => { await efs.mkdir(n0, { mode: 0o0755 }); @@ -182,7 +241,11 @@ describe('EncryptedFS Directories', () => { await efs.chmod(path.join(n0, n1), 0o0555); efs.gid = 0o65543; efs.uid = 0o65543; - await expectError(efs.rmdir(path.join(n0, n1, n2)), errno.EACCES); + await expectError( + efs.rmdir(path.join(n0, n1, n2)), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test.each(supportedTypes)( 'recursively deletes the directory if it contains %s', @@ -207,6 +270,126 @@ describe('EncryptedFS Directories', () => { await efs.rmdir(n0, { recursive: true }); await expect(efs.readdir('.')).resolves.toEqual([]); }); + // Recursive deleting + const MODE_RX = 0o555; // R-xr-xr-x + const MODE_RW = 0o655; // Rw-r-xr-x + test('to fail recursive delete when the parent directory lacks the write permission', async () => { + await efs.chown('.', 1000, 1000); + efs.uid = 1000; + efs.gid = 1000; + await efs.mkdir(n0, { mode: 0o0755 }); + await efs.mkdir(path.join(n0, n1), { mode: 0o0755 }); + await efs.mkdir(path.join(n0, n1, n2), { mode: 0o0755 }); + await efs.writeFile(path.join(n0, n2), 'test'); + await efs.writeFile(path.join(n0, n0), 'test'); + await efs.writeFile(path.join(n0, n1, n1), 'test'); + await efs.writeFile(path.join(n0, n1, n0), 'test'); + await efs.writeFile(path.join(n0, n1, n2, n0), 'test'); + await efs.writeFile(path.join(n0, n1, n2, n1), 'test'); + await efs.writeFile(path.join(n0, n1, n2, n2), 'test'); + // Removing write permissions from directory n0 + await efs.chmod(n0, MODE_RX); + // Expectation is that direct n0, n0/n1 has no permissions + await expectError( + efs.rmdir(n0, { recursive: true }), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.rmdir(path.join(n0, n1), { recursive: true }), + ErrorEncryptedFSError, + errno.EACCES, + ); + // But n0/n1/n2 and deeper still has permission to be removed + await expect( + efs.rmdir(path.join(n0, n1, n2), { recursive: true }), + ).resolves.toBeUndefined(); + }); + test('to fail recursive delete when the parent directory lacks the search permission', async () => { + await efs.chown('.', 1000, 1000); + efs.uid = 1000; + efs.gid = 1000; + await efs.mkdir(n0, { mode: 0o0755 }); + await efs.mkdir(path.join(n0, n1), { mode: 0o0755 }); + await efs.mkdir(path.join(n0, n1, n2), { mode: 0o0755 }); + await efs.writeFile(path.join(n0, n2), 'test'); + await efs.writeFile(path.join(n0, n0), 'test'); + await efs.writeFile(path.join(n0, n1, n1), 'test'); + await efs.writeFile(path.join(n0, n1, n0), 'test'); + await efs.writeFile(path.join(n0, n1, n2, n0), 'test'); + await efs.writeFile(path.join(n0, n1, n2, n1), 'test'); + await efs.writeFile(path.join(n0, n1, n2, n2), 'test'); + // Removing search permissions from directory n0 + await efs.chmod(n0, MODE_RW); + // Expectation is that direct n0, n0/n1 and n0/n1/n2 has no permissions + await expectError( + efs.rmdir(n0, { recursive: true }), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.rmdir(path.join(n0, n1), { recursive: true }), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.rmdir(path.join(n0, n1, n2), { recursive: true }), + ErrorEncryptedFSError, + errno.EACCES, + ); + }); + test('to recursively delete when the target directory lacks write permission and is empty', async () => { + await efs.chown('.', 1000, 1000); + efs.uid = 1000; + efs.gid = 1000; + await efs.mkdir(n0, { mode: 0o0755 }); + // Removing write permissions from directory n0 + await efs.chmod(n0, MODE_RX); + // Expectation is that direct n0 can be removed when empty + await expect(efs.rmdir(n0, { recursive: true })).resolves.toBeUndefined(); + }); + test('to fail recursive delete when the target directory lacks write permission and is not empty', async () => { + await efs.chown('.', 1000, 1000); + efs.uid = 1000; + efs.gid = 1000; + await efs.mkdir(n0, { mode: 0o0755 }); + await efs.writeFile(path.join(n0, n2), 'test'); + await efs.writeFile(path.join(n0, n0), 'test'); + // Removing write permissions from directory n0 + await efs.chmod(n0, MODE_RX); + // Expectation is that directory n0 lacks permissions + await expectError( + efs.rmdir(n0, { recursive: true }), + ErrorEncryptedFSError, + errno.EACCES, + ); + }); + test('to recursively delete when the target directory lacks search permissions and is empty', async () => { + await efs.chown('.', 1000, 1000); + efs.uid = 1000; + efs.gid = 1000; + await efs.mkdir(n0, { mode: 0o0755 }); + // Removing search permissions from directory n0 + await efs.chmod(n0, MODE_RW); + // Expectation is that direct n0 can be removed when empty + await expect(efs.rmdir(n0, { recursive: true })).resolves.toBeUndefined(); + }); + test('to fail recursive delete when the target directory lacks search permissions and is not empty', async () => { + await efs.chown('.', 1000, 1000); + efs.uid = 1000; + efs.gid = 1000; + await efs.mkdir(n0, { mode: 0o0755 }); + await efs.writeFile(path.join(n0, n2), 'test'); + await efs.writeFile(path.join(n0, n0), 'test'); + // Removing write permissions from directory n0 + await efs.chmod(n0, MODE_RW); + // Expectation is that direct n0, n0/n1 has no permissions + await expectError( + efs.rmdir(n0, { recursive: true }), + ErrorEncryptedFSError, + errno.EACCES, + ); + }); }); describe('mkdir & mkdtemp', () => { test('is able to make directories', async () => { @@ -238,7 +421,7 @@ describe('EncryptedFS Directories', () => { ).resolves.toEqual(buffer.toString()); }); test('should not make the root directory', async () => { - await expectError(efs.mkdir('/'), errno.EEXIST); + await expectError(efs.mkdir('/'), ErrorEncryptedFSError, errno.EEXIST); }); test("trailing '/.' should not result in any errors", async () => { await expect( @@ -258,7 +441,11 @@ describe('EncryptedFS Directories', () => { await efs.mkdir(path.join(n1, n2), { mode: dp }); await efs.rmdir(path.join(n1, n2)); await efs.chmod(n1, 0o0555); - await expectError(efs.mkdir(path.join(n1, n2)), errno.EACCES); + await expectError( + efs.mkdir(path.join(n1, n2)), + ErrorEncryptedFSError, + errno.EACCES, + ); await efs.chmod(n1, dp); await efs.mkdir(path.join(n1, n2), { mode: dp }); }); @@ -267,7 +454,11 @@ describe('EncryptedFS Directories', () => { async (type) => { await efs.mkdir('test'); await createFile(efs, type, n0); - await expectError(efs.mkdir(n0, { mode: dp }), errno.EEXIST); + await expectError( + efs.mkdir(n0, { mode: dp }), + ErrorEncryptedFSError, + errno.EEXIST, + ); }, ); }); @@ -281,17 +472,33 @@ describe('EncryptedFS Directories', () => { test('cannot rename the current or parent directory to a subdirectory', async () => { await efs.mkdir('/cwd'); await efs.chdir('/cwd'); - await expectError(efs.rename('.', 'subdir'), errno.EBUSY); + await expectError( + efs.rename('.', 'subdir'), + ErrorEncryptedFSError, + errno.EBUSY, + ); await efs.mkdir('/cwd/cwd'); await efs.chdir('/cwd/cwd'); - await expectError(efs.rename('..', 'subdir'), errno.EBUSY); + await expectError( + efs.rename('..', 'subdir'), + ErrorEncryptedFSError, + errno.EBUSY, + ); }); test('cannot rename where the old path is a strict prefix of the new path', async () => { await efs.mkdir('/cwd1/cwd2', { recursive: true }); await efs.chdir('/cwd1/cwd2'); - await expectError(efs.rename('../cwd2', 'subdir'), errno.EINVAL); + await expectError( + efs.rename('../cwd2', 'subdir'), + ErrorEncryptedFSError, + errno.EINVAL, + ); await efs.mkdir('/cwd1/cwd2/cwd3'); - await expectError(efs.rename('./cwd3', './cwd3/cwd4'), errno.EINVAL); + await expectError( + efs.rename('./cwd3', './cwd3/cwd4'), + ErrorEncryptedFSError, + errno.EINVAL, + ); }); types = ['regular', 'block']; test.each(types)( @@ -300,7 +507,7 @@ describe('EncryptedFS Directories', () => { await createFile(efs, type as FileTypes, n0, 0o0644); const inode = (await efs.lstat(n0)).ino; await efs.rename(n0, n1); - await expectError(efs.lstat(n0), errno.ENOENT); + await expectError(efs.lstat(n0), ErrorEncryptedFSError, errno.ENOENT); let stat = await efs.lstat(n1); expect(stat.ino).toEqual(inode); // Expect(stat.mode).toEqual(0o0644); @@ -319,7 +526,7 @@ describe('EncryptedFS Directories', () => { expect(stat.ino).toEqual(inode); // Expect(stat.mode).toEqual(0o0644); expect(stat.nlink).toEqual(2); - await expectError(efs.lstat(n1), errno.ENOENT); + await expectError(efs.lstat(n1), ErrorEncryptedFSError, errno.ENOENT); stat = await efs.lstat(n2); expect(stat.ino).toEqual(inode); // Expect(stat.mode).toEqual(0o0644); @@ -331,7 +538,7 @@ describe('EncryptedFS Directories', () => { // Expect dir,0755 lstat ${n0} type,mode const inode = (await efs.lstat(n0)).ino; await efs.rename(n0, n1); - await expectError(efs.lstat(n0), errno.ENOENT); + await expectError(efs.lstat(n0), ErrorEncryptedFSError, errno.ENOENT); const stat = await efs.lstat(n1); expect(stat.ino).toEqual(inode); // Expect(stat.mode).toEqual(0o0755); @@ -349,7 +556,7 @@ describe('EncryptedFS Directories', () => { await efs.rename(n1, n2); stat = await efs.lstat(n0); expect(stat.ino).toEqual(rinode); - await expectError(efs.lstat(n1), errno.ENOENT); + await expectError(efs.lstat(n1), ErrorEncryptedFSError, errno.ENOENT); stat = await efs.lstat(n2); expect(stat.ino).toEqual(sinode); }); @@ -360,7 +567,11 @@ describe('EncryptedFS Directories', () => { const ctime1 = (await efs.lstat(n0)).ctime; await sleep(10); setId(efs, tuid); - await expectError(efs.rename(n0, n1), errno.EACCES); + await expectError( + efs.rename(n0, n1), + ErrorEncryptedFSError, + errno.EACCES, + ); const ctime2 = (await efs.lstat(n0)).ctime; expect(ctime1).toEqual(ctime2); }, @@ -369,11 +580,13 @@ describe('EncryptedFS Directories', () => { await efs.mkdir(n0, { mode: dp }); await expectError( efs.rename(path.join(n0, n1, 'test'), n2), + ErrorEncryptedFSError, errno.ENOENT, ); await createFile(efs, 'regular', n2); await expectError( efs.rename(n2, path.join(n0, n1, 'test')), + ErrorEncryptedFSError, errno.ENOENT, ); }); @@ -391,10 +604,12 @@ describe('EncryptedFS Directories', () => { await efs.chmod(n1, 0o0644); await expectError( efs.rename(path.join(n1, n3), path.join(n1, n4)), + ErrorEncryptedFSError, errno.EACCES, ); await expectError( efs.rename(path.join(n1, n3), path.join(n2, n4)), + ErrorEncryptedFSError, errno.EACCES, ); @@ -402,6 +617,7 @@ describe('EncryptedFS Directories', () => { await efs.chmod(n2, 0o0644); await expectError( efs.rename(path.join(n1, n3), path.join(n2, n4)), + ErrorEncryptedFSError, errno.EACCES, ); }); @@ -419,22 +635,40 @@ describe('EncryptedFS Directories', () => { await efs.chmod(n2, 0o0555); await expectError( efs.rename(path.join(n1, n3), path.join(n2, n4)), + ErrorEncryptedFSError, errno.EACCES, ); await efs.chmod(n1, 0o0555); await expectError( efs.rename(path.join(n1, n3), path.join(n1, n4)), + ErrorEncryptedFSError, errno.EACCES, ); }); test('returns ELOOP if too many symbolic links were encountered in translating one of the pathnames', async () => { await efs.symlink(n0, n1); await efs.symlink(n1, n0); - await expectError(efs.rename(path.join(n0, 'test'), n2), errno.ELOOP); - await expectError(efs.rename(path.join(n0, 'test'), n1), errno.ELOOP); + await expectError( + efs.rename(path.join(n0, 'test'), n2), + ErrorEncryptedFSError, + errno.ELOOP, + ); + await expectError( + efs.rename(path.join(n0, 'test'), n1), + ErrorEncryptedFSError, + errno.ELOOP, + ); await createFile(efs, 'regular', n2); - await expectError(efs.rename(n2, path.join(n0, 'test')), errno.ELOOP); - await expectError(efs.rename(n2, path.join(n1, 'test')), errno.ELOOP); + await expectError( + efs.rename(n2, path.join(n0, 'test')), + ErrorEncryptedFSError, + errno.ELOOP, + ); + await expectError( + efs.rename(n2, path.join(n1, 'test')), + ErrorEncryptedFSError, + errno.ELOOP, + ); }); types = ['regular', 'block']; test.each(types)( @@ -444,11 +678,13 @@ describe('EncryptedFS Directories', () => { await createFile(efs, type as FileTypes, path.join(n0, n1)); await expectError( efs.rename(path.join(n0, n1, 'test'), path.join(n0, n2)), + ErrorEncryptedFSError, errno.ENOTDIR, ); await createFile(efs, type as FileTypes, path.join(n0, n2)); await expectError( efs.rename(path.join(n0, n2), path.join(n0, n1, 'test')), + ErrorEncryptedFSError, errno.ENOTDIR, ); }, @@ -459,7 +695,11 @@ describe('EncryptedFS Directories', () => { async (type) => { await efs.mkdir(n0, { mode: dp }); await createFile(efs, type as FileTypes, n1); - await expectError(efs.rename(n0, n1), errno.ENOTDIR); + await expectError( + efs.rename(n0, n1), + ErrorEncryptedFSError, + errno.ENOTDIR, + ); }, ); test.each(types)( @@ -467,15 +707,27 @@ describe('EncryptedFS Directories', () => { async (type) => { await efs.mkdir(n0, { mode: dp }); await createFile(efs, type as FileTypes, n1); - await expectError(efs.rename(n1, n0), errno.EISDIR); + await expectError( + efs.rename(n1, n0), + ErrorEncryptedFSError, + errno.EISDIR, + ); }, ); test("returns EINVAL when the 'from' argument is a parent directory of 'to'", async () => { await efs.mkdir(n0, { mode: dp }); await efs.mkdir(path.join(n0, n1), { mode: dp }); - await expectError(efs.rename(n0, path.join(n0, n1)), errno.EINVAL); - await expectError(efs.rename(n0, path.join(n0, n1, n2)), errno.EINVAL); + await expectError( + efs.rename(n0, path.join(n0, n1)), + ErrorEncryptedFSError, + errno.EINVAL, + ); + await expectError( + efs.rename(n0, path.join(n0, n1, n2)), + ErrorEncryptedFSError, + errno.EINVAL, + ); }); test.each(supportedTypes)( "returns ENOTEMPTY if the 'to' argument is a directory and contains %s", @@ -483,7 +735,11 @@ describe('EncryptedFS Directories', () => { await efs.mkdir(n0, { mode: dp }); await efs.mkdir(n1, { mode: dp }); await createFile(efs, type, path.join(n1, n2)); - await expectError(efs.rename(n0, n1), errno.ENOTEMPTY); + await expectError( + efs.rename(n0, n1), + ErrorEncryptedFSError, + errno.ENOTEMPTY, + ); }, ); test.each(supportedTypes)('changes file ctime for %s', async (type) => { diff --git a/tests/EncryptedFS.files.test.ts b/tests/EncryptedFS.files.test.ts index f2b56ef0..2a4da126 100644 --- a/tests/EncryptedFS.files.test.ts +++ b/tests/EncryptedFS.files.test.ts @@ -5,37 +5,37 @@ import pathNode from 'path'; import path from 'path'; import Logger, { StreamHandler, LogLevel } from '@matrixai/logger'; import { code as errno } from 'errno'; +import EncryptedFS from '@/EncryptedFS'; +import { ErrorEncryptedFSError } from '@/errors'; import * as utils from '@/utils'; -import { EncryptedFS, constants } from '@'; +import * as constants from '@/constants'; import { createFile, expectError, setId, sleep, supportedTypes } from './utils'; -describe('EncryptedFS Files', () => { - const logger = new Logger('EncryptedFS Files', LogLevel.WARN, [ +describe(`${EncryptedFS.name} Files`, () => { + const logger = new Logger(`${EncryptedFS.name} Files`, LogLevel.WARN, [ new StreamHandler(), ]); - let dataDir: string; - let dbPath: string; const dbKey: Buffer = utils.generateKeySync(256); + let dataDir: string; let efs: EncryptedFS; const n0 = 'zero'; const n1 = 'one'; const n2 = 'two'; const dp = 0o0755; const tuid = 0o65534; - const flags = constants; beforeEach(async () => { dataDir = await fs.promises.mkdtemp( pathNode.join(os.tmpdir(), 'encryptedfs-test-'), ); - dbPath = `${dataDir}/db`; efs = await EncryptedFS.createEncryptedFS({ + dbPath: dataDir, dbKey, - dbPath, umask: 0o022, logger, }); }); afterEach(async () => { + await efs.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, @@ -86,7 +86,7 @@ describe('EncryptedFS Files', () => { test('can seek different parts of a file', async () => { const fd = await efs.open('/fdtest', 'w+'); await efs.write(fd, 'abc'); - const pos = await efs.lseek(fd, -1, flags.SEEK_CUR); + const pos = await efs.lseek(fd, -1, constants.SEEK_CUR); expect(pos).toBe(2); await efs.write(fd, 'd'); await efs.close(fd); @@ -96,13 +96,26 @@ describe('EncryptedFS Files', () => { test('can seek beyond the file length and create a zeroed "sparse" file', async () => { await efs.writeFile('/fdtest', Buffer.from([0x61, 0x62, 0x63])); const fd = await efs.open('/fdtest', 'r+'); - const pos = await efs.lseek(fd, 1, flags.SEEK_END); + const pos = await efs.lseek(fd, 1, constants.SEEK_END); expect(pos).toBe(4); await efs.write(fd, Buffer.from([0x64])); await efs.close(fd); const buf = await efs.readFile('/fdtest'); expect(buf).toEqual(Buffer.from([0x61, 0x62, 0x63, 0x00, 0x64])); }); + test('can seek beyond the file length and create a zeroed "sparse" file with an empty file', async () => { + const path1 = '/fdtest'; + const data = 'test'; + const jump = 10; + const fd = await efs.open(path1, 'wx'); + await efs.lseek(fd, jump, constants.SEEK_CUR); + await efs.write(fd, data); + await efs.close(fd); + const contents = (await efs.readFile(path1)).toString(); + const stat = await efs.stat(path1); + expect(stat.size).toEqual(jump + data.length); + expect(contents).toEqual('\0'.repeat(jump) + data); + }); }); describe('fallocate', () => { test('can extend the file length', async () => { @@ -185,6 +198,21 @@ describe('EncryptedFS Files', () => { expect(buf).toEqual(Buffer.from('dbc')); await efs.close(fd); }); + test('truncates the file contents', async () => { + const path1 = path.join('dir', 'path1'); + await efs.mkdir('dir'); + await efs.writeFile(path1, 'abcdef'); + expect(await efs.readFile(path1, { encoding: 'utf-8' })).toEqual( + 'abcdef', + ); + const fd = await efs.open(path1, 'r+'); + expect(await efs.readFile(path1, { encoding: 'utf-8' })).toEqual( + 'abcdef', + ); + await efs.ftruncate(fd, 4); + await efs.close(fd); + expect(await efs.readFile(path1, { encoding: 'utf-8' })).toEqual('abcd'); + }); }); describe('read', () => { test('can be called using different styles', async () => { @@ -383,10 +411,18 @@ describe('EncryptedFS Files', () => { // Become the other efs.uid = 1000; efs.gid = 1000; - await efs.access('/test1', flags.R_OK); - await expectError(efs.access('/test1', flags.W_OK), errno.EACCES); - await efs.access('/test2', flags.R_OK); - await expectError(efs.access('/test1', flags.W_OK), errno.EACCES); + await efs.access('/test1', constants.R_OK); + await expectError( + efs.access('/test1', constants.W_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); + await efs.access('/test2', constants.R_OK); + await expectError( + efs.access('/test1', constants.W_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test('file writes from the beginning, and does not move the fd position', async () => { const fd = await efs.open('/fdtest', 'w+'); @@ -434,6 +470,11 @@ describe('EncryptedFS Files', () => { await efs.writeFile(`dir/hello-world`, buffer); await efs.copyFile('dir/hello-world', 'hello-universe'); await expect(efs.readFile('hello-universe')).resolves.toEqual(buffer); + const statSrc = await efs.stat('dir/hello-world'); + const statDst = await efs.stat('hello-universe'); + expect(statDst.size).toEqual(statSrc.size); + expect(statDst.blocks).toEqual(statSrc.blocks); + expect(statDst.blksize).toEqual(statSrc.blksize); }); test('using append moves with the fd position', async () => { const fd = await efs.open('/fdtest', 'w+'); @@ -448,13 +489,14 @@ describe('EncryptedFS Files', () => { }); describe('open', () => { test("opens a file if O_CREAT is specified and the file doesn't exist", async () => { - const modeCheck = flags.S_IRWXU | flags.S_IRWXG | flags.S_IRWXO; + const modeCheck = + constants.S_IRWXU | constants.S_IRWXG | constants.S_IRWXO; let fd; - fd = await efs.open(n0, flags.O_CREAT | flags.O_WRONLY, dp); + fd = await efs.open(n0, constants.O_CREAT | constants.O_WRONLY, dp); expect((await efs.lstat(n0)).mode & modeCheck).toEqual(dp & ~0o022); await efs.unlink(n0); await efs.close(fd); - fd = await efs.open(n0, flags.O_CREAT | flags.O_WRONLY, 0o0151); + fd = await efs.open(n0, constants.O_CREAT | constants.O_WRONLY, 0o0151); expect((await efs.lstat(n0)).mode & modeCheck).toEqual(0o0151 & ~0o022); await efs.unlink(n0); await efs.close(fd); @@ -484,7 +526,11 @@ describe('EncryptedFS Files', () => { const dmtime = (await efs.stat(n1)).mtime.getTime(); const dctime = (await efs.stat(n1)).ctime.getTime(); await sleep(10); - const fd = await efs.open(PUT, flags.O_CREAT | flags.O_RDONLY, 0o0644); + const fd = await efs.open( + PUT, + constants.O_CREAT | constants.O_RDONLY, + 0o0644, + ); const mtime = (await efs.stat(n1)).mtime.getTime(); expect(dmtime).toEqual(mtime); const ctime = (await efs.stat(n1)).ctime.getTime(); @@ -497,18 +543,28 @@ describe('EncryptedFS Files', () => { async (type) => { const PUT = path.join(n1, 'test'); await createFile(efs, type as FileTypes, n1); - await expectError(efs.open(PUT, 'r'), errno.ENOTDIR); - await expectError(efs.open(PUT, 'w', 0o0644), errno.ENOTDIR); + await expectError( + efs.open(PUT, 'r'), + ErrorEncryptedFSError, + errno.ENOTDIR, + ); + await expectError( + efs.open(PUT, 'w', 0o0644), + ErrorEncryptedFSError, + errno.ENOTDIR, + ); }, ); test('returns ENOENT if a component of the path name that must exist does not exist or O_CREAT is not set and the named file does not exist', async () => { await efs.mkdir(n0, { mode: dp }); await expectError( - efs.open(path.join(n0, n1, 'test'), flags.O_CREAT, 0o0644), + efs.open(path.join(n0, n1, 'test'), constants.O_CREAT, 0o0644), + ErrorEncryptedFSError, errno.ENOENT, ); await expectError( - efs.open(path.join(n0, n1, 'test'), flags.O_RDONLY), + efs.open(path.join(n0, n1, 'test'), constants.O_RDONLY), + ErrorEncryptedFSError, errno.ENOENT, ); }); @@ -517,15 +573,16 @@ describe('EncryptedFS Files', () => { await efs.chown(n1, tuid, tuid); setId(efs, tuid); await createFile(efs, 'regular', path.join(n1, n2)); - let fd = await efs.open(path.join(n1, n2), flags.O_RDONLY); + let fd = await efs.open(path.join(n1, n2), constants.O_RDONLY); await efs.close(fd); await efs.chmod(n1, 0o0644); await expectError( - efs.open(path.join(n1, n2), flags.O_RDONLY), + efs.open(path.join(n1, n2), constants.O_RDONLY), + ErrorEncryptedFSError, errno.EACCES, ); await efs.chmod(n1, 0o0755); - fd = await efs.open(path.join(n1, n2), flags.O_RDONLY); + fd = await efs.open(path.join(n1, n2), constants.O_RDONLY); await efs.close(fd); }); test('returns EACCES when the required permissions are denied for a regular file', async () => { @@ -547,11 +604,11 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - fd = await efs.open(n1, flags.O_RDONLY); + fd = await efs.open(n1, constants.O_RDONLY); await efs.close(fd); - fd = await efs.open(n1, flags.O_WRONLY); + fd = await efs.open(n1, constants.O_WRONLY); await efs.close(fd); - fd = await efs.open(n1, flags.O_RDWR); + fd = await efs.open(n1, constants.O_RDWR); await efs.close(fd); setId(efs, tuid); } @@ -566,10 +623,18 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - fd = await efs.open(n1, flags.O_RDONLY); + fd = await efs.open(n1, constants.O_RDONLY); await efs.close(fd); - await expectError(efs.open(n1, flags.O_WRONLY), errno.EACCES); - await expectError(efs.open(n1, flags.O_RDWR), errno.EACCES); + await expectError( + efs.open(n1, constants.O_WRONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.open(n1, constants.O_RDWR), + ErrorEncryptedFSError, + errno.EACCES, + ); setId(efs, tuid); } @@ -584,10 +649,18 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - await expectError(efs.open(n1, flags.O_RDONLY), errno.EACCES); - fd = await efs.open(n1, flags.O_WRONLY); + await expectError( + efs.open(n1, constants.O_RDONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); + fd = await efs.open(n1, constants.O_WRONLY); await efs.close(fd); - await expectError(efs.open(n1, flags.O_RDWR), errno.EACCES); + await expectError( + efs.open(n1, constants.O_RDWR), + ErrorEncryptedFSError, + errno.EACCES, + ); setId(efs, tuid); } @@ -602,9 +675,21 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - await expectError(efs.open(n1, flags.O_RDONLY), errno.EACCES); - await expectError(efs.open(n1, flags.O_WRONLY), errno.EACCES); - await expectError(efs.open(n1, flags.O_RDWR), errno.EACCES); + await expectError( + efs.open(n1, constants.O_RDONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.open(n1, constants.O_WRONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.open(n1, constants.O_RDWR), + ErrorEncryptedFSError, + errno.EACCES, + ); setId(efs, tuid); } @@ -619,9 +704,21 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - await expectError(efs.open(n1, flags.O_RDONLY), errno.EACCES); - await expectError(efs.open(n1, flags.O_WRONLY), errno.EACCES); - await expectError(efs.open(n1, flags.O_RDWR), errno.EACCES); + await expectError( + efs.open(n1, constants.O_RDONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.open(n1, constants.O_WRONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.open(n1, constants.O_RDWR), + ErrorEncryptedFSError, + errno.EACCES, + ); setId(efs, tuid); } }); @@ -644,7 +741,7 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - fd = await efs.open(n1, flags.O_RDONLY); + fd = await efs.open(n1, constants.O_RDONLY); await efs.close(fd); setId(efs, tuid); } @@ -660,7 +757,7 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - fd = await efs.open(n1, flags.O_RDONLY); + fd = await efs.open(n1, constants.O_RDONLY); await efs.close(fd); setId(efs, tuid); } @@ -676,7 +773,11 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - await expectError(efs.open(n1, flags.O_RDONLY), errno.EACCES); + await expectError( + efs.open(n1, constants.O_RDONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); setId(efs, tuid); } @@ -691,7 +792,11 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - await expectError(efs.open(n1, flags.O_RDONLY), errno.EACCES); + await expectError( + efs.open(n1, constants.O_RDONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); setId(efs, tuid); } @@ -706,7 +811,11 @@ describe('EncryptedFS Files', () => { } else { await efs.chmod(n1, mode); } - await expectError(efs.open(n1, flags.O_RDONLY), errno.EACCES); + await expectError( + efs.open(n1, constants.O_RDONLY), + ErrorEncryptedFSError, + errno.EACCES, + ); setId(efs, tuid); } }); @@ -721,7 +830,7 @@ describe('EncryptedFS Files', () => { 0o0477, 0o0177, 0o0077, 0o0747, 0o0717, 0o0707, 0o0774, 0o0771, 0o0770, ]; let counter = 0; - const flag = flags.O_RDONLY | flags.O_TRUNC; + const flag = constants.O_RDONLY | constants.O_TRUNC; for (const mode of modes) { if (counter < 3) { await efs.chmod(n1, mode); @@ -732,7 +841,11 @@ describe('EncryptedFS Files', () => { await efs.chmod(n1, mode); setId(efs, 1000, 1000); } - await expectError(efs.open(n1, flag), errno.EACCES); + await expectError( + efs.open(n1, flag), + ErrorEncryptedFSError, + errno.EACCES, + ); counter++; setId(efs, tuid); } @@ -741,43 +854,69 @@ describe('EncryptedFS Files', () => { await efs.symlink(n0, n1); await efs.symlink(n1, n0); await expectError( - efs.open(path.join(n0, 'test'), flags.O_RDONLY), + efs.open(path.join(n0, 'test'), constants.O_RDONLY), + ErrorEncryptedFSError, errno.ELOOP, ); await expectError( - efs.open(path.join(n1, 'test'), flags.O_RDONLY), + efs.open(path.join(n1, 'test'), constants.O_RDONLY), + ErrorEncryptedFSError, errno.ELOOP, ); }); test('returns EISDIR when trying to open a directory for writing', async () => { await efs.mkdir(n0, { mode: dp }); - const fd = await efs.open(n0, flags.O_RDONLY); + const fd = await efs.open(n0, constants.O_RDONLY); await efs.close(fd); - await expectError(efs.open(n0, flags.O_WRONLY), errno.EISDIR); - await expectError(efs.open(n0, flags.O_RDWR), errno.EISDIR); await expectError( - efs.open(n0, flags.O_RDONLY | flags.O_TRUNC), + efs.open(n0, constants.O_WRONLY), + ErrorEncryptedFSError, + errno.EISDIR, + ); + await expectError( + efs.open(n0, constants.O_RDWR), + ErrorEncryptedFSError, errno.EISDIR, ); await expectError( - efs.open(n0, flags.O_WRONLY | flags.O_TRUNC), + efs.open(n0, constants.O_RDONLY | constants.O_TRUNC), + ErrorEncryptedFSError, errno.EISDIR, ); await expectError( - efs.open(n0, flags.O_RDWR | flags.O_TRUNC), + efs.open(n0, constants.O_WRONLY | constants.O_TRUNC), + ErrorEncryptedFSError, + errno.EISDIR, + ); + await expectError( + efs.open(n0, constants.O_RDWR | constants.O_TRUNC), + ErrorEncryptedFSError, errno.EISDIR, ); }); test('returns ELOOP when O_NOFOLLOW was specified and the target is a symbolic link', async () => { await efs.symlink(n0, n1); - const nf = flags.O_NOFOLLOW; + const nf = constants.O_NOFOLLOW; + await expectError( + efs.open(n1, constants.O_RDONLY | constants.O_CREAT | nf), + ErrorEncryptedFSError, + errno.ELOOP, + ); + await expectError( + efs.open(n1, constants.O_RDONLY | nf), + ErrorEncryptedFSError, + errno.ELOOP, + ); + await expectError( + efs.open(n1, constants.O_WRONLY | nf), + ErrorEncryptedFSError, + errno.ELOOP, + ); await expectError( - efs.open(n1, flags.O_RDONLY | flags.O_CREAT | nf), + efs.open(n1, constants.O_RDWR | nf), + ErrorEncryptedFSError, errno.ELOOP, ); - await expectError(efs.open(n1, flags.O_RDONLY | nf), errno.ELOOP); - await expectError(efs.open(n1, flags.O_WRONLY | nf), errno.ELOOP); - await expectError(efs.open(n1, flags.O_RDWR | nf), errno.ELOOP); }); test.each(supportedTypes)( 'returns EEXIST when O_CREAT and O_EXCL were specified and the %s exists', @@ -785,7 +924,8 @@ describe('EncryptedFS Files', () => { await efs.mkdir('test'); await createFile(efs, type, n0); await expectError( - efs.open(n0, flags.O_CREAT | flags.O_EXCL, 0o0644), + efs.open(n0, constants.O_CREAT | constants.O_EXCL, 0o0644), + ErrorEncryptedFSError, errno.EEXIST, ); }, diff --git a/tests/EncryptedFS.links.test.ts b/tests/EncryptedFS.links.test.ts index 0edde6f1..328a9f09 100644 --- a/tests/EncryptedFS.links.test.ts +++ b/tests/EncryptedFS.links.test.ts @@ -5,17 +5,17 @@ import pathNode from 'path'; import path from 'path'; import Logger, { StreamHandler, LogLevel } from '@matrixai/logger'; import { code as errno } from 'errno'; +import EncryptedFS from '@/EncryptedFS'; +import { ErrorEncryptedFSError } from '@/errors'; import * as utils from '@/utils'; -import { EncryptedFS } from '@'; import { expectError, createFile, supportedTypes, sleep, setId } from './utils'; -describe('EncryptedFS Links', () => { - const logger = new Logger('EncryptedFS Links', LogLevel.WARN, [ +describe(`${EncryptedFS.name} Links`, () => { + const logger = new Logger(`${EncryptedFS.name} Links`, LogLevel.WARN, [ new StreamHandler(), ]); - let dataDir: string; - let dbPath: string; const dbKey: Buffer = utils.generateKeySync(256); + let dataDir: string; let efs: EncryptedFS; const n0 = 'zero'; const n1 = 'one'; @@ -29,15 +29,15 @@ describe('EncryptedFS Links', () => { dataDir = await fs.promises.mkdtemp( pathNode.join(os.tmpdir(), 'encryptedfs-test-'), ); - dbPath = `${dataDir}/db`; efs = await EncryptedFS.createEncryptedFS({ + dbPath: dataDir, dbKey, - dbPath, umask: 0o022, logger, }); }); afterEach(async () => { + await efs.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, @@ -62,9 +62,9 @@ describe('EncryptedFS Links', () => { // Const stat = await efs.lstat(n0); // Expect(stat.mode).toEqual(0o0644); await efs.symlink(n0, n1); - // Should check that it is a link here. + // Should check that it is a link here await efs.unlink(n0); - await expectError(efs.stat(n1), errno.ENOENT); + await expectError(efs.stat(n1), ErrorEncryptedFSError, errno.ENOENT); await efs.unlink(n1); await efs.mkdir(n0, { mode: dp }); @@ -87,13 +87,21 @@ describe('EncryptedFS Links', () => { }); test('can resolve 1 symlink loop', async () => { await efs.symlink('/test', '/test'); - await expectError(efs.readFile('/test'), errno.ELOOP); + await expectError( + efs.readFile('/test'), + ErrorEncryptedFSError, + errno.ELOOP, + ); }); test('can resolve 2 symlink loops', async () => { await efs.mkdir('/dirtolink'); await efs.symlink('/dirtolink/test', '/test'); await efs.symlink('/test', '/dirtolink/test'); - await expectError(efs.readFile('/test/non-existent'), errno.ELOOP); + await expectError( + efs.readFile('/test/non-existent'), + ErrorEncryptedFSError, + errno.ELOOP, + ); }); test('can be expanded by realpath', async () => { await efs.writeFile('/test', Buffer.from('Hello')); @@ -106,7 +114,11 @@ describe('EncryptedFS Links', () => { test('cannot be traversed by rmdir', async () => { await efs.mkdir(`directory`); await efs.symlink(`directory`, `linktodirectory`); - await expectError(efs.rmdir(`linktodirectory`), errno.ENOTDIR); + await expectError( + efs.rmdir(`linktodirectory`), + ErrorEncryptedFSError, + errno.ENOTDIR, + ); }); test('is able to be added and traversed transitively', async () => { await efs.mkdir('/test'); @@ -144,7 +156,11 @@ describe('EncryptedFS Links', () => { await efs.chmod(n1, 0o0644); setId(efs, tuid); - await expectError(efs.symlink('test', path.join(n1, n2)), errno.EACCES); + await expectError( + efs.symlink('test', path.join(n1, n2)), + ErrorEncryptedFSError, + errno.EACCES, + ); await efs.chmod(n1, dp); await efs.symlink('test', path.join(n1, n2)); await efs.unlink(path.join(n1, n2)); @@ -159,7 +175,11 @@ describe('EncryptedFS Links', () => { await efs.chmod(n1, 0o0555); setId(efs, tuid); - await expectError(efs.symlink('test', path.join(n1, n2)), errno.EACCES); + await expectError( + efs.symlink('test', path.join(n1, n2)), + ErrorEncryptedFSError, + errno.EACCES, + ); await efs.chmod(n1, 0o0755); await efs.symlink('test', path.join(n1, n2)); await efs.unlink(path.join(n1, n2)); @@ -169,10 +189,12 @@ describe('EncryptedFS Links', () => { await efs.symlink(n1, n0); await expectError( efs.symlink('test', path.join(n0, 'test')), + ErrorEncryptedFSError, errno.ELOOP, ); await expectError( efs.symlink('test', path.join(n1, 'test')), + ErrorEncryptedFSError, errno.ELOOP, ); await efs.unlink(n0); @@ -182,7 +204,11 @@ describe('EncryptedFS Links', () => { 'returns EEXIST if the 2nd name argument already exists as a %s', async (type) => { await createFile(efs, type, n0); - await expectError(efs.symlink('test', n0), errno.EEXIST); + await expectError( + efs.symlink('test', n0), + ErrorEncryptedFSError, + errno.EEXIST, + ); }, ); }); @@ -211,7 +237,7 @@ describe('EncryptedFS Links', () => { const ctime1 = (await efs.stat(n0)).ctime.getTime(); await sleep(10); setId(efs, tuid); - await expectError(efs.unlink(n1), errno.EACCES); + await expectError(efs.unlink(n1), ErrorEncryptedFSError, errno.EACCES); const ctime2 = (await efs.stat(n0)).ctime.getTime(); expect(ctime1).toEqual(ctime2); }, @@ -229,13 +255,17 @@ describe('EncryptedFS Links', () => { test('returns ENOTDIR if a component of the path prefix is not a directory', async () => { await efs.mkdir(n0, { mode: dp }); await createFile(efs, 'regular', path.join(n0, n1)); - await expectError(efs.unlink(path.join(n0, n1, 'test')), errno.ENOTDIR); + await expectError( + efs.unlink(path.join(n0, n1, 'test')), + ErrorEncryptedFSError, + errno.ENOTDIR, + ); }); test('returns ENOENT if the named file does not exist', async () => { await createFile(efs, 'regular', n0); await efs.unlink(n0); - await expectError(efs.unlink(n0), errno.ENOENT); - await expectError(efs.unlink(n1), errno.ENOENT); + await expectError(efs.unlink(n0), ErrorEncryptedFSError, errno.ENOENT); + await expectError(efs.unlink(n1), ErrorEncryptedFSError, errno.ENOENT); }); test('returns EACCES when search permission is denied for a component of the path prefix', async () => { await efs.mkdir(n1, { mode: dp }); @@ -243,7 +273,11 @@ describe('EncryptedFS Links', () => { setId(efs, tuid); await createFile(efs, 'regular', path.join(n1, n2)); await efs.chmod(n1, 0o0644); - await expectError(efs.unlink(path.join(n1, n2)), errno.EACCES); + await expectError( + efs.unlink(path.join(n1, n2)), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test('returns EACCES when write permission is denied on the directory containing the link to be removed', async () => { await efs.mkdir(n1, { mode: dp }); @@ -251,17 +285,29 @@ describe('EncryptedFS Links', () => { setId(efs, tuid); await createFile(efs, 'regular', path.join(n1, n2)); await efs.chmod(n1, 0o0555); - await expectError(efs.unlink(path.join(n1, n2)), errno.EACCES); + await expectError( + efs.unlink(path.join(n1, n2)), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test('returns ELOOP if too many symbolic links were encountered in translating the pathname', async () => { await efs.symlink(n0, n1); await efs.symlink(n1, n0); - await expectError(efs.unlink(path.join(n0, 'test')), errno.ELOOP); - await expectError(efs.unlink(path.join(n1, 'test')), errno.ELOOP); + await expectError( + efs.unlink(path.join(n0, 'test')), + ErrorEncryptedFSError, + errno.ELOOP, + ); + await expectError( + efs.unlink(path.join(n1, 'test')), + ErrorEncryptedFSError, + errno.ELOOP, + ); }); test('returns EISDIR if the named file is a directory', async () => { await efs.mkdir(n0, { mode: dp }); - await expectError(efs.unlink(n0), errno.EISDIR); + await expectError(efs.unlink(n0), ErrorEncryptedFSError, errno.EISDIR); }); test('will not immeadiately free a file', async () => { const message = 'Hello, World!'; @@ -320,7 +366,7 @@ describe('EncryptedFS Links', () => { expect(stat.gid).toEqual(0o65533); await efs.unlink(n0); - await expectError(efs.lstat(n0), errno.ENOENT); + await expectError(efs.lstat(n0), ErrorEncryptedFSError, errno.ENOENT); stat = await efs.lstat(n1); // Expect(stat.mode).toEqual(0o0201); expect(stat.nlink).toEqual(2); @@ -333,18 +379,18 @@ describe('EncryptedFS Links', () => { expect(stat.gid).toEqual(0o65533); await efs.unlink(n2); - await expectError(efs.lstat(n0), errno.ENOENT); + await expectError(efs.lstat(n0), ErrorEncryptedFSError, errno.ENOENT); stat = await efs.lstat(n1); // Expect(stat.mode).toEqual(0o0201); expect(stat.nlink).toEqual(1); expect(stat.uid).toEqual(0o65533); expect(stat.gid).toEqual(0o65533); - await expectError(efs.lstat(n2), errno.ENOENT); + await expectError(efs.lstat(n2), ErrorEncryptedFSError, errno.ENOENT); await efs.unlink(n1); - await expectError(efs.lstat(n0), errno.ENOENT); - await expectError(efs.lstat(n1), errno.ENOENT); - await expectError(efs.lstat(n2), errno.ENOENT); + await expectError(efs.lstat(n0), ErrorEncryptedFSError, errno.ENOENT); + await expectError(efs.lstat(n1), ErrorEncryptedFSError, errno.ENOENT); + await expectError(efs.lstat(n2), ErrorEncryptedFSError, errno.ENOENT); }); test.each(types)('successful updates ctime of %s', async (type) => { await createFile(efs, type as FileTypes, n0); @@ -369,7 +415,11 @@ describe('EncryptedFS Links', () => { const dmtime1 = (await efs.stat(n0)).mtime.getTime(); await sleep(10); setId(efs, 0o65534); - await expectError(efs.link(n0, n1), errno.EACCES); + await expectError( + efs.link(n0, n1), + ErrorEncryptedFSError, + errno.EACCES, + ); const ctime2 = (await efs.stat(n0)).ctime.getTime(); expect(ctime1).toEqual(ctime2); const dctime2 = (await efs.stat(n0)).ctime.getTime(); @@ -380,7 +430,11 @@ describe('EncryptedFS Links', () => { ); test('should not create hardlinks to directories', async () => { await efs.mkdir(`test`); - await expectError(efs.link(`test`, `hardlinkttotest`), errno.EPERM); + await expectError( + efs.link(`test`, `hardlinkttotest`), + ErrorEncryptedFSError, + errno.EPERM, + ); }); test('can create multiple hardlinks to the same file', async () => { await efs.mkdir(`test`); @@ -400,11 +454,13 @@ describe('EncryptedFS Links', () => { await createFile(efs, type as FileTypes, path.join(n0, n1)); await expectError( efs.link(path.join(n0, n1, 'test'), path.join(n0, n2)), + ErrorEncryptedFSError, errno.ENOTDIR, ); await createFile(efs, type as FileTypes, path.join(n0, n2)); await expectError( efs.link(path.join(n0, n2), path.join(n0, n1, 'test')), + ErrorEncryptedFSError, errno.ENOTDIR, ); } @@ -422,16 +478,19 @@ describe('EncryptedFS Links', () => { await efs.chmod(n1, 0o0644); await expectError( efs.link(path.join(n1, n3), path.join(n1, n4)), + ErrorEncryptedFSError, errno.EACCES, ); await expectError( efs.link(path.join(n1, n3), path.join(n2, n4)), + ErrorEncryptedFSError, errno.EACCES, ); await efs.chmod(n1, 0o0755); await efs.chmod(n2, 0o0644); await expectError( efs.link(path.join(n1, n3), path.join(n2, n4)), + ErrorEncryptedFSError, errno.EACCES, ); }); @@ -449,46 +508,68 @@ describe('EncryptedFS Links', () => { await efs.chmod(n2, 0o0555); await expectError( efs.link(path.join(n1, n3), path.join(n2, n4)), + ErrorEncryptedFSError, errno.EACCES, ); await efs.chmod(n1, 0o0555); await expectError( efs.link(path.join(n1, n3), path.join(n1, n4)), + ErrorEncryptedFSError, errno.EACCES, ); }); test('returns ELOOP if too many symbolic links were encountered in translating one of the pathnames', async () => { await efs.symlink(n0, n1); await efs.symlink(n1, n0); - await expectError(efs.link(path.join(n0, 'test'), n2), errno.ELOOP); - await expectError(efs.link(path.join(n1, 'test'), n2), errno.ELOOP); + await expectError( + efs.link(path.join(n0, 'test'), n2), + ErrorEncryptedFSError, + errno.ELOOP, + ); + await expectError( + efs.link(path.join(n1, 'test'), n2), + ErrorEncryptedFSError, + errno.ELOOP, + ); await createFile(efs, 'regular', n2); - await expectError(efs.link(n2, path.join(n0, 'test')), errno.ELOOP); - await expectError(efs.link(n2, path.join(n1, 'test')), errno.ELOOP); + await expectError( + efs.link(n2, path.join(n0, 'test')), + ErrorEncryptedFSError, + errno.ELOOP, + ); + await expectError( + efs.link(n2, path.join(n1, 'test')), + ErrorEncryptedFSError, + errno.ELOOP, + ); }); test('returns ENOENT if the source file does not exist', async () => { await createFile(efs, 'regular', n0); await efs.link(n0, n1); await efs.unlink(n0); await efs.unlink(n1); - await expectError(efs.link(n0, n1), errno.ENOENT); + await expectError(efs.link(n0, n1), ErrorEncryptedFSError, errno.ENOENT); }); test.each(supportedTypes)( 'returns EEXIST if the destination %s does exist', async (type) => { await efs.writeFile(n0, ''); await createFile(efs, type, n1); - await expectError(efs.link(n0, n1), errno.EEXIST); + await expectError( + efs.link(n0, n1), + ErrorEncryptedFSError, + errno.EEXIST, + ); }, ); test('returns EPERM if the source file is a directory', async () => { await efs.mkdir(n0); - await expectError(efs.link(n0, n1), errno.EPERM); + await expectError(efs.link(n0, n1), ErrorEncryptedFSError, errno.EPERM); await efs.mkdir(n2, { mode: dp }); await efs.chown(n2, tuid, tuid); setId(efs, tuid); - await expectError(efs.link(n2, n3), errno.EPERM); + await expectError(efs.link(n2, n3), ErrorEncryptedFSError, errno.EPERM); }); }); }); diff --git a/tests/EncryptedFS.nav.test.ts b/tests/EncryptedFS.nav.test.ts index 3cfb088a..c8c90073 100644 --- a/tests/EncryptedFS.nav.test.ts +++ b/tests/EncryptedFS.nav.test.ts @@ -4,9 +4,10 @@ import pathNode from 'path'; import Logger, { StreamHandler, LogLevel } from '@matrixai/logger'; import { running } from '@matrixai/async-init'; import { code as errno } from 'errno'; -import { EncryptedFS, constants } from '@'; +import EncryptedFS from '@/EncryptedFS'; +import * as constants from '@/constants'; import * as utils from '@/utils'; -import * as errors from '@/errors'; +import { ErrorEncryptedFSError } from '@/errors'; import { expectError } from './utils'; describe('EncryptedFS Navigation', () => { @@ -36,27 +37,6 @@ describe('EncryptedFS Navigation', () => { recursive: true, }); }); - test('creation of EFS', async () => { - expect(efs).toBeInstanceOf(EncryptedFS); - }); - test('Validation of keys', async () => { - await efs.stop(); - const falseDbKey = await utils.generateKey(256); - await expect( - EncryptedFS.createEncryptedFS({ - dbKey: falseDbKey, - dbPath, - umask: 0o022, - logger, - }), - ).rejects.toThrow(errors.ErrorEncryptedFSKey); - efs = await EncryptedFS.createEncryptedFS({ - dbKey, - dbPath, - umask: 0o022, - logger, - }); - }); test('EFS using callback style functions', (done) => { const str = 'callback'; const flags = constants.O_CREAT | constants.O_RDWR; @@ -121,10 +101,18 @@ describe('EncryptedFS Navigation', () => { }); test('trailing slash refers to the directory instead of a file', async () => { await efs.writeFile(`abc`, ''); - await expectError(efs.access(`abc/`, undefined), errno.ENOTDIR); - await expectError(efs.access(`abc/.`, undefined), errno.ENOTDIR); - await expectError(efs.mkdir(`abc/.`), errno.ENOTDIR); - await expectError(efs.mkdir(`abc/`), errno.EEXIST); + await expectError( + efs.access(`abc/`, undefined), + ErrorEncryptedFSError, + errno.ENOTDIR, + ); + await expectError( + efs.access(`abc/.`, undefined), + ErrorEncryptedFSError, + errno.ENOTDIR, + ); + await expectError(efs.mkdir(`abc/.`), ErrorEncryptedFSError, errno.ENOTDIR); + await expectError(efs.mkdir(`abc/`), ErrorEncryptedFSError, errno.EEXIST); }); test('trailing slash works for non-existent directories when intending to create them', async () => { await efs.mkdir(`abc/`); @@ -132,47 +120,116 @@ describe('EncryptedFS Navigation', () => { expect(stat.isDirectory()).toStrictEqual(true); }); test('trailing `/.` for mkdir should result in errors', async () => { - await expectError(efs.mkdir(`abc/.`), errno.ENOENT); + await expectError(efs.mkdir(`abc/.`), ErrorEncryptedFSError, errno.ENOENT); await efs.mkdir(`abc`); - await expectError(efs.mkdir(`abc/.`), errno.EEXIST); + await expectError(efs.mkdir(`abc/.`), ErrorEncryptedFSError, errno.EEXIST); }); test('navigating invalid paths', async () => { await efs.mkdir('/test/a/b/c', { recursive: true }); await efs.mkdir('/test/a/bc', { recursive: true }); await efs.mkdir('/test/abc', { recursive: true }); - await expectError(efs.readdir('/test/abc/a/b/c'), errno.ENOENT); - await expectError(efs.readdir('/abc'), errno.ENOENT); - await expectError(efs.stat('/test/abc/a/b/c'), errno.ENOENT); - await expectError(efs.mkdir('/test/abc/a/b/c'), errno.ENOENT); - await expectError(efs.writeFile('/test/abc/a/b/c', 'Hello'), errno.ENOENT); - await expectError(efs.readFile('/test/abc/a/b/c'), errno.ENOENT); - await expectError(efs.readFile('/test/abcd'), errno.ENOENT); - await expectError(efs.mkdir('/test/abcd/dir'), errno.ENOENT); - await expectError(efs.unlink('/test/abcd'), errno.ENOENT); - await expectError(efs.unlink('/test/abcd/file'), errno.ENOENT); - await expectError(efs.stat('/test/a/d/b/c'), errno.ENOENT); - await expectError(efs.stat('/test/abcd'), errno.ENOENT); + await expectError( + efs.readdir('/test/abc/a/b/c'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError(efs.readdir('/abc'), ErrorEncryptedFSError, errno.ENOENT); + await expectError( + efs.stat('/test/abc/a/b/c'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.mkdir('/test/abc/a/b/c'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.writeFile('/test/abc/a/b/c', 'Hello'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.readFile('/test/abc/a/b/c'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.readFile('/test/abcd'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.mkdir('/test/abcd/dir'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.unlink('/test/abcd'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.unlink('/test/abcd/file'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.stat('/test/a/d/b/c'), + ErrorEncryptedFSError, + errno.ENOENT, + ); + await expectError( + efs.stat('/test/abcd'), + ErrorEncryptedFSError, + errno.ENOENT, + ); }); test('various failure situations', async () => { await efs.mkdir('/test/dir', { recursive: true }); + await efs.writeFile('/test/file', 'Hello'); + await expectError( + efs.writeFile('/test/dir', 'Hello'), + ErrorEncryptedFSError, + errno.EISDIR, + ); await expectError( - efs.mkdir('/test/dir', { recursive: true }), + efs.writeFile('/', 'Hello'), + ErrorEncryptedFSError, + errno.EISDIR, + ); + await expectError(efs.rmdir('/'), ErrorEncryptedFSError, errno.EINVAL); + await expectError(efs.unlink('/'), ErrorEncryptedFSError, errno.EISDIR); + await expectError( + efs.mkdir('/test/dir'), + ErrorEncryptedFSError, + errno.EEXIST, + ); + await expectError( + efs.mkdir('/test/file'), + ErrorEncryptedFSError, errno.EEXIST, ); - await efs.writeFile('/test/file', 'Hello'); - await expectError(efs.writeFile('/test/dir', 'Hello'), errno.EISDIR); - await expectError(efs.writeFile('/', 'Hello'), errno.EISDIR); - await expectError(efs.rmdir('/'), errno.EINVAL); - await expectError(efs.unlink('/'), errno.EISDIR); - await expectError(efs.mkdir('/test/dir'), errno.EEXIST); - await expectError(efs.mkdir('/test/file'), errno.EEXIST); await expectError( efs.mkdir('/test/file', { recursive: true }), + ErrorEncryptedFSError, errno.EEXIST, ); - await expectError(efs.readdir('/test/file'), errno.ENOTDIR); - await expectError(efs.readlink('/test/dir'), errno.EINVAL); - await expectError(efs.readlink('/test/file'), errno.EINVAL); + await expectError( + efs.readdir('/test/file'), + ErrorEncryptedFSError, + errno.ENOTDIR, + ); + await expectError( + efs.readlink('/test/dir'), + ErrorEncryptedFSError, + errno.EINVAL, + ); + await expectError( + efs.readlink('/test/file'), + ErrorEncryptedFSError, + errno.EINVAL, + ); }); test('cwd returns the absolute fully resolved path', async () => { await efs.mkdir('/a/b', { recursive: true }); @@ -220,7 +277,7 @@ describe('EncryptedFS Navigation', () => { await efs.mkdir('/dir'); await efs.chmod('/dir', 0o666); efs.uid = 1000; - await expectError(efs.chdir('/dir'), errno.EACCES); + await expectError(efs.chdir('/dir'), ErrorEncryptedFSError, errno.EACCES); }); test('should be able to access inodes inside chroot', async () => { await efs.mkdir('dir'); @@ -241,14 +298,22 @@ describe('EncryptedFS Navigation', () => { const efs2 = await efs.chroot('dir'); await efs.exists('/../../../file'); await expect(efs2.exists('/../../../file')).resolves.toBeFalsy(); - await expectError(efs2.readFile('../../../file'), errno.ENOENT); + await expectError( + efs2.readFile('../../../file'), + ErrorEncryptedFSError, + errno.ENOENT, + ); }); test('should not be able to access inodes outside chroot using symlink', async () => { await efs.mkdir(`dir`); await efs.writeFile('file', 'test'); await efs.symlink('file', 'dir/link'); const efs2 = await efs.chroot('dir'); - await expectError(efs2.readFile('link'), errno.ENOENT); + await expectError( + efs2.readFile('link'), + ErrorEncryptedFSError, + errno.ENOENT, + ); }); test('prevents users from changing current directory above the chroot', async () => { await efs.mkdir('dir'); @@ -256,7 +321,11 @@ describe('EncryptedFS Navigation', () => { const efs2 = await efs.chroot('dir'); await efs2.chdir('/../'); await expect(efs2.readdir('.')).resolves.toEqual([]); - await expectError(efs2.chdir('/../dir1'), errno.ENOENT); + await expectError( + efs2.chdir('/../dir1'), + ErrorEncryptedFSError, + errno.ENOENT, + ); }); test('can sustain a current directory inside a chroot', async () => { await efs.mkdir('dir'); diff --git a/tests/EncryptedFS.perms.test.ts b/tests/EncryptedFS.perms.test.ts index f33a69ef..52a23d7d 100644 --- a/tests/EncryptedFS.perms.test.ts +++ b/tests/EncryptedFS.perms.test.ts @@ -3,8 +3,11 @@ import fs from 'fs'; import pathNode from 'path'; import Logger, { StreamHandler, LogLevel } from '@matrixai/logger'; import { code as errno } from 'errno'; +import EncryptedFS from '@/EncryptedFS'; +import { ErrorEncryptedFSError } from '@/errors'; +import * as constants from '@/constants'; +import * as permissions from '@/permissions'; import * as utils from '@/utils'; -import { EncryptedFS, constants, permissions } from '@'; import { expectError } from './utils'; describe('EncryptedFS Permissions', () => { @@ -28,6 +31,7 @@ describe('EncryptedFS Permissions', () => { }); }); afterEach(async () => { + await efs.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, @@ -81,10 +85,18 @@ describe('EncryptedFS Permissions', () => { efs.gid = 1000; await efs.chown('file', 1000, 1000); // You cannot give away files - await expectError(efs.chown('file', 2000, 2000), errno.EPERM); + await expectError( + efs.chown('file', 2000, 2000), + ErrorEncryptedFSError, + errno.EPERM, + ); // If you don't own the file, you also cannot change (even if your change is noop) efs.uid = 3000; - await expectError(efs.chown('file', 1000, 1000), errno.EPERM); + await expectError( + efs.chown('file', 1000, 1000), + ErrorEncryptedFSError, + errno.EPERM, + ); }); test('chmod only works if you are the owner of the file', async () => { await efs.writeFile('file', 'hello'); @@ -92,7 +104,11 @@ describe('EncryptedFS Permissions', () => { efs.uid = 1000; await efs.chmod('file', 0o000); efs.uid = 2000; - await expectError(efs.chmod('file', 0o777), errno.EPERM); + await expectError( + efs.chmod('file', 0o777), + ErrorEncryptedFSError, + errno.EPERM, + ); }); test('permissions are checked in stages of user, group then other', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -112,17 +128,27 @@ describe('EncryptedFS Permissions', () => { efs.uid = 2000; await efs.access('testfile', constants.R_OK | constants.W_OK); await efs.access('dir', constants.R_OK | constants.W_OK); - await expectError(efs.access('testfile', constants.X_OK), errno.EACCES); - await expectError(efs.access('dir', constants.X_OK), errno.EACCES); + await expectError( + efs.access('testfile', constants.X_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.access('dir', constants.X_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); efs.gid = 2000; await efs.access('testfile', constants.R_OK); await efs.access('dir', constants.R_OK); await expectError( efs.access('testfile', fs.constants.W_OK | fs.constants.X_OK), + ErrorEncryptedFSError, errno.EACCES, ); await expectError( efs.access('dir', fs.constants.W_OK | fs.constants.X_OK), + ErrorEncryptedFSError, errno.EACCES, ); }); @@ -149,8 +175,16 @@ describe('EncryptedFS Permissions', () => { efs.gid = 1000; await efs.access('testfile', constants.R_OK | constants.W_OK); await efs.access('dir', constants.R_OK | constants.W_OK); - await expectError(efs.access('testfile', constants.X_OK), errno.EACCES); - await expectError(efs.access('dir', constants.X_OK), errno.EACCES); + await expectError( + efs.access('testfile', constants.X_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.access('dir', constants.X_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); efs.uid = permissions.DEFAULT_ROOT_UID; efs.uid = permissions.DEFAULT_ROOT_GID; await efs.chown('testfile', 2000, 2000); @@ -161,10 +195,12 @@ describe('EncryptedFS Permissions', () => { await efs.access('dir', constants.R_OK); await expectError( efs.access('testfile', constants.W_OK | constants.X_OK), + ErrorEncryptedFSError, errno.EACCES, ); await expectError( efs.access('dir', constants.W_OK | constants.X_OK), + ErrorEncryptedFSError, errno.EACCES, ); }); @@ -177,10 +213,12 @@ describe('EncryptedFS Permissions', () => { efs.gid = 1000; await expectError( efs.access(`file`, constants.R_OK | constants.W_OK), + ErrorEncryptedFSError, errno.EACCES, ); await expectError( efs.access(`dir`, constants.R_OK | constants.W_OK), + ErrorEncryptedFSError, errno.EACCES, ); await efs.access(`file`, constants.X_OK); @@ -194,9 +232,21 @@ describe('EncryptedFS Permissions', () => { efs.gid = 1000; await efs.writeFile(`file`, 'hello'); await efs.chmod(`file`, 0o000); - await expectError(efs.access(`file`, constants.X_OK), errno.EACCES); - await expectError(efs.open(`file`, 'r'), errno.EACCES); - await expectError(efs.open(`file`, 'w'), errno.EACCES); + await expectError( + efs.access(`file`, constants.X_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.open(`file`, 'r'), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.open(`file`, 'w'), + ErrorEncryptedFSError, + errno.EACCES, + ); const stat = await efs.stat(`file`); expect(stat.isFile()).toStrictEqual(true); }); @@ -210,11 +260,19 @@ describe('EncryptedFS Permissions', () => { const str = 'hello'; await efs.writeFile(`file`, str); await efs.chmod(`file`, 0o400); - await expectError(efs.access(`file`, constants.X_OK), errno.EACCES); + await expectError( + efs.access(`file`, constants.X_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); await expect(efs.readFile(`file`, { encoding: 'utf8' })).resolves.toEqual( str, ); - await expectError(efs.open(`file`, 'w'), errno.EACCES); + await expectError( + efs.open(`file`, 'w'), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test('file permissions rw-', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -224,7 +282,11 @@ describe('EncryptedFS Permissions', () => { efs.gid = 1000; await efs.writeFile(`file`, 'world', { mode: 0o666 }); await efs.chmod(`file`, 0o600); - await expectError(efs.access(`file`, constants.X_OK), errno.EACCES); + await expectError( + efs.access(`file`, constants.X_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); const str = 'hello'; await efs.writeFile(`file`, str); await expect(efs.readFile(`file`, { encoding: 'utf8' })).resolves.toEqual( @@ -269,9 +331,17 @@ describe('EncryptedFS Permissions', () => { const str = 'hello'; await efs.writeFile(`file`, str); await efs.chmod(`file`, 0o200); - await expectError(efs.access(`file`, constants.X_OK), errno.EACCES); + await expectError( + efs.access(`file`, constants.X_OK), + ErrorEncryptedFSError, + errno.EACCES, + ); await efs.writeFile(`file`, str); - await expectError(efs.open(`file`, 'r'), errno.EACCES); + await expectError( + efs.open(`file`, 'r'), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test('file permissions -wx', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -284,7 +354,11 @@ describe('EncryptedFS Permissions', () => { await efs.chmod(`file`, 0o300); await efs.access(`file`, constants.X_OK); await efs.writeFile(`file`, str); - await expectError(efs.open(`file`, 'r'), errno.EACCES); + await expectError( + efs.open(`file`, 'r'), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test('file permissions --x', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -295,8 +369,16 @@ describe('EncryptedFS Permissions', () => { await efs.writeFile(`file`, 'hello'); await efs.chmod(`file`, 0o100); await efs.access(`file`, constants.X_OK); - await expectError(efs.open(`file`, 'w'), errno.EACCES); - await expectError(efs.open(`file`, 'r'), errno.EACCES); + await expectError( + efs.open(`file`, 'w'), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError( + efs.open(`file`, 'r'), + ErrorEncryptedFSError, + errno.EACCES, + ); }); test('directory permissions ---', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -308,9 +390,13 @@ describe('EncryptedFS Permissions', () => { await efs.chmod(`---`, 0o000); const stat = await efs.stat(`---`); expect(stat.isDirectory()).toStrictEqual(true); - await expectError(efs.writeFile(`---/a`, 'hello'), errno.EACCES); - await expectError(efs.chdir(`---`), errno.EACCES); - await expectError(efs.readdir(`---`), errno.EACCES); + await expectError( + efs.writeFile(`---/a`, 'hello'), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError(efs.chdir(`---`), ErrorEncryptedFSError, errno.EACCES); + await expectError(efs.readdir(`---`), ErrorEncryptedFSError, errno.EACCES); }); test('directory permissions r--', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -321,12 +407,16 @@ describe('EncryptedFS Permissions', () => { await efs.mkdir(`r--`); await efs.writeFile(`r--/a`, 'hello'); await efs.chmod(`r--`, 0o400); - await expectError(efs.writeFile(`r--/b`, 'hello'), errno.EACCES); - await expectError(efs.chdir(`r--`), errno.EACCES); + await expectError( + efs.writeFile(`r--/b`, 'hello'), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError(efs.chdir(`r--`), ErrorEncryptedFSError, errno.EACCES); await expect(efs.readdir(`r--`)).resolves.toContain('a'); // You can always change metadata even without write permissions await efs.utimes(`r--`, new Date(), new Date()); - await expectError(efs.stat(`r--/a`), errno.EACCES); + await expectError(efs.stat(`r--/a`), ErrorEncryptedFSError, errno.EACCES); }); test('directory permissions rw-', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -338,15 +428,23 @@ describe('EncryptedFS Permissions', () => { await efs.writeFile(`rw-/a`, 'hello'); await efs.chmod(`rw-`, 0o600); // You cannot write into a file - await expectError(efs.writeFile(`rw-/a`, 'world'), errno.EACCES); + await expectError( + efs.writeFile(`rw-/a`, 'world'), + ErrorEncryptedFSError, + errno.EACCES, + ); // You cannot create a new file - await expectError(efs.writeFile(`rw-/b`, 'hello'), errno.EACCES); + await expectError( + efs.writeFile(`rw-/b`, 'hello'), + ErrorEncryptedFSError, + errno.EACCES, + ); // You cannot remove files - await expectError(efs.unlink(`rw-/a`), errno.EACCES); - await expectError(efs.chdir(`rw-`), errno.EACCES); + await expectError(efs.unlink(`rw-/a`), ErrorEncryptedFSError, errno.EACCES); + await expectError(efs.chdir(`rw-`), ErrorEncryptedFSError, errno.EACCES); await expect(efs.readdir(`rw-`)).resolves.toContain('a'); await efs.utimes(`rw-`, new Date(), new Date()); - await expectError(efs.stat(`rw-/a`), errno.EACCES); + await expectError(efs.stat(`rw-/a`), ErrorEncryptedFSError, errno.EACCES); }); test('directory permissions rwx', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -380,7 +478,11 @@ describe('EncryptedFS Permissions', () => { // You can write to the file await efs.writeFile(`r-x/a`, str); // You cannot create new files - await expectError(efs.writeFile(`r-x/b`, str), errno.EACCES); + await expectError( + efs.writeFile(`r-x/b`, str), + ErrorEncryptedFSError, + errno.EACCES, + ); // You can read the directory await expect(efs.readdir(`r-x`)).resolves.toContain('a'); await expect(efs.readdir(`r-x`)).resolves.toContain('dir'); @@ -393,9 +495,9 @@ describe('EncryptedFS Permissions', () => { const stat = await efs.stat(`dir`); expect(stat.isDirectory()).toStrictEqual(true); // You cannot delete the file - await expectError(efs.unlink(`./a`), errno.EACCES); + await expectError(efs.unlink(`./a`), ErrorEncryptedFSError, errno.EACCES); // Cannot delete the directory - await expectError(efs.rmdir(`dir`), errno.EACCES); + await expectError(efs.rmdir(`dir`), ErrorEncryptedFSError, errno.EACCES); }); test('directory permissions -w-', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -405,9 +507,13 @@ describe('EncryptedFS Permissions', () => { efs.gid = 1000; await efs.mkdir(`-w-`); await efs.chmod(`-w-`, 0o000); - await expectError(efs.writeFile(`-w-/a`, 'hello'), errno.EACCES); - await expectError(efs.chdir(`-w-`), errno.EACCES); - await expectError(efs.readdir(`-w-`), errno.EACCES); + await expectError( + efs.writeFile(`-w-/a`, 'hello'), + ErrorEncryptedFSError, + errno.EACCES, + ); + await expectError(efs.chdir(`-w-`), ErrorEncryptedFSError, errno.EACCES); + await expectError(efs.readdir(`-w-`), ErrorEncryptedFSError, errno.EACCES); }); test('directory permissions -wx', async () => { await efs.mkdir('/home/1000', { recursive: true }); @@ -425,7 +531,7 @@ describe('EncryptedFS Permissions', () => { await efs.unlink(`-wx/a`); await efs.chdir('-wx'); await efs.mkdir(`./dir`); - await expectError(efs.readdir(`.`), errno.EACCES); + await expectError(efs.readdir(`.`), ErrorEncryptedFSError, errno.EACCES); const stat = await efs.stat(`./dir`); expect(stat.isDirectory()).toStrictEqual(true); }); @@ -439,10 +545,14 @@ describe('EncryptedFS Permissions', () => { const str = 'hello'; await efs.writeFile(`--x/a`, str); await efs.chmod(`--x`, 0o100); - await expectError(efs.writeFile(`--x/b`, 'world'), errno.EACCES); + await expectError( + efs.writeFile(`--x/b`, 'world'), + ErrorEncryptedFSError, + errno.EACCES, + ); await efs.chdir('--x'); - await expectError(efs.unlink(`./a`), errno.EACCES); - await expectError(efs.readdir(`.`), errno.EACCES); + await expectError(efs.unlink(`./a`), ErrorEncryptedFSError, errno.EACCES); + await expectError(efs.readdir(`.`), ErrorEncryptedFSError, errno.EACCES); await expect(efs.readFile(`./a`, { encoding: 'utf8' })).resolves.toEqual( str, ); @@ -504,10 +614,12 @@ describe('EncryptedFS Permissions', () => { await efs.chmod('dir', 0o124); await expectError( efs.access('file', constants.R_OK | constants.W_OK), + ErrorEncryptedFSError, errno.EACCES, ); await expectError( efs.access('dir', constants.R_OK | constants.W_OK), + ErrorEncryptedFSError, errno.EACCES, ); await efs.access('file', fs.constants.X_OK); diff --git a/tests/EncryptedFS.streams.test.ts b/tests/EncryptedFS.streams.test.ts index 6d1f07c8..0b5e0a94 100644 --- a/tests/EncryptedFS.streams.test.ts +++ b/tests/EncryptedFS.streams.test.ts @@ -3,8 +3,9 @@ import fs from 'fs'; import pathNode from 'path'; import Logger, { StreamHandler, LogLevel } from '@matrixai/logger'; import { Readable, Writable } from 'readable-stream'; +import EncryptedFS from '@/EncryptedFS'; import * as utils from '@/utils'; -import { EncryptedFS } from '@'; +import { promise } from '@/utils'; describe('EncryptedFS Streams', () => { const logger = new Logger('EncryptedFS Streams', LogLevel.WARN, [ @@ -27,6 +28,7 @@ describe('EncryptedFS Streams', () => { }); }); afterEach(async () => { + await efs.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, @@ -36,7 +38,7 @@ describe('EncryptedFS Streams', () => { test("using 'for await'", async () => { const str = 'Hello'; await efs.writeFile(`/test`, str); - const readable = await efs.createReadStream(`/test`, { + const readable = efs.createReadStream(`/test`, { encoding: 'utf8', start: 0, end: str.length - 1, @@ -47,10 +49,10 @@ describe('EncryptedFS Streams', () => { } expect(readString).toBe(str); }); - test("using 'event readable'", async (done) => { + test("using 'event readable'", async () => { const str = 'Hello'; await efs.writeFile(`/test`, str); - const readable = await efs.createReadStream(`/test`, { + const readable = efs.createReadStream(`/test`, { encoding: 'utf8', start: 0, end: str.length - 1, @@ -62,15 +64,17 @@ describe('EncryptedFS Streams', () => { data += chunk; } }); + const ended = promise(); readable.on('end', () => { expect(data).toBe(str); - done(); + ended.resolveP(); }); + await ended.p; }); - test("using 'event data'", async (done) => { + test("using 'event data'", async () => { const str = 'Hello'; await efs.writeFile(`/test`, str); - const readable = await efs.createReadStream(`/test`, { + const readable = efs.createReadStream(`/test`, { encoding: 'utf8', start: 0, end: str.length - 1, @@ -79,15 +83,17 @@ describe('EncryptedFS Streams', () => { readable.on('data', (chunk) => { data += chunk; }); + const ended = promise(); readable.on('end', () => { expect(data).toBe(str); - done(); + ended.resolveP(); }); + await ended.p; }); - test('respects start and end options', async (done) => { + test('respects start and end options', async () => { const str = 'Hello'; await efs.writeFile(`file`, str, { encoding: 'utf8' }); - const readable = await efs.createReadStream(`file`, { + const readable = efs.createReadStream(`file`, { encoding: 'utf8', start: 1, end: 3, @@ -99,16 +105,18 @@ describe('EncryptedFS Streams', () => { data += chunk; } }); + const ended = promise(); readable.on('end', () => { expect(data).toBe(str.slice(1, 4)); - done(); + ended.resolveP(); }); + await ended.p; }); - test('respects the high watermark', async (done) => { + test('respects the high watermark', async () => { const str = 'Hello'; const highWatermark = 2; await efs.writeFile(`file`, str, { encoding: 'utf8' }); - const readable = await efs.createReadStream(`file`, { + const readable = efs.createReadStream(`file`, { encoding: 'utf8', highWaterMark: highWatermark, }); @@ -122,17 +130,19 @@ describe('EncryptedFS Streams', () => { counter += highWatermark; } }); + const ended = promise(); readable.on('end', () => { expect(data).toBe(str); - done(); + ended.resolveP(); }); + await ended.p; }); - test('respects the start option', async (done) => { + test('respects the start option', async () => { const str = 'Hello'; const filePath = `file`; const offset = 1; await efs.writeFile(filePath, str, { encoding: 'utf8' }); - const readable = await efs.createReadStream(filePath, { + const readable = efs.createReadStream(filePath, { encoding: 'utf8', start: offset, }); @@ -143,16 +153,18 @@ describe('EncryptedFS Streams', () => { data += chunk; } }); + const ended = promise(); readable.on('end', () => { expect(data).toBe(str.slice(offset)); - done(); + ended.resolveP(); }); + await ended.p; }); - test('end option is ignored without the start option', async (done) => { + test('end option is ignored without the start option', async () => { const str = 'Hello'; const filePath = `file`; await efs.writeFile(filePath, str); - const readable = await efs.createReadStream(filePath, { + const readable = efs.createReadStream(filePath, { encoding: 'utf8', end: 1, }); @@ -163,19 +175,21 @@ describe('EncryptedFS Streams', () => { data += chunk; } }); + const ended = promise(); readable.on('end', () => { expect(data).toBe(str); - done(); + ended.resolveP(); }); + await ended.p; }); - test('can use a file descriptor', async (done) => { + test('can use a file descriptor', async () => { const str = 'Hello'; const filePath = `file`; await efs.writeFile(filePath, str); const fd = await efs.open(filePath, 'r'); const offset = 1; await efs.lseek(fd, offset); - const readable = await efs.createReadStream('', { + const readable = efs.createReadStream('', { encoding: 'utf8', fd: fd, }); @@ -186,17 +200,19 @@ describe('EncryptedFS Streams', () => { data += chunk; } }); + const ended = promise(); readable.on('end', () => { expect(data).toBe(str.slice(offset)); - done(); + ended.resolveP(); }); + await ended.p; }); - test('with start option overrides the file descriptor position', async (done) => { + test('with start option overrides the file descriptor position', async () => { const str = 'Hello'; await efs.writeFile(`file`, str); const fd = await efs.open(`file`, 'r'); const offset = 1; - const readable = await efs.createReadStream('', { + const readable = efs.createReadStream('', { encoding: 'utf8', fd: fd, start: offset, @@ -208,32 +224,36 @@ describe('EncryptedFS Streams', () => { data += chunk; } }); + const ended = promise(); readable.on('end', async () => { expect(data).toBe(str.slice(offset)); const buf = Buffer.allocUnsafe(1); await efs.read(fd, buf, 0, buf.length); expect(buf.toString('utf8')).toBe(str.slice(0, buf.length)); - done(); + ended.resolveP(); }); + await ended.p; }); - test('can handle errors asynchronously', async (done) => { - const stream = await efs.createReadStream(`file`); + test('can handle errors asynchronously', async () => { + const stream = efs.createReadStream(`file`); + const ended = promise(); stream.on('error', (err) => { expect(err instanceof Error).toBe(true); const error = err as any; expect(error.code).toBe('ENOENT'); - done(); + ended.resolveP(); }); stream.read(10); + await ended.p; }); - test('can compose with pipes', async (done) => { + test('can compose with pipes', async () => { const str = 'Hello'; await efs.writeFile(`file`, str); - const readStream = await efs.createReadStream(`file`, { + const readStream = efs.createReadStream(`file`, { encoding: 'utf8', end: 10, }); - // Creating a test writable stream. + // Creating a test writable stream let data = ''; class TestWritable extends Writable { constructor() { @@ -245,22 +265,24 @@ describe('EncryptedFS Streams', () => { } } + const ended = promise(); const testWritable = new TestWritable(); - // @ts-ignore: This works but problem with types. + // @ts-ignore: This works but problem with types readStream.pipe(testWritable); testWritable.on('finish', () => { expect(data).toEqual(str); - done(); + ended.resolveP(); }); + await ended.p; }); }); describe('writestream', () => { - test('can compose with pipes', async (done) => { + test('can compose with pipes', async () => { const message = 'Hello there kenobi'; const str = ''; await efs.writeFile(`file`, str); - const writeStream = await efs.createWriteStream('file', { + const writeStream = efs.createWriteStream('file', { encoding: 'utf8', }); @@ -279,44 +301,50 @@ describe('EncryptedFS Streams', () => { } } + const ended = promise(); const testReadableStream = new TestReadableStream(); - // @ts-ignore: This works but problem with types. + // @ts-ignore: This works but problem with types testReadableStream.pipe(writeStream); writeStream.on('finish', async () => { const data = await efs.readFile('file'); expect(data.toString()).toEqual(message); - done(); + ended.resolveP(); }); + await ended.p; }); - test('can create and truncate files', async (done) => { + test('can create and truncate files', async () => { const str = 'Hello'; const fileName = `file`; - const writable = await efs.createWriteStream(fileName, {}); + const ended = promise(); + const writable = efs.createWriteStream(fileName, {}); writable.end(str, async () => { const readStr = await efs.readFile(fileName, { encoding: 'utf-8' }); expect(readStr).toEqual(str); - const truncateWritable = await efs.createWriteStream(fileName, {}); + const truncateWritable = efs.createWriteStream(fileName, {}); truncateWritable.end('', async () => { const readStr = await efs.readFile(fileName, { encoding: 'utf-8' }); expect(readStr).toEqual(''); - done(); + ended.resolveP(); }); }); + await ended.p; }); - test('can be written into', async (done) => { + test('can be written into', async () => { const str = 'Hello'; - const stream = await efs.createWriteStream('file'); + const stream = efs.createWriteStream('file'); stream.write(Buffer.from(str)); stream.end(); + const ended = promise(); stream.on('finish', async () => { const readStr = await efs.readFile('file', { encoding: 'utf-8' }); expect(readStr).toEqual(str); - done(); + ended.resolveP(); }); + await ended.p; }); - test('allow ignoring of the drain event, temporarily ignoring resource usage control', async (done) => { + test('allow ignoring of the drain event, temporarily ignoring resource usage control', async () => { const waterMark = 10; - const writable = await efs.createWriteStream('file', { + const writable = efs.createWriteStream('file', { highWaterMark: waterMark, }); const buf = Buffer.allocUnsafe(waterMark).fill(97); @@ -324,20 +352,23 @@ describe('EncryptedFS Streams', () => { for (let i = 0; i < times; ++i) { expect(writable.write(buf)).toBe(false); } + const ended = promise(); writable.end(async () => { const readStr = await efs.readFile('file', { encoding: 'utf8' }); expect(readStr).toBe(buf.toString().repeat(times)); - done(); + ended.resolveP(); }); + await ended.p; }); - test('can use the drain event to manage resource control', async (done) => { + test('can use the drain event to manage resource control', async () => { const waterMark = 10; - const writable = await efs.createWriteStream('file', { + const writable = efs.createWriteStream('file', { highWaterMark: waterMark, }); const buf = Buffer.allocUnsafe(waterMark).fill(97); let times = 10; const timesOrig = times; + const ended = promise(); const writing = () => { let status; do { @@ -347,7 +378,7 @@ describe('EncryptedFS Streams', () => { writable.end(async () => { const readStr = await efs.readFile('file', { encoding: 'utf8' }); expect(readStr).toBe(buf.toString().repeat(timesOrig)); - done(); + ended.resolveP(); }); } } while (times > 0 && status); @@ -356,18 +387,21 @@ describe('EncryptedFS Streams', () => { } }; writing(); + await ended.p; }); - test('can handle errors asynchronously', async (done) => { + test('can handle errors asynchronously', async () => { const fileName = `file/unknown`; - const writable = await efs.createWriteStream(fileName); + const writable = efs.createWriteStream(fileName); // Note that it is possible to have the finish event occur before the error event + const ended = promise(); writable.once('error', (err) => { expect(err instanceof Error).toBe(true); const error = err as any; expect(error.code).toBe('ENOENT'); - done(); + ended.resolveP(); }); writable.end(); + await ended.p; }); }); }); diff --git a/tests/EncryptedFS.test.ts b/tests/EncryptedFS.test.ts index 1ff67365..2aa1f1a7 100644 --- a/tests/EncryptedFS.test.ts +++ b/tests/EncryptedFS.test.ts @@ -2,12 +2,12 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import EncryptedFS from '@/EncryptedFS'; import * as utils from '@/utils'; import * as errors from '@/errors'; -import { EncryptedFS } from '@'; -describe('EncryptedFS', () => { - const logger = new Logger('EncryptedFS Test', LogLevel.WARN, [ +describe(EncryptedFS.name, () => { + const logger = new Logger(`${EncryptedFS.name} Test`, LogLevel.WARN, [ new StreamHandler(), ]); const dbKey: Buffer = utils.generateKeySync(256); @@ -95,4 +95,88 @@ describe('EncryptedFS', () => { expect(efs.constants.S_IRWXG).toBeDefined(); await efs.stop(); }); + test('validate key', async () => { + let efs = await EncryptedFS.createEncryptedFS({ + dbPath: dataDir, + dbKey, + logger, + }); + await efs.stop(); + const falseDbKey = await utils.generateKey(256); + await expect( + EncryptedFS.createEncryptedFS({ + dbPath: dataDir, + dbKey: falseDbKey, + logger, + }), + ).rejects.toThrow(errors.ErrorEncryptedFSKey); + efs = await EncryptedFS.createEncryptedFS({ + dbKey, + dbPath: dataDir, + logger, + }); + await efs.stop(); + }); + test('iNode allocation across restarts', async () => { + const d1 = 'dir1'; + const d1f1 = path.join(d1, 'file1'); + const d1f2 = path.join(d1, 'file2'); + const d1f3 = path.join(d1, 'file3'); + const d2 = 'dir2'; + const d2f1 = path.join(d2, 'file1'); + const d2f2 = path.join(d2, 'file2'); + const d2f3 = path.join(d2, 'file3'); + + let efs = await EncryptedFS.createEncryptedFS({ + dbPath: dataDir, + dbKey, + logger, + }); + + const listNodes = async (efs) => { + const iNodeManager = efs.iNodeMgr; + const nodes: Array = []; + for await (const iNode of iNodeManager.getAll()) { + nodes.push(iNode.ino); + } + return nodes; + }; + + await efs.mkdir(d1); + await efs.writeFile(d1f1, d1f1); + await efs.writeFile(d1f2, d1f2); + await efs.writeFile(d1f3, d1f3); + await efs.mkdir(d2); + await efs.writeFile(d2f1, d2f1); + await efs.writeFile(d2f2, d2f2); + await efs.writeFile(d2f3, d2f3); + // Inodes 1-9 allocated + + expect(await listNodes(efs)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + await efs.rmdir(d1, { recursive: true }); + // Inodes 1, 6-9 left + expect(await listNodes(efs)).toEqual([1, 6, 7, 8, 9]); + + // Re-creating the efs + await efs.stop(); + efs = await EncryptedFS.createEncryptedFS({ + dbPath: dataDir, + dbKey, + logger, + }); + + // Nodes should be maintained + expect(await listNodes(efs)).toEqual([1, 6, 7, 8, 9]); + + // Creating new nodes. + await efs.mkdir(d1); + await efs.writeFile(d1f1, d1f1); + await efs.writeFile(d1f2, d1f2); + await efs.writeFile(d1f3, d1f3); + + // Expecting 3, 4, 5 and 10 to be created + expect(await listNodes(efs)).toEqual([1, 3, 4, 5, 6, 7, 8, 9, 10]); + // Note that 2 is skipped, this seems to be incremented + // but not created when the RFS is created + }); }); diff --git a/tests/fd/FileDescriptor.test.ts b/tests/fd/FileDescriptor.test.ts index 23418be3..a2215be4 100644 --- a/tests/fd/FileDescriptor.test.ts +++ b/tests/fd/FileDescriptor.test.ts @@ -3,10 +3,11 @@ import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; -import { INodeManager } from '@/inodes'; -import { FileDescriptor } from '@/fd'; +import INodeManager from '@/inodes/INodeManager'; +import FileDescriptor from '@/fd/FileDescriptor'; import * as utils from '@/utils'; -import { constants, permissions } from '@'; +import * as constants from '@/constants'; +import * as permissions from '@/permissions'; describe('File Descriptor', () => { const logger = new Logger('File Descriptor', LogLevel.WARN, [ @@ -74,41 +75,38 @@ describe('File Descriptor', () => { const fileIno = iNodeMgr.inoAllocate(); const fd = new FileDescriptor(iNodeMgr, fileIno, 0); // Rejects as the iNode has not been created - await iNodeMgr.transact( - async (tran) => { - await expect(fd.setPos(tran, 1, constants.SEEK_SET)).rejects.toThrow( - Error, - ); - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - origBuffer, - ); - }, - [fileIno], - ); - await iNodeMgr.transact(async (tran) => { + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + await expect(fd.setPos(1, constants.SEEK_SET, tran)).rejects.toThrow( + Error, + ); + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + origBuffer, + tran, + ); + }); + await iNodeMgr.withTransactionF(async (tran) => { // Rejects as the new position would be a negativ number - await expect(fd.setPos(tran, -10, 0)).rejects.toThrow(Error); + await expect(fd.setPos(-10, 0, tran)).rejects.toThrow(Error); // Will seek the absolute position given - await fd.setPos(tran, 5, constants.SEEK_SET); + await fd.setPos(5, constants.SEEK_SET, tran); expect(fd.pos).toBe(5); // Will seek the current position plus the absolute position - await fd.setPos(tran, 5, constants.SEEK_CUR); + await fd.setPos(5, constants.SEEK_CUR, tran); expect(fd.pos).toBe(5 + 5); // Will seek the end of the data plus the absolute position - await fd.setPos(tran, 5, constants.SEEK_END); + await fd.setPos(5, constants.SEEK_END, tran); expect(fd.pos).toBe(origBuffer.length + 5); - }, []); + }); }); test('read all the data on the file iNode', async () => { // Allocate the size of the buffer to be read into @@ -119,33 +117,30 @@ describe('File Descriptor', () => { }); const fileIno = iNodeMgr.inoAllocate(); let atime; - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - origBuffer, - ); - atime = await iNodeMgr.statGetProp(tran, fileIno, 'atime'); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + origBuffer, + tran, + ); + atime = await iNodeMgr.statGetProp(fileIno, 'atime', tran); + }); const fd = new FileDescriptor(iNodeMgr, fileIno, 0); bytesRead = await fd.read(readBuffer); expect(fd.pos).toBe(origBuffer.length); expect(readBuffer).toStrictEqual(origBuffer); expect(bytesRead).toBe(readBuffer.length); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGetProp(tran, fileIno, 'atime'); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGetProp(fileIno, 'atime', tran); expect(stat.getTime()).toBeGreaterThan(atime.getTime()); }); }); @@ -158,26 +153,23 @@ describe('File Descriptor', () => { }); const fileIno = iNodeMgr.inoAllocate(); let atime; - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - origBuffer, - ); - atime = await iNodeMgr.statGetProp(tran, fileIno, 'atime'); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + origBuffer, + tran, + ); + atime = await iNodeMgr.statGetProp(fileIno, 'atime', tran); + }); const fd = new FileDescriptor(iNodeMgr, fileIno, 0); // Start reading from byte number 4 bytesRead = await fd.read(readBuffer, 4); @@ -186,8 +178,8 @@ describe('File Descriptor', () => { Buffer.from(' Buffer for File Descriptor'), ); expect(bytesRead).toBe(readBuffer.length); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGetProp(tran, fileIno, 'atime'); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGetProp(fileIno, 'atime', tran); expect(stat.getTime()).toBeGreaterThan(atime.getTime()); }); }); @@ -200,26 +192,23 @@ describe('File Descriptor', () => { }); const fileIno = iNodeMgr.inoAllocate(); let atime; - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - origBuffer, - ); - atime = await iNodeMgr.statGetProp(tran, fileIno, 'atime'); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + origBuffer, + tran, + ); + atime = await iNodeMgr.statGetProp(fileIno, 'atime', tran); + }); const fd = new FileDescriptor(iNodeMgr, fileIno, 0); // Return buffer is only 21 bytes and starts at byte 6 // so should only reach the 27th byte of the original buffer @@ -227,8 +216,8 @@ describe('File Descriptor', () => { expect(fd.pos).toBe(0); expect(returnBuffer).toStrictEqual(Buffer.from('uffer for File Descri')); expect(bytesRead).toBe(returnBuffer.length); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGetProp(tran, fileIno, 'atime'); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGetProp(fileIno, 'atime', tran); expect(stat.getTime()).toBeGreaterThan(atime.getTime()); }); }); @@ -240,35 +229,33 @@ describe('File Descriptor', () => { }); const fileIno = iNodeMgr.inoAllocate(); let mtime, ctime; - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - ); - const stat = await iNodeMgr.statGet(tran, fileIno); - mtime = stat['mtime'].getTime(); - ctime = stat['ctime'].getTime(); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + undefined, + tran, + ); + const stat = await iNodeMgr.statGet(fileIno, tran); + mtime = stat['mtime'].getTime(); + ctime = stat['ctime'].getTime(); + }); const fd = new FileDescriptor(iNodeMgr, fileIno, 0); bytesWritten = await fd.write(origBuffer); expect(fd.pos).toBe(origBuffer.length); expect(bytesWritten).toBe(origBuffer.length); await fd.read(returnBuffer, 0); expect(returnBuffer).toStrictEqual(origBuffer); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGet(tran, fileIno); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGet(fileIno, tran); expect(stat['mtime'].getTime()).toBeGreaterThan(mtime); expect(stat['ctime'].getTime()).toBeGreaterThan(ctime); expect(stat['size']).toBe(origBuffer.length); @@ -286,28 +273,25 @@ describe('File Descriptor', () => { }); const fileIno = iNodeMgr.inoAllocate(); let mtime, ctime; - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - origBuffer, - ); - const stat = await iNodeMgr.statGet(tran, fileIno); - mtime = stat['mtime'].getTime(); - ctime = stat['ctime'].getTime(); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + origBuffer, + tran, + ); + const stat = await iNodeMgr.statGet(fileIno, tran); + mtime = stat['mtime'].getTime(); + ctime = stat['ctime'].getTime(); + }); const fd = new FileDescriptor(iNodeMgr, fileIno, 0); // Overwrite the existing buffer at position 0 @@ -319,8 +303,8 @@ describe('File Descriptor', () => { expect(readBuffer).toStrictEqual( Buffer.from('Nice Buffer for File Descriptor'), ); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGet(tran, fileIno); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGet(fileIno, tran); expect(stat['mtime'].getTime()).toBeGreaterThan(mtime); expect(stat['ctime'].getTime()).toBeGreaterThan(ctime); expect(stat['size']).toBe(origBuffer.length); @@ -338,28 +322,25 @@ describe('File Descriptor', () => { }); const fileIno = iNodeMgr.inoAllocate(); let mtime, ctime; - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - origBuffer, - ); - const stat = await iNodeMgr.statGet(tran, fileIno); - mtime = stat['mtime'].getTime(); - ctime = stat['ctime'].getTime(); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + origBuffer, + tran, + ); + const stat = await iNodeMgr.statGet(fileIno, tran); + mtime = stat['mtime'].getTime(); + ctime = stat['ctime'].getTime(); + }); const fd = new FileDescriptor(iNodeMgr, fileIno, 0); // Overwrite the original buffer starting from byte 4 @@ -369,8 +350,8 @@ describe('File Descriptor', () => { expect(readBuffer).toStrictEqual( Buffer.from('Testing Buf for File Descriptor'), ); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGet(tran, fileIno); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGet(fileIno, tran); expect(stat['mtime'].getTime()).toBeGreaterThan(mtime); expect(stat['ctime'].getTime()).toBeGreaterThan(ctime); expect(stat['size']).toBe(origBuffer.length); @@ -388,28 +369,25 @@ describe('File Descriptor', () => { }); const fileIno = iNodeMgr.inoAllocate(); let mtime, ctime; - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - origBuffer, - ); - const stat = await iNodeMgr.statGet(tran, fileIno); - mtime = stat['mtime'].getTime(); - ctime = stat['ctime'].getTime(); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + origBuffer, + tran, + ); + const stat = await iNodeMgr.statGet(fileIno, tran); + mtime = stat['mtime'].getTime(); + ctime = stat['ctime'].getTime(); + }); const fd = new FileDescriptor(iNodeMgr, fileIno, 0); // Write to bytes on from the end of the data @@ -423,8 +401,8 @@ describe('File Descriptor', () => { await fd.read(readBuffer, origBuffer.length + 2); // This should be the buffer that was written expect(readBuffer).toEqual(writeBuffer); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGet(tran, fileIno); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGet(fileIno, tran); expect(stat['mtime'].getTime()).toBeGreaterThan(mtime); expect(stat['ctime'].getTime()).toBeGreaterThan(ctime); expect(stat['size']).toBe(origBuffer.length + writeBuffer.length + 2); @@ -444,28 +422,25 @@ describe('File Descriptor', () => { }); const fileIno = iNodeMgr.inoAllocate(); let mtime, ctime; - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - origBuffer, - ); - const stat = await iNodeMgr.statGet(tran, fileIno); - mtime = stat['mtime'].getTime(); - ctime = stat['ctime'].getTime(); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + origBuffer, + tran, + ); + const stat = await iNodeMgr.statGet(fileIno, tran); + mtime = stat['mtime'].getTime(); + ctime = stat['ctime'].getTime(); + }); const fd = new FileDescriptor(iNodeMgr, fileIno, constants.O_APPEND); // Appending data to a non full block which will exceed the block size @@ -477,8 +452,8 @@ describe('File Descriptor', () => { expect(readBuffer).toStrictEqual( Buffer.from('Test Buffer for File Descriptor Tests'), ); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGet(tran, fileIno); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGet(fileIno, tran); expect(stat['mtime'].getTime()).toBeGreaterThan(mtime); mtime = stat['mtime'].getTime(); expect(stat['ctime'].getTime()).toBeGreaterThan(ctime); @@ -499,8 +474,8 @@ describe('File Descriptor', () => { expect(readBuffer).toStrictEqual( Buffer.from('Test Buffer for File Descriptor Testssss'), ); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGet(tran, fileIno); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGet(fileIno, tran); expect(stat['mtime'].getTime()).toBeGreaterThan(mtime); mtime = stat['mtime'].getTime(); expect(stat['ctime'].getTime()).toBeGreaterThan(ctime); @@ -530,8 +505,8 @@ describe('File Descriptor', () => { expect(readBuffer).toStrictEqual( Buffer.from('Test Buffer for File Descriptor Testssss Tests'), ); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGet(tran, fileIno); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGet(fileIno, tran); expect(stat['mtime'].getTime()).toBeGreaterThan(mtime); expect(stat['ctime'].getTime()).toBeGreaterThan(ctime); expect(stat['size']).toBe( diff --git a/tests/fd/FileDescriptorManager.test.ts b/tests/fd/FileDescriptorManager.test.ts index 56125b1f..1ff20b5f 100644 --- a/tests/fd/FileDescriptorManager.test.ts +++ b/tests/fd/FileDescriptorManager.test.ts @@ -2,13 +2,11 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - import { DB } from '@matrixai/db'; import { INodeManager } from '@/inodes'; -import { FileDescriptorManager } from '@/fd'; -import { FileDescriptor } from '@/fd'; -import { permissions } from '@'; - +import FileDescriptorManager from '@/fd/FileDescriptorManager'; +import FileDescriptor from '@/fd/FileDescriptor'; +import * as permissions from '@/permissions'; import * as utils from '@/utils'; describe('File Descriptor Manager', () => { @@ -115,51 +113,39 @@ describe('File Descriptor Manager', () => { }); const fdMgr = new FileDescriptorManager(iNodeMgr); const rootIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, {}); - }, - [rootIno], - ); + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + }); + await iNodeMgr.dirCreate(rootIno, {}, undefined, tran); + }); const fileIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - 4096, - origBuffer, - ); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + 4096, + origBuffer, + tran, + ); + }); // The file is 'added' to the directory - await iNodeMgr.transact( - async (tran) => { - await iNodeMgr.dirSetEntry(tran, rootIno, 'file', fileIno); - }, - [rootIno, fileIno], - ); + await iNodeMgr.withTransactionF(rootIno, fileIno, async (tran) => { + await iNodeMgr.dirSetEntry(rootIno, 'file', fileIno, tran); + }); // The ref to the file iNode is made here const [fd, fdIndex] = await fdMgr.createFd(fileIno, 0); // The file is 'deleted' from the directory - await iNodeMgr.transact( - async (tran) => { - await iNodeMgr.dirUnsetEntry(tran, rootIno, 'file'); - }, - [rootIno, fileIno], - ); + await iNodeMgr.withTransactionF(rootIno, fileIno, async (tran) => { + await iNodeMgr.dirUnsetEntry(rootIno, 'file', tran); + }); bytesRead = await fd.read(readBuffer); expect(fd.pos).toBe(origBuffer.length); expect(readBuffer).toStrictEqual(origBuffer); diff --git a/tests/inodes/INodeManager.dir.test.ts b/tests/inodes/INodeManager.dir.test.ts index e21e5d75..e69f97dd 100644 --- a/tests/inodes/INodeManager.dir.test.ts +++ b/tests/inodes/INodeManager.dir.test.ts @@ -1,13 +1,12 @@ import type { INodeIndex } from '@/inodes/types'; - import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; -import { INodeManager } from '@/inodes'; +import INodeManager from '@/inodes/INodeManager'; import * as utils from '@/utils'; -import { permissions } from '@'; +import * as permissions from '@/permissions'; describe('INodeManager Directory', () => { const logger = new Logger('INodeManager Directory Test', LogLevel.WARN, [ @@ -47,46 +46,45 @@ describe('INodeManager Directory', () => { logger, }); const rootIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, { + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + }); + await iNodeMgr.dirCreate( + rootIno, + { mode: permissions.DEFAULT_ROOT_PERM, uid: permissions.DEFAULT_ROOT_UID, gid: permissions.DEFAULT_ROOT_GID, - }); - const stat = await iNodeMgr.statGet(tran, rootIno); - expect(stat['ino']).toBe(rootIno); - expect(stat.isDirectory()).toBe(true); - expect(stat['uid']).toBe(permissions.DEFAULT_ROOT_UID); - expect(stat['gid']).toBe(permissions.DEFAULT_ROOT_GID); - // Root directories should have nlink of 2 - expect(stat['nlink']).toBe(2); - // All timestamps should be the same at creation - expect(stat['atime']).toEqual(stat['mtime']); - expect(stat['mtime']).toEqual(stat['ctime']); - expect(stat['birthtime']).toEqual(stat['birthtime']); - }, - [rootIno], - ); - await iNodeMgr.transact( - async (tran) => { - const iNode = await iNodeMgr.get(tran, rootIno); - expect(iNode).toBeDefined(); - expect(iNode!).toEqual({ - ino: rootIno, - type: 'Directory', - gc: false, - }); - const stat = await iNodeMgr.statGet(tran, rootIno); - expect(stat['ino']).toBe(rootIno); - const rootIno_ = await iNodeMgr.dirGetEntry(tran, rootIno, '..'); - expect(rootIno_).toBe(rootIno); - }, - [rootIno], - ); + }, + undefined, + tran, + ); + const stat = await iNodeMgr.statGet(rootIno, tran); + expect(stat['ino']).toBe(rootIno); + expect(stat.isDirectory()).toBe(true); + expect(stat['uid']).toBe(permissions.DEFAULT_ROOT_UID); + expect(stat['gid']).toBe(permissions.DEFAULT_ROOT_GID); + // Root directories should have nlink of 2 + expect(stat['nlink']).toBe(2); + // All timestamps should be the same at creation + expect(stat['atime']).toEqual(stat['mtime']); + expect(stat['mtime']).toEqual(stat['ctime']); + expect(stat['birthtime']).toEqual(stat['birthtime']); + }); + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + const iNode = await iNodeMgr.get(rootIno, tran); + expect(iNode).toBeDefined(); + expect(iNode!).toEqual({ + ino: rootIno, + type: 'Directory', + gc: false, + }); + const stat = await iNodeMgr.statGet(rootIno, tran); + expect(stat['ino']).toBe(rootIno); + const rootIno_ = await iNodeMgr.dirGetEntry(rootIno, '..', tran); + expect(rootIno_).toBe(rootIno); + }); }); test('create subdirectory', async () => { const iNodeMgr = await INodeManager.createINodeManager({ @@ -94,47 +92,46 @@ describe('INodeManager Directory', () => { logger, }); const rootIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, { + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + }); + await iNodeMgr.dirCreate( + rootIno, + { mode: permissions.DEFAULT_ROOT_PERM, uid: permissions.DEFAULT_ROOT_UID, gid: permissions.DEFAULT_ROOT_GID, - }); - }, - [rootIno], - ); + }, + undefined, + tran, + ); + }); const childIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(childIno); - }); - await iNodeMgr.dirCreate( - tran, - childIno, - { - mode: permissions.DEFAULT_DIRECTORY_PERM, - }, - rootIno, - ); - await iNodeMgr.dirSetEntry(tran, rootIno, 'childdir', childIno); - }, - [rootIno, childIno], - ); - await iNodeMgr.transact(async (tran) => { - const childIno_ = await iNodeMgr.dirGetEntry(tran, rootIno, 'childdir'); + await iNodeMgr.withTransactionF(rootIno, childIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(childIno); + }); + await iNodeMgr.dirCreate( + childIno, + { + mode: permissions.DEFAULT_DIRECTORY_PERM, + }, + rootIno, + tran, + ); + await iNodeMgr.dirSetEntry(rootIno, 'childdir', childIno, tran); + }); + await iNodeMgr.withTransactionF(async (tran) => { + const childIno_ = await iNodeMgr.dirGetEntry(rootIno, 'childdir', tran); expect(childIno_).toBeDefined(); expect(childIno_).toBe(childIno); - const parentIno = await iNodeMgr.dirGetEntry(tran, childIno, '..'); + const parentIno = await iNodeMgr.dirGetEntry(childIno, '..', tran); expect(parentIno).toBeDefined(); expect(parentIno).toBe(rootIno); - const statParent = await iNodeMgr.statGet(tran, rootIno); + const statParent = await iNodeMgr.statGet(rootIno, tran); expect(statParent['nlink']).toBe(3); - const statChild = await iNodeMgr.statGet(tran, childIno); + const statChild = await iNodeMgr.statGet(childIno, tran); expect(statChild['nlink']).toBe(2); }); }); @@ -146,35 +143,31 @@ describe('INodeManager Directory', () => { const rootIno = iNodeMgr.inoAllocate(); const childIno1 = iNodeMgr.inoAllocate(); const childIno2 = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( + await iNodeMgr.withTransactionF( + rootIno, + childIno1, + childIno2, async (tran) => { tran.queueFailure(() => { iNodeMgr.inoDeallocate(rootIno); iNodeMgr.inoDeallocate(childIno1); iNodeMgr.inoDeallocate(childIno2); }); - await iNodeMgr.dirCreate(tran, rootIno, {}); - await iNodeMgr.dirCreate(tran, childIno1, {}, rootIno); - await iNodeMgr.dirCreate(tran, childIno2, {}, rootIno); + await iNodeMgr.dirCreate(rootIno, {}, undefined, tran); + await iNodeMgr.dirCreate(childIno1, {}, rootIno, tran); + await iNodeMgr.dirCreate(childIno2, {}, rootIno, tran); }, - [rootIno, childIno1, childIno2], ); await Promise.all([ - iNodeMgr.transact( - async (tran) => { - await iNodeMgr.dirSetEntry(tran, rootIno, 'child1', childIno1); - }, - [rootIno, childIno1], - ), - iNodeMgr.transact( - async (tran) => { - await iNodeMgr.dirSetEntry(tran, rootIno, 'child2', childIno2); - }, - [rootIno, childIno2], - ), + iNodeMgr.withTransactionF(rootIno, childIno1, async (tran) => { + await iNodeMgr.dirSetEntry(rootIno, 'child1', childIno1, tran); + }), + iNodeMgr.withTransactionF(rootIno, childIno2, async (tran) => { + await iNodeMgr.dirSetEntry(rootIno, 'child2', childIno2, tran); + }), ]); - await iNodeMgr.transact(async (tran) => { - const stat = await iNodeMgr.statGet(tran, rootIno); + await iNodeMgr.withTransactionF(async (tran) => { + const stat = await iNodeMgr.statGet(rootIno, tran); // If the rootIno locking wasn't done // this nlink would be clobbered by a race condition expect(stat['nlink']).toBe(4); @@ -187,42 +180,41 @@ describe('INodeManager Directory', () => { }); const rootIno = iNodeMgr.inoAllocate(); const childIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - iNodeMgr.inoDeallocate(childIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, { + await iNodeMgr.withTransactionF(rootIno, childIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + iNodeMgr.inoDeallocate(childIno); + }); + await iNodeMgr.dirCreate( + rootIno, + { mode: permissions.DEFAULT_ROOT_PERM, uid: permissions.DEFAULT_ROOT_UID, gid: permissions.DEFAULT_ROOT_GID, - }); - await iNodeMgr.dirCreate( - tran, - childIno, - { - mode: permissions.DEFAULT_DIRECTORY_PERM, - }, - rootIno, - ); - await iNodeMgr.dirSetEntry(tran, rootIno, 'childdir', childIno); - }, - [rootIno, childIno], - ); - await iNodeMgr.transact( - async (tran) => { - await iNodeMgr.dirUnsetEntry(tran, rootIno, 'childdir'); - }, - [rootIno, childIno], - ); - await iNodeMgr.transact(async (tran) => { - const statParent = await iNodeMgr.statGet(tran, rootIno); + }, + undefined, + tran, + ); + await iNodeMgr.dirCreate( + childIno, + { + mode: permissions.DEFAULT_DIRECTORY_PERM, + }, + rootIno, + tran, + ); + await iNodeMgr.dirSetEntry(rootIno, 'childdir', childIno, tran); + }); + await iNodeMgr.withTransactionF(rootIno, childIno, async (tran) => { + await iNodeMgr.dirUnsetEntry(rootIno, 'childdir', tran); + }); + await iNodeMgr.withTransactionF(async (tran) => { + const statParent = await iNodeMgr.statGet(rootIno, tran); expect(statParent['nlink']).toBe(2); expect( - await iNodeMgr.dirGetEntry(tran, rootIno, 'childdir'), + await iNodeMgr.dirGetEntry(rootIno, 'childdir', tran), ).toBeUndefined(); - expect(await iNodeMgr.get(tran, childIno)).toBeUndefined(); + expect(await iNodeMgr.get(childIno, tran)).toBeUndefined(); }); }); test('rename directory entry', async () => { @@ -231,53 +223,54 @@ describe('INodeManager Directory', () => { logger, }); const rootIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, {}); - }, - [rootIno], - ); + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + }); + await iNodeMgr.dirCreate(rootIno, {}, undefined, tran); + }); // We are going to rename over an existing inode const childIno1 = iNodeMgr.inoAllocate(); const childIno2 = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( + await iNodeMgr.withTransactionF( + rootIno, + childIno1, + childIno2, async (tran) => { tran.queueFailure(() => { iNodeMgr.inoDeallocate(childIno1); iNodeMgr.inoDeallocate(childIno2); }); - await iNodeMgr.dirCreate(tran, childIno1, {}, rootIno); - await iNodeMgr.dirCreate(tran, childIno2, {}, rootIno); - await iNodeMgr.dirSetEntry(tran, rootIno, 'child1', childIno1); - await iNodeMgr.dirSetEntry(tran, rootIno, 'child2', childIno2); + await iNodeMgr.dirCreate(childIno1, {}, rootIno, tran); + await iNodeMgr.dirCreate(childIno2, {}, rootIno, tran); + await iNodeMgr.dirSetEntry(rootIno, 'child1', childIno1, tran); + await iNodeMgr.dirSetEntry(rootIno, 'child2', childIno2, tran); }, - [rootIno, childIno1, childIno2], ); - await iNodeMgr.transact(async (tran) => { + await iNodeMgr.withTransactionF(async (tran) => { // Parent has 4 nlinks now - const statParent = await iNodeMgr.statGet(tran, rootIno); + const statParent = await iNodeMgr.statGet(rootIno, tran); expect(statParent['nlink']).toBe(4); }); - await iNodeMgr.transact( + await iNodeMgr.withTransactionF( + rootIno, + childIno1, + childIno2, async (tran) => { // Perform the renaming! - await iNodeMgr.dirResetEntry(tran, rootIno, 'child1', 'child2'); + await iNodeMgr.dirResetEntry(rootIno, 'child1', 'child2', tran); }, - [rootIno, childIno1, childIno2], ); - await iNodeMgr.transact(async (tran) => { - const statParent = await iNodeMgr.statGet(tran, rootIno); + await iNodeMgr.withTransactionF(async (tran) => { + const statParent = await iNodeMgr.statGet(rootIno, tran); expect(statParent['nlink']).toBe(3); - const childIno1_ = await iNodeMgr.dirGetEntry(tran, rootIno, 'child2'); + const childIno1_ = await iNodeMgr.dirGetEntry(rootIno, 'child2', tran); expect(childIno1_).toBeDefined(); expect(childIno1_).toBe(childIno1); expect( - await iNodeMgr.dirGetEntry(tran, rootIno, 'child1'), + await iNodeMgr.dirGetEntry(rootIno, 'child1', tran), ).toBeUndefined(); - expect(await iNodeMgr.get(tran, childIno2)).toBeUndefined(); + expect(await iNodeMgr.get(childIno2, tran)).toBeUndefined(); }); }); test('iterate directory entries', async () => { @@ -286,39 +279,35 @@ describe('INodeManager Directory', () => { logger, }); const rootIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, {}); - }, - [rootIno], - ); + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + }); + await iNodeMgr.dirCreate(rootIno, {}, undefined, tran); + }); const childIno1 = iNodeMgr.inoAllocate(); const childIno2 = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( + await iNodeMgr.withTransactionF( + rootIno, + childIno1, + childIno2, async (tran) => { tran.queueFailure(() => { iNodeMgr.inoDeallocate(childIno1); iNodeMgr.inoDeallocate(childIno2); }); - await iNodeMgr.dirCreate(tran, childIno1, {}, rootIno); - await iNodeMgr.dirCreate(tran, childIno2, {}, rootIno); - await iNodeMgr.dirSetEntry(tran, rootIno, 'child1', childIno1); - await iNodeMgr.dirSetEntry(tran, rootIno, 'child2', childIno2); + await iNodeMgr.dirCreate(childIno1, {}, rootIno, tran); + await iNodeMgr.dirCreate(childIno2, {}, rootIno, tran); + await iNodeMgr.dirSetEntry(rootIno, 'child1', childIno1, tran); + await iNodeMgr.dirSetEntry(rootIno, 'child2', childIno2, tran); }, - [rootIno, childIno1, childIno2], ); const entries: Array<[string, INodeIndex]> = []; - await iNodeMgr.transact( - async (tran) => { - for await (const [name, ino] of iNodeMgr.dirGet(tran, rootIno)) { - entries.push([name, ino]); - } - }, - [rootIno], - ); + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + for await (const [name, ino] of iNodeMgr.dirGet(rootIno, tran)) { + entries.push([name, ino]); + } + }); expect(entries).toContainEqual(['.', rootIno]); expect(entries).toContainEqual(['..', rootIno]); expect(entries).toContainEqual(['child1', childIno1]); diff --git a/tests/inodes/INodeManager.file.test.ts b/tests/inodes/INodeManager.file.test.ts index dc2d521f..d4994feb 100644 --- a/tests/inodes/INodeManager.file.test.ts +++ b/tests/inodes/INodeManager.file.test.ts @@ -3,9 +3,10 @@ import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; -import { INodeManager } from '@/inodes'; +import INodeManager from '@/inodes/INodeManager'; import * as utils from '@/utils'; -import { constants, permissions } from '@'; +import * as constants from '@/constants'; +import * as permissions from '@/permissions'; describe('INodeManager File', () => { const logger = new Logger('INodeManager File Test', LogLevel.WARN, [ @@ -47,12 +48,11 @@ describe('INodeManager File', () => { logger, }); const fileIno = iNodeMgr.inoAllocate(); - await db.transact(async (tran) => { + await iNodeMgr.withTransactionF(async (tran) => { tran.queueFailure(() => { iNodeMgr.inoDeallocate(fileIno); }); await iNodeMgr.fileCreate( - tran, fileIno, { mode: permissions.DEFAULT_FILE_PERM, @@ -60,8 +60,10 @@ describe('INodeManager File', () => { gid: permissions.DEFAULT_ROOT_GID, }, blockSize, + undefined, + tran, ); - const stat = await iNodeMgr.statGet(tran, fileIno); + const stat = await iNodeMgr.statGet(fileIno, tran); expect(stat['ino']).toBe(fileIno); expect(stat.isDirectory()).toBe(false); expect(stat['uid']).toBe(permissions.DEFAULT_ROOT_UID); @@ -86,64 +88,57 @@ describe('INodeManager File', () => { logger, }); const fileIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - buffer, - ); - const stat = await iNodeMgr.statGet(tran, fileIno); - expect(stat['ino']).toBe(fileIno); - expect(stat.isDirectory()).toBe(false); - expect(stat['uid']).toBe(permissions.DEFAULT_ROOT_UID); - expect(stat['gid']).toBe(permissions.DEFAULT_ROOT_GID); - // The size, blocks and block size should be set if data supplied - expect(stat['size']).toBe(buffer.length); - expect(stat['blksize']).toBe(5); - expect(stat['blocks']).toBe(3); - // All timestamps should be the same at creation - expect(stat['atime']).toEqual(stat['mtime']); - expect(stat['mtime']).toEqual(stat['ctime']); - expect(stat['birthtime']).toEqual(stat['birthtime']); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + buffer, + tran, + ); + const stat = await iNodeMgr.statGet(fileIno, tran); + expect(stat['ino']).toBe(fileIno); + expect(stat.isDirectory()).toBe(false); + expect(stat['uid']).toBe(permissions.DEFAULT_ROOT_UID); + expect(stat['gid']).toBe(permissions.DEFAULT_ROOT_GID); + // The size, blocks and block size should be set if data supplied + expect(stat['size']).toBe(buffer.length); + expect(stat['blksize']).toBe(5); + expect(stat['blocks']).toBe(3); + // All timestamps should be the same at creation + expect(stat['atime']).toEqual(stat['mtime']); + expect(stat['mtime']).toEqual(stat['ctime']); + expect(stat['birthtime']).toEqual(stat['birthtime']); + }); let counter = 0; // Alocate a buffer that will accept the blocks of the iNode const compareBuffer = Buffer.alloc(buffer.length); - await iNodeMgr.transact( - async (tran) => { - for await (const block of iNodeMgr.fileGetBlocks( - tran, - fileIno, - blockSize, - )) { - // Copy the blocks into the compare buffer - block.copy(compareBuffer, counter); - counter += blockSize; - } - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + for await (const block of iNodeMgr.fileGetBlocks( + fileIno, + blockSize, + undefined, + undefined, + tran, + )) { + // Copy the blocks into the compare buffer + block.copy(compareBuffer, counter); + counter += blockSize; + } + }); expect(compareBuffer).toStrictEqual(buffer); let idx, block; // Check that we can also correctly get the last block of the data - await iNodeMgr.transact( - async (tran) => { - [idx, block] = await iNodeMgr.fileGetLastBlock(tran, fileIno); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + [idx, block] = await iNodeMgr.fileGetLastBlock(fileIno, tran); + }); expect(idx).toBe(2); expect(block).toStrictEqual(Buffer.from('r')); }); @@ -153,40 +148,37 @@ describe('INodeManager File', () => { logger, }); const fileIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - ); - await iNodeMgr.fileSetBlocks(tran, fileIno, buffer, blockSize); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + undefined, + tran, + ); + await iNodeMgr.fileSetBlocks(fileIno, buffer, blockSize, undefined, tran); + }); let counter = 0; const compareBuffer = Buffer.alloc(buffer.length); - await iNodeMgr.transact( - async (tran) => { - for await (const block of iNodeMgr.fileGetBlocks( - tran, - fileIno, - blockSize, - )) { - block.copy(compareBuffer, counter); - counter += blockSize; - } - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + for await (const block of iNodeMgr.fileGetBlocks( + fileIno, + blockSize, + undefined, + undefined, + tran, + )) { + block.copy(compareBuffer, counter); + counter += blockSize; + } + }); expect(compareBuffer).toStrictEqual(buffer); }); test('read a single block from a file', async () => { @@ -195,77 +187,149 @@ describe('INodeManager File', () => { logger, }); const fileIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - buffer, - ); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + buffer, + tran, + ); + }); let blockComp; - await iNodeMgr.transact(async (tran) => { + await iNodeMgr.withTransactionF(async (tran) => { for await (const block of iNodeMgr.fileGetBlocks( - tran, fileIno, blockSize, 0, 1, + tran, )) { blockComp = block; } }); expect(blockComp).toStrictEqual(Buffer.from('Test ')); }); - test('write a single block from a file', async () => { + test('read sparse blocks from a file', async () => { + // Sparse blocks can occur when file descriptor write + // with a position that is beyond the end of the file + // Because there will be missing intermediate blocks, + // EncryptedFS will dynamically create zeroed-blocks to return + // Additionally this can also occur with ftruncate and fallocate + // However those operations may be implemented to just set zeroed-out buffers const iNodeMgr = await INodeManager.createINodeManager({ db, logger, }); + const data0to3 = Buffer.allocUnsafe(3 * blockSize).fill(0x01); + const data7to8 = Buffer.allocUnsafe(1 * blockSize).fill(0x02); + const data9to11 = Buffer.allocUnsafe(2 * blockSize).fill(0x03); const fileIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - ); - await iNodeMgr.fileSetBlocks(tran, fileIno, buffer, blockSize); - }, - [fileIno], + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + // Creates a file with blocks set for [0, 3) + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + data0to3, + tran, + ); + // Sets blocks for [7, 8) + await iNodeMgr.fileSetBlocks(fileIno, data7to8, blockSize, 7, tran); + }); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + // Zero out block 1 + await iNodeMgr.fileWriteBlock( + fileIno, + Buffer.alloc(blockSize), + 1, + 0, + tran, + ); + // Sets blocks for [9, 11) + await iNodeMgr.fileSetBlocks(fileIno, data9to11, blockSize, 9, tran); + }); + const blocks: Array = []; + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + // Blocks [3, 6] should be empty + for await (const block of iNodeMgr.fileGetBlocks( + fileIno, + blockSize, + 0, + undefined, + tran, + )) { + blocks.push(block); + } + }); + const blockComp = Buffer.concat(blocks); + expect(blockComp).toStrictEqual( + Buffer.concat([ + // [0, 3) (with zeroed out block 1) + data0to3.slice(0, 1 * blockSize), + Buffer.alloc(1 * blockSize), + data0to3.slice(0, 1 * blockSize), + // [3, 7) + Buffer.alloc(4 * blockSize), + // [7, 8) + data7to8, + // [8, 9) + Buffer.alloc(1 * blockSize), + // [9, 11) + data9to11, + ]), ); + }); + test('write a single block from a file', async () => { + const iNodeMgr = await INodeManager.createINodeManager({ + db, + logger, + }); + const fileIno = iNodeMgr.inoAllocate(); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + undefined, + tran, + ); + await iNodeMgr.fileSetBlocks(fileIno, buffer, blockSize, undefined, tran); + }); // Write the byte 'B' at the beginning of the first block - await iNodeMgr.transact(async (tran) => { - await iNodeMgr.fileWriteBlock(tran, fileIno, Buffer.from('B'), 0, 0); + await iNodeMgr.withTransactionF(async (tran) => { + await iNodeMgr.fileWriteBlock(fileIno, Buffer.from('B'), 0, 0, tran); }); let blockComp; // Obtain blocks which have an index greater than or equal to 0 and less than 1 - await iNodeMgr.transact(async (tran) => { + await iNodeMgr.withTransactionF(async (tran) => { for await (const block of iNodeMgr.fileGetBlocks( - tran, fileIno, blockSize, 0, 1, + tran, )) { blockComp = block; } @@ -278,35 +342,32 @@ describe('INodeManager File', () => { logger, }); const fileIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(fileIno); - }); - await iNodeMgr.fileCreate( - tran, - fileIno, - { - mode: permissions.DEFAULT_FILE_PERM, - uid: permissions.DEFAULT_ROOT_UID, - gid: permissions.DEFAULT_ROOT_GID, - }, - blockSize, - buffer, - ); - }, - [fileIno], - ); + await iNodeMgr.withTransactionF(fileIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(fileIno); + }); + await iNodeMgr.fileCreate( + fileIno, + { + mode: permissions.DEFAULT_FILE_PERM, + uid: permissions.DEFAULT_ROOT_UID, + gid: permissions.DEFAULT_ROOT_GID, + }, + blockSize, + buffer, + tran, + ); + }); let counter = 0; const compareBuffer = Buffer.alloc(buffer.length); - await iNodeMgr.transact(async (tran) => { + await iNodeMgr.withTransactionF(async (tran) => { // Database only has blocks 0, 1 and 2 but we take up to block 9 for await (const block of iNodeMgr.fileGetBlocks( - tran, fileIno, blockSize, 0, 10, + tran, )) { block.copy(compareBuffer, counter); counter += blockSize; diff --git a/tests/inodes/INodeManager.symlink.test.ts b/tests/inodes/INodeManager.symlink.test.ts index ef1c7d37..54bd953c 100644 --- a/tests/inodes/INodeManager.symlink.test.ts +++ b/tests/inodes/INodeManager.symlink.test.ts @@ -3,7 +3,7 @@ import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; -import { INodeManager } from '@/inodes'; +import INodeManager from '@/inodes/INodeManager'; import * as utils from '@/utils'; describe('INodeManager Symlink', () => { @@ -43,44 +43,35 @@ describe('INodeManager Symlink', () => { logger, }); const rootIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, {}); - }, - [rootIno], - ); + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + }); + await iNodeMgr.dirCreate(rootIno, {}, undefined, tran); + }); const symlinkIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(symlinkIno); - }); - await iNodeMgr.symlinkCreate(tran, symlinkIno, {}, 'a link'); - await iNodeMgr.dirSetEntry(tran, rootIno, 'somelink', symlinkIno); - }, - [rootIno, symlinkIno], - ); - await iNodeMgr.transact(async (tran) => { - expect(await iNodeMgr.symlinkGetLink(tran, symlinkIno)).toBe('a link'); - const statSymlink = await iNodeMgr.statGet(tran, symlinkIno); + await iNodeMgr.withTransactionF(rootIno, symlinkIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(symlinkIno); + }); + await iNodeMgr.symlinkCreate(symlinkIno, {}, 'a link', tran); + await iNodeMgr.dirSetEntry(rootIno, 'somelink', symlinkIno, tran); + }); + await iNodeMgr.withTransactionF(async (tran) => { + expect(await iNodeMgr.symlinkGetLink(symlinkIno, tran)).toBe('a link'); + const statSymlink = await iNodeMgr.statGet(symlinkIno, tran); expect(statSymlink.isSymbolicLink()).toBe(true); expect(statSymlink['ino']).toBe(symlinkIno); expect(statSymlink['nlink']).toBe(1); - const statDir = await iNodeMgr.statGet(tran, rootIno); + const statDir = await iNodeMgr.statGet(rootIno, tran); expect(statDir['nlink']).toBe(2); }); - await iNodeMgr.transact( - async (tran) => { - await iNodeMgr.dirUnsetEntry(tran, rootIno, 'somelink'); - }, - [rootIno, symlinkIno], - ); - await iNodeMgr.transact(async (tran) => { - expect(await iNodeMgr.get(tran, symlinkIno)).toBeUndefined(); - const stat = await iNodeMgr.statGet(tran, rootIno); + await iNodeMgr.withTransactionF(rootIno, symlinkIno, async (tran) => { + await iNodeMgr.dirUnsetEntry(rootIno, 'somelink', tran); + }); + await iNodeMgr.withTransactionF(async (tran) => { + expect(await iNodeMgr.get(symlinkIno, tran)).toBeUndefined(); + const stat = await iNodeMgr.statGet(rootIno, tran); expect(stat['nlink']).toBe(2); }); }); diff --git a/tests/inodes/INodeManager.test.ts b/tests/inodes/INodeManager.test.ts index 35a9f991..e6412d86 100644 --- a/tests/inodes/INodeManager.test.ts +++ b/tests/inodes/INodeManager.test.ts @@ -3,9 +3,9 @@ import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; -import { INodeManager } from '@/inodes'; +import INodeManager from '@/inodes/INodeManager'; import * as utils from '@/utils'; -import { permissions } from '@'; +import * as permissions from '@/permissions'; describe('INodeManager', () => { const logger = new Logger('INodeManager Test', LogLevel.WARN, [ @@ -44,18 +44,15 @@ describe('INodeManager', () => { }); const rootIno = iNodeMgr.inoAllocate(); const childIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - iNodeMgr.inoDeallocate(childIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, {}); - await iNodeMgr.dirCreate(tran, childIno, {}, rootIno); - await iNodeMgr.dirSetEntry(tran, rootIno, 'childdir', childIno); - }, - [rootIno, childIno], - ); + await iNodeMgr.withTransactionF(rootIno, childIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + iNodeMgr.inoDeallocate(childIno); + }); + await iNodeMgr.dirCreate(rootIno, {}, undefined, tran); + await iNodeMgr.dirCreate(childIno, {}, rootIno, tran); + await iNodeMgr.dirSetEntry(rootIno, 'childdir', childIno, tran); + }); await db.stop(); db = await DB.createDB({ dbPath: `${dataDir}/db`, @@ -72,11 +69,11 @@ describe('INodeManager', () => { db, logger, }); - await iNodeMgr.transact(async (tran) => { + await iNodeMgr.withTransactionF(async (tran) => { const rootIno_ = await iNodeMgr.dirGetRoot(tran); expect(rootIno_).toBeDefined(); expect(rootIno_).toBe(rootIno); - const childIno_ = await iNodeMgr.dirGetEntry(tran, rootIno, 'childdir'); + const childIno_ = await iNodeMgr.dirGetEntry(rootIno, 'childdir', tran); expect(childIno_).toBeDefined(); expect(childIno_).toBe(childIno); }); @@ -87,47 +84,41 @@ describe('INodeManager', () => { logger, }); // Demonstrate a counter increment race condition - await iNodeMgr.transact(async (tran) => { - await tran.put(iNodeMgr.mgrDomain, 'test', 0); + await iNodeMgr.withTransactionF(async (tran) => { + await tran.put([...iNodeMgr.mgrDbPath, 'test'], 0); }); await Promise.all([ - iNodeMgr.transact(async (tran) => { - const num = (await tran.get(iNodeMgr.mgrDomain, 'test'))!; - await tran.put(iNodeMgr.mgrDomain, 'test', num + 1); + iNodeMgr.withTransactionF(async (tran) => { + const num = (await tran.get([...iNodeMgr.mgrDbPath, 'test']))!; + await tran.put([...iNodeMgr.mgrDbPath, 'test'], num + 1); }), - iNodeMgr.transact(async (tran) => { - const num = (await tran.get(iNodeMgr.mgrDomain, 'test'))!; - await tran.put(iNodeMgr.mgrDomain, 'test', num + 1); + iNodeMgr.withTransactionF(async (tran) => { + const num = (await tran.get([...iNodeMgr.mgrDbPath, 'test']))!; + await tran.put([...iNodeMgr.mgrDbPath, 'test'], num + 1); }), ]); - await iNodeMgr.transact(async (tran) => { - const num = (await tran.get(iNodeMgr.mgrDomain, 'test'))!; + await iNodeMgr.withTransactionF(async (tran) => { + const num = (await tran.get([...iNodeMgr.mgrDbPath, 'test']))!; // Race condition clobbers the counter expect(num).toBe(1); }); // Now with proper locking, the race condition doesn't happen - await iNodeMgr.transact(async (tran) => { - await tran.put(iNodeMgr.mgrDomain, 'test', 0); + await iNodeMgr.withTransactionF(async (tran) => { + await tran.put([...iNodeMgr.mgrDbPath, 'test'], 0); }); const ino = iNodeMgr.inoAllocate(); await Promise.all([ - iNodeMgr.transact( - async (tran) => { - const num = (await tran.get(iNodeMgr.mgrDomain, 'test'))!; - await tran.put(iNodeMgr.mgrDomain, 'test', num + 1); - }, - [ino], - ), - iNodeMgr.transact( - async (tran) => { - const num = (await tran.get(iNodeMgr.mgrDomain, 'test'))!; - await tran.put(iNodeMgr.mgrDomain, 'test', num + 1); - }, - [ino], - ), + iNodeMgr.withTransactionF(ino, async (tran) => { + const num = (await tran.get([...iNodeMgr.mgrDbPath, 'test']))!; + await tran.put([...iNodeMgr.mgrDbPath, 'test'], num + 1); + }), + iNodeMgr.withTransactionF(ino, async (tran) => { + const num = (await tran.get([...iNodeMgr.mgrDbPath, 'test']))!; + await tran.put([...iNodeMgr.mgrDbPath, 'test'], num + 1); + }), ]); - await iNodeMgr.transact(async (tran) => { - const num = (await tran.get(iNodeMgr.mgrDomain, 'test'))!; + await iNodeMgr.withTransactionF(async (tran) => { + const num = (await tran.get([...iNodeMgr.mgrDbPath, 'test']))!; // Race condition is solved by the locking the ino expect(num).toBe(2); }); @@ -138,55 +129,51 @@ describe('INodeManager', () => { logger, }); const rootIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(rootIno); - }); - await iNodeMgr.dirCreate(tran, rootIno, { + await iNodeMgr.withTransactionF(rootIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(rootIno); + }); + await iNodeMgr.dirCreate( + rootIno, + { mode: permissions.DEFAULT_ROOT_PERM, uid: permissions.DEFAULT_ROOT_UID, gid: permissions.DEFAULT_ROOT_GID, - }); - }, - [rootIno], - ); + }, + undefined, + tran, + ); + }); const childIno = iNodeMgr.inoAllocate(); - await iNodeMgr.transact( - async (tran) => { - tran.queueFailure(() => { - iNodeMgr.inoDeallocate(childIno); - }); - await iNodeMgr.dirCreate( - tran, - childIno, - { - mode: permissions.DEFAULT_DIRECTORY_PERM, - }, - rootIno, - ); - await iNodeMgr.dirSetEntry(tran, rootIno, 'childdir', childIno); - }, - [rootIno, childIno], - ); - await iNodeMgr.transact( - async (tran) => { - iNodeMgr.ref(childIno); - await iNodeMgr.dirUnsetEntry(tran, rootIno, 'childdir'); - }, - [rootIno, childIno], - ); - await iNodeMgr.transact(async (tran) => { - const data = await iNodeMgr.get(tran, childIno); + await iNodeMgr.withTransactionF(rootIno, childIno, async (tran) => { + tran.queueFailure(() => { + iNodeMgr.inoDeallocate(childIno); + }); + await iNodeMgr.dirCreate( + childIno, + { + mode: permissions.DEFAULT_DIRECTORY_PERM, + }, + rootIno, + tran, + ); + await iNodeMgr.dirSetEntry(rootIno, 'childdir', childIno, tran); + }); + await iNodeMgr.withTransactionF(rootIno, childIno, async (tran) => { + iNodeMgr.ref(childIno); + await iNodeMgr.dirUnsetEntry(rootIno, 'childdir', tran); + }); + await iNodeMgr.withTransactionF(async (tran) => { + const data = await iNodeMgr.get(childIno, tran); expect(data).toBeDefined(); expect(data!.gc).toBe(true); expect(data!.ino).toBe(childIno); }); - await iNodeMgr.transact(async (tran) => { - await iNodeMgr.unref(tran, childIno); + await iNodeMgr.withTransactionF(async (tran) => { + await iNodeMgr.unref(childIno, tran); }); - await iNodeMgr.transact(async (tran) => { - const data = await iNodeMgr.get(tran, childIno); + await iNodeMgr.withTransactionF(async (tran) => { + const data = await iNodeMgr.get(childIno, tran); expect(data).toBeUndefined(); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 1541fc80..0602105f 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,15 +1,43 @@ import type EncryptedFS from '@/EncryptedFS'; -import { constants } from '@'; +import * as constants from '@/constants'; /** - * - * @param promise - the Promise that throws the expected error. - * @param code - Error code such as 'errno.ENOTDIR' + * Checks if asynchronous operation throws an exception */ -async function expectError(promise: Promise, code) { - await expect(promise).rejects.toThrow(); - await expect(promise).rejects.toHaveProperty('code', code.code); - await expect(promise).rejects.toHaveProperty('errno', code.errno); +async function expectError( + p: Promise, + exception: new (...params: Array) => Error = Error, + errno?: { + errno?: number; + code?: string; + description?: string; + }, +): Promise { + await expect(p).rejects.toThrow(exception); + if (errno != null) { + await expect(p).rejects.toHaveProperty('code', errno.code); + await expect(p).rejects.toHaveProperty('errno', errno.errno); + await expect(p).rejects.toHaveProperty('description', errno.description); + } +} + +function expectReason( + result: PromiseSettledResult, + exception: new (...params: Array) => Error = Error, + errno?: { + errno?: number; + code?: string; + description?: string; + }, +): void { + expect(result.status).toBe('rejected'); + if (result.status === 'fulfilled') throw Error('never'); // Let typescript know the status + expect(result.reason).toBeInstanceOf(exception); + if (errno != null) { + expect(result.reason).toHaveProperty('code', errno.code); + expect(result.reason).toHaveProperty('errno', errno.errno); + expect(result.reason).toHaveProperty('description', errno.description); + } } type FileTypes = 'none' | 'regular' | 'dir' | 'block' | 'symlink'; @@ -73,6 +101,6 @@ function setId(efs: EncryptedFS, uid: number, gid?: number) { efs.gid = gid ?? uid; } -export { expectError, createFile, supportedTypes, sleep, setId }; +export { expectError, expectReason, createFile, supportedTypes, sleep, setId }; export type { FileTypes };