From 0573cd47b2fa7a22a23bc733a4397edd1b79539d Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 12 Feb 2025 14:17:53 +1100 Subject: [PATCH 01/19] chore: updating to new templating --- .editorconfig | 8 + .eslintrc | 178 + .github/workflows/feature.yml | 19 + .github/workflows/staging.yml | 26 + .github/workflows/tag.yml | 16 + .gitignore | 132 +- .npmignore | 15 + .npmrc | 2 + .prettierrc | 7 + .travis.yml | 3 - LICENSE | 21 - READMEOTHER.md | 45 - index.js | 367 - jest.config.js | 81 + lib/VirtualTar.js | 29 - mytarball.tar | Bin 4096 -> 0 bytes package-lock.json | 10563 ++++++++++++++++++++++------ package.json | 77 +- shell.nix | 9 - src/index.ts | 1 + test/fixtures/a/hello.txt | 1 - test/fixtures/b/a/test.txt | 1 - test/fixtures/c/.gitignore | 1 - test/fixtures/d/file1 | 0 test/fixtures/d/file2 | 0 test/fixtures/d/sub-dir/file5 | 0 test/fixtures/d/sub-files/file3 | 0 test/fixtures/d/sub-files/file4 | 0 test/fixtures/e/directory/.ignore | 0 test/fixtures/e/file | 0 test/fixtures/e/symlink | 1 - test/fixtures/invalid.tar | Bin 2560 -> 0 bytes test/index.js | 346 - tsconfig.build.json | 13 + tsconfig.json | 37 + 35 files changed, 8865 insertions(+), 3134 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc create mode 100644 .github/workflows/feature.yml create mode 100644 .github/workflows/staging.yml create mode 100644 .github/workflows/tag.yml create mode 100644 .npmignore create mode 100644 .npmrc create mode 100644 .prettierrc delete mode 100644 .travis.yml delete mode 100644 LICENSE delete mode 100644 READMEOTHER.md delete mode 100644 index.js create mode 100644 jest.config.js delete mode 100644 lib/VirtualTar.js delete mode 100644 mytarball.tar delete mode 100644 shell.nix create mode 100644 src/index.ts delete mode 100644 test/fixtures/a/hello.txt delete mode 100644 test/fixtures/b/a/test.txt delete mode 100644 test/fixtures/c/.gitignore delete mode 100644 test/fixtures/d/file1 delete mode 100644 test/fixtures/d/file2 delete mode 100644 test/fixtures/d/sub-dir/file5 delete mode 100644 test/fixtures/d/sub-files/file3 delete mode 100644 test/fixtures/d/sub-files/file4 delete mode 100644 test/fixtures/e/directory/.ignore delete mode 100644 test/fixtures/e/file delete mode 120000 test/fixtures/e/symlink delete mode 100644 test/fixtures/invalid.tar delete mode 100644 test/index.js create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f3245e7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..44a8d5a --- /dev/null +++ b/.eslintrc @@ -0,0 +1,178 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es2021": true, + "node": true, + "jest": true + }, + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "prettier" + ], + "plugins": [ + "import" + ], + "parserOptions": { + "project": "tsconfig.json", + "sourceType": "module" + }, + "rules": { + "linebreak-style": ["error", "unix"], + "no-empty": 1, + "no-useless-catch": 1, + "no-prototype-builtins": 1, + "no-constant-condition": 0, + "no-useless-escape": 0, + "no-console": "error", + "no-restricted-globals": [ + "error", + { + "name": "global", + "message": "Use `globalThis` instead" + }, + { + "name": "window", + "message": "Use `globalThis` instead" + } + ], + "require-yield": 0, + "eqeqeq": ["error", "smart"], + "spaced-comment": [ + "warn", + "always", + { + "line": { + "exceptions": ["-"] + }, + "block": { + "exceptions": ["*"] + }, + "markers": ["/"] + } + ], + "capitalized-comments": [ + "warn", + "always", + { + "ignoreInlineComments": true, + "ignoreConsecutiveComments": true + } + ], + "curly": [ + "error", + "multi-line", + "consistent" + ], + "import/order": [ + "error", + { + "groups": [ + "type", + "builtin", + "external", + "internal", + "index", + "sibling", + "parent", + "object" + ], + "pathGroups": [ + { + "pattern": "@", + "group": "internal" + }, + { + "pattern": "@/**", + "group": "internal" + } + ], + "pathGroupsExcludedImportTypes": [ + "type" + ], + "newlines-between": "never" + } + ], + "@typescript-eslint/no-namespace": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "varsIgnorePattern": "^_", + "argsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-inferrable-types": 0, + "@typescript-eslint/no-non-null-assertion": 0, + "@typescript-eslint/no-this-alias": 0, + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-empty-interface": 0, + "@typescript-eslint/consistent-type-imports": ["error"], + "@typescript-eslint/consistent-type-exports": ["error"], + "no-throw-literal": "off", + "@typescript-eslint/no-throw-literal": "off", + "@typescript-eslint/no-floating-promises": ["error", { + "ignoreVoid": true, + "ignoreIIFE": true + }], + "@typescript-eslint/no-misused-promises": ["error", { + "checksVoidReturn": false + }], + "@typescript-eslint/await-thenable": ["error"], + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "default", + "format": ["camelCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "function", + "format": ["camelCase", "PascalCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "variable", + "format": ["camelCase", "UPPER_CASE", "PascalCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "parameter", + "format": ["camelCase"], + "leadingUnderscore": "allow", + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "typeLike", + "format": ["PascalCase"], + "trailingUnderscore": "allowSingleOrDouble" + }, + { + "selector": "enumMember", + "format": ["PascalCase", "UPPER_CASE"] + }, + { + "selector": "objectLiteralProperty", + "format": null + }, + { + "selector": "typeProperty", + "format": null + } + ], + "@typescript-eslint/ban-ts-comment": [ + "error", + { + "ts-ignore": "allow-with-description" + } + ] + } +} diff --git a/.github/workflows/feature.yml b/.github/workflows/feature.yml new file mode 100644 index 0000000..596cfd2 --- /dev/null +++ b/.github/workflows/feature.yml @@ -0,0 +1,19 @@ +name: "CI / Feature" + +on: + push: + branches: + - feature* + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + use-library-js-feature: + permissions: + contents: read + actions: write + checks: write + uses: MatrixAI/.github/.github/workflows/library-js-feature.yml@master diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml new file mode 100644 index 0000000..61843ee --- /dev/null +++ b/.github/workflows/staging.yml @@ -0,0 +1,26 @@ +name: "CI / Staging" + +on: + push: + branches: + - staging + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + use-library-js-staging: + permissions: + contents: write + actions: write + checks: write + pull-requests: write + uses: MatrixAI/.github/.github/workflows/library-js-staging.yml@master + secrets: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} + GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} + GIT_COMMITTER_EMAIL: ${{ secrets.GIT_COMMITTER_EMAIL }} + GIT_COMMITTER_NAME: ${{ secrets.GIT_COMMITTER_NAME }} diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml new file mode 100644 index 0000000..8965caf --- /dev/null +++ b/.github/workflows/tag.yml @@ -0,0 +1,16 @@ +name: "CI / Tag" + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + +jobs: + use-library-js-tag: + permissions: + contents: read + actions: write + uses: MatrixAI/.github/.github/workflows/library-js-tag.yml@master + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 4674b2c..84856b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,128 @@ -node_modules -test/fixtures/copy -test/fixtures/invalid -test/fixtures/outside +/tmp +/dist +.env* +!.env.example +# nix +/result* +/builds +# node-gyp +/build +# prebuildify +/prebuilds + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..6e59877 --- /dev/null +++ b/.npmignore @@ -0,0 +1,15 @@ +.* +/*.nix +/nix +/tsconfig.json +/tsconfig.build.json +/jest.config.js +/scripts +/src +/tests +/tmp +/docs +/benches +/build +/builds +/dist/tsbuildinfo diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..7c06da2 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +# Enables npm link +prefix=~/.npm diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..fa9699b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2 +} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6e5919d..0000000 --- a/.travis.yml +++ /dev/null @@ -1,3 +0,0 @@ -language: node_js -node_js: - - "0.10" diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 757562e..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Mathias Buus - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file diff --git a/READMEOTHER.md b/READMEOTHER.md deleted file mode 100644 index 20f4d69..0000000 --- a/READMEOTHER.md +++ /dev/null @@ -1,45 +0,0 @@ -# VirtualTar - -This only exposes `extract` and `pack`. - - -You can `pack()` a directory and pipe it as stream. - -You can pipe a stream into `tar.extract()`. - -So it just uses `tar-stream`. But what about direct access to the tar stream? - -When you pack, it accepts a bunch of callbacks. - -And it uses the `fs`, but we want it to accept a `fs` parameter, preferably with `VirtualFS`. - -So it can be isolated, and if we can have direct access to the `tar-stream` too. - -``` -ignore -``` - -The `pack` takes `cwd` and `opts`. - -Oh it takes the `fs` parameter. Cool. So it's just `xfs = opts.fs || fs`. So if we pass it a virtualfs. Then we can just create it there then! - -Wait... it's still using `fs`... Oh... -Ok so all we need to do is ignore fs. - -``` -var vfs = require('virtualfs'); -var tar = require('./index.js'); - -var fs = new vfs.VirtualFS; - -fs.mkdirSync('/dir'); -fs.writeFileSync('/dir/a', 'abc'); - -tar.pack('/dir', { - fs: fs -}); -``` - -Ok so this works. We just need to pass our vfs into it. - -And everything else is fine. diff --git a/index.js b/index.js deleted file mode 100644 index 4c870a7..0000000 --- a/index.js +++ /dev/null @@ -1,367 +0,0 @@ -import pathNode from 'path'; -import tar from 'tar-stream'; -import vfs from 'virtualfs'; - -// prefer posix join -const pathJoin = (pathNode.posix) ? pathNode.posix.join : pathNode.join; - - -// we already have mkdirp -// no need for it - - -// the default is more improtant there - - - - -// we need to implement chownr into the virtualfs -// it's recursive -// so we keep going down - - -// var chownr = require('chownr') -// var tar = require('tar-stream') -// var pump = require('pump') -// var mkdirp = require('mkdirp') -// var path = require('path') -// var os = require('os') - - -var win32 = os.platform() === 'win32' - -var noop = function () {} - -var echo = function (name) { - return name -} - -var normalize = !win32 ? echo : function (name) { - return name.replace(/\\/g, '/').replace(/[:?<>|]/g, '_') -} - -var statAll = function (fs, stat, cwd, ignore, entries, sort) { - var queue = entries || ['.'] - - return function loop (callback) { - if (!queue.length) return callback() - var next = queue.shift() - var nextAbs = path.join(cwd, next) - - stat(nextAbs, function (err, stat) { - if (err) return callback(err) - - if (!stat.isDirectory()) return callback(null, next, stat) - - fs.readdir(nextAbs, function (err, files) { - if (err) return callback(err) - - if (sort) files.sort() - for (var i = 0; i < files.length; i++) { - if (!ignore(path.join(cwd, next, files[i]))) queue.push(path.join(next, files[i])) - } - - callback(null, next, stat) - }) - }) - } -} - -var strip = function (map, level) { - return function (header) { - header.name = header.name.split('/').slice(level).join('/') - - var linkname = header.linkname - if (linkname && (header.type === 'link' || path.isAbsolute(linkname))) { - header.linkname = linkname.split('/').slice(level).join('/') - } - - return map(header) - } -} - -exports.pack = function (cwd, opts) { - if (!cwd) cwd = '.' - if (!opts) opts = {} - - var xfs = opts.fs || fs - var ignore = opts.ignore || opts.filter || noop - var map = opts.map || noop - var mapStream = opts.mapStream || echo - var statNext = statAll(xfs, opts.dereference ? xfs.stat : xfs.lstat, cwd, ignore, opts.entries, opts.sort) - var strict = opts.strict !== false - var umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask() - var dmode = typeof opts.dmode === 'number' ? opts.dmode : 0 - var fmode = typeof opts.fmode === 'number' ? opts.fmode : 0 - var pack = opts.pack || tar.pack() - var finish = opts.finish || noop - - if (opts.strip) map = strip(map, opts.strip) - - if (opts.readable) { - dmode |= parseInt(555, 8) - fmode |= parseInt(444, 8) - } - if (opts.writable) { - dmode |= parseInt(333, 8) - fmode |= parseInt(222, 8) - } - - var onsymlink = function (filename, header) { - xfs.readlink(path.join(cwd, filename), function (err, linkname) { - if (err) return pack.destroy(err) - header.linkname = normalize(linkname) - pack.entry(header, onnextentry) - }) - } - - var onstat = function (err, filename, stat) { - if (err) return pack.destroy(err) - if (!filename) { - if (opts.finalize !== false) pack.finalize() - return finish(pack) - } - - if (stat.isSocket()) return onnextentry() // tar does not support sockets... - - var header = { - name: normalize(filename), - mode: (stat.mode | (stat.isDirectory() ? dmode : fmode)) & umask, - mtime: stat.mtime, - size: stat.size, - type: 'file', - uid: stat.uid, - gid: stat.gid - } - - if (stat.isDirectory()) { - header.size = 0 - header.type = 'directory' - header = map(header) || header - return pack.entry(header, onnextentry) - } - - if (stat.isSymbolicLink()) { - header.size = 0 - header.type = 'symlink' - header = map(header) || header - return onsymlink(filename, header) - } - - // TODO: add fifo etc... - - header = map(header) || header - - if (!stat.isFile()) { - if (strict) return pack.destroy(new Error('unsupported type for ' + filename)) - return onnextentry() - } - - var entry = pack.entry(header, onnextentry) - if (!entry) return - - var rs = mapStream(xfs.createReadStream(path.join(cwd, filename)), header) - - rs.on('error', function (err) { // always forward errors on destroy - entry.destroy(err) - }) - - pump(rs, entry) - } - - var onnextentry = function (err) { - if (err) return pack.destroy(err) - statNext(onstat) - } - - onnextentry() - - return pack -} - -var head = function (list) { - return list.length ? list[list.length - 1] : null -} - -var processGetuid = function () { - return process.getuid ? process.getuid() : -1 -} - -var processUmask = function () { - return process.umask ? process.umask() : 0 -} - -exports.extract = function (cwd, opts) { - if (!cwd) cwd = '.' - if (!opts) opts = {} - - var xfs = opts.fs || fs - var ignore = opts.ignore || opts.filter || noop - var map = opts.map || noop - var mapStream = opts.mapStream || echo - var own = opts.chown !== false && !win32 && processGetuid() === 0 - var extract = opts.extract || tar.extract() - var stack = [] - var now = new Date() - var umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask() - var dmode = typeof opts.dmode === 'number' ? opts.dmode : 0 - var fmode = typeof opts.fmode === 'number' ? opts.fmode : 0 - var strict = opts.strict !== false - - if (opts.strip) map = strip(map, opts.strip) - - if (opts.readable) { - dmode |= parseInt(555, 8) - fmode |= parseInt(444, 8) - } - if (opts.writable) { - dmode |= parseInt(333, 8) - fmode |= parseInt(222, 8) - } - - var utimesParent = function (name, cb) { // we just set the mtime on the parent dir again everytime we write an entry - var top - while ((top = head(stack)) && name.slice(0, top[0].length) !== top[0]) stack.pop() - if (!top) return cb() - xfs.utimes(top[0], now, top[1], cb) - } - - var utimes = function (name, header, cb) { - if (opts.utimes === false) return cb() - - if (header.type === 'directory') return xfs.utimes(name, now, header.mtime, cb) - if (header.type === 'symlink') return utimesParent(name, cb) // TODO: how to set mtime on link? - - xfs.utimes(name, now, header.mtime, function (err) { - if (err) return cb(err) - utimesParent(name, cb) - }) - } - - var chperm = function (name, header, cb) { - var link = header.type === 'symlink' - var chmod = link ? xfs.lchmod : xfs.chmod - var chown = link ? xfs.lchown : xfs.chown - - if (!chmod) return cb() - - var mode = (header.mode | (header.type === 'directory' ? dmode : fmode)) & umask - chmod(name, mode, function (err) { - if (err) return cb(err) - if (!own) return cb() - if (!chown) return cb() - chown(name, header.uid, header.gid, cb) - }) - } - - extract.on('entry', function (header, stream, next) { - header = map(header) || header - header.name = normalize(header.name) - var name = path.join(cwd, path.join('/', header.name)) - - if (ignore(name, header)) { - stream.resume() - return next() - } - - var stat = function (err) { - if (err) return next(err) - utimes(name, header, function (err) { - if (err) return next(err) - if (win32) return next() - chperm(name, header, next) - }) - } - - var onsymlink = function () { - if (win32) return next() // skip symlinks on win for now before it can be tested - xfs.unlink(name, function () { - xfs.symlink(header.linkname, name, stat) - }) - } - - var onlink = function () { - if (win32) return next() // skip links on win for now before it can be tested - xfs.unlink(name, function () { - var srcpath = path.join(cwd, path.join('/', header.linkname)) - - xfs.link(srcpath, name, function (err) { - if (err && err.code === 'EPERM' && opts.hardlinkAsFilesFallback) { - stream = xfs.createReadStream(srcpath) - return onfile() - } - - stat(err) - }) - }) - } - - var onfile = function () { - var ws = xfs.createWriteStream(name) - var rs = mapStream(stream, header) - - ws.on('error', function (err) { // always forward errors on destroy - rs.destroy(err) - }) - - pump(rs, ws, function (err) { - if (err) return next(err) - ws.on('close', stat) - }) - } - - if (header.type === 'directory') { - stack.push([name, header.mtime]) - return mkdirfix(name, { - fs: xfs, own: own, uid: header.uid, gid: header.gid - }, stat) - } - - var dir = path.dirname(name) - - validate(xfs, dir, path.join(cwd, '.'), function (err, valid) { - if (err) return next(err) - if (!valid) return next(new Error(dir + ' is not a valid path')) - - mkdirfix(dir, { - fs: xfs, own: own, uid: header.uid, gid: header.gid - }, function (err) { - if (err) return next(err) - - switch (header.type) { - case 'file': return onfile() - case 'link': return onlink() - case 'symlink': return onsymlink() - } - - if (strict) return next(new Error('unsupported type for ' + name + ' (' + header.type + ')')) - - stream.resume() - next() - }) - }) - }) - - if (opts.finish) extract.on('finish', opts.finish) - - return extract -} - -function validate (fs, name, root, cb) { - if (name === root) return cb(null, true) - fs.lstat(name, function (err, st) { - if (err && err.code !== 'ENOENT') return cb(err) - if (err || st.isDirectory()) return validate(fs, path.join(name, '..'), root, cb) - cb(null, false) - }) -} - -function mkdirfix (name, opts, cb) { - mkdirp(name, {fs: opts.fs}, function (err, made) { - if (!err && made && opts.own) { - chownr(made, opts.uid, opts.gid, cb) - } else { - cb(err) - } - }) -} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..58d32f3 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,81 @@ +const path = require('path'); +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig'); + +const moduleNameMapper = pathsToModuleNameMapper(compilerOptions.paths, { + prefix: '/src/', +}); + +// Global variables that are shared across the jest worker pool +// These variables must be static and serializable +const globals = { + // Absolute directory to the project root + projectDir: __dirname, + // Absolute directory to the test root + testDir: path.join(__dirname, 'tests'), + // Default asynchronous test timeout + defaultTimeout: 20000, + // Timeouts rely on setTimeout which takes 32 bit numbers + maxTimeout: Math.pow(2, 31) - 1, +}; + +// The `globalSetup` and `globalTeardown` cannot access the `globals` +// They run in their own process context +// They can however receive the process environment +// Use `process.env` to set variables + +module.exports = { + testEnvironment: 'node', + verbose: true, + collectCoverage: false, + cacheDirectory: '/tmp/jest', + coverageDirectory: '/tmp/coverage', + roots: ['/tests'], + testMatch: ['**/?(*.)+(spec|test|unit.test).+(ts|tsx|js|jsx)'], + transform: { + "^.+\\.(t|j)sx?$": [ + "@swc/jest", + { + jsc: { + parser: { + syntax: "typescript", + tsx: true, + decorators: compilerOptions.experimentalDecorators, + dynamicImport: true, + }, + target: compilerOptions.target.toLowerCase(), + keepClassNames: true, + }, + } + ], + }, + reporters: [ + 'default', + ['jest-junit', { + outputDirectory: '/tmp/junit', + classNameTemplate: '{classname}', + ancestorSeparator: ' > ', + titleTemplate: '{title}', + addFileAttribute: 'true', + reportTestSuiteErrors: 'true', + }], + ], + collectCoverageFrom: ['src/**/*.{ts,tsx,js,jsx}', '!src/**/*.d.ts'], + coverageReporters: ['text', 'cobertura'], + globals, + // Global setup script executed once before all test files + globalSetup: '/tests/globalSetup.ts', + // Global teardown script executed once after all test files + globalTeardown: '/tests/globalTeardown.ts', + // Setup files are executed before each test file + // Can access globals + setupFiles: ['/tests/setup.ts'], + // Setup files after env are executed before each test file + // after the jest test environment is installed + // Can access globals + setupFilesAfterEnv: [ + 'jest-extended/all', + '/tests/setupAfterEnv.ts' + ], + moduleNameMapper: moduleNameMapper, +}; diff --git a/lib/VirtualTar.js b/lib/VirtualTar.js deleted file mode 100644 index 072b925..0000000 --- a/lib/VirtualTar.js +++ /dev/null @@ -1,29 +0,0 @@ -import pathNode from 'path'; -import tar from 'tar-stream'; - -const pathJoin = (pathNode.posix) ? pathNode.posix.join : pathNode.join; - -// we cannot use tar-fs -// because it works against streams -// but usually we want it to utilise an fs -// passed in as function -// so we use a constructor -// to pass in the correct fs - -class VirtualTar { - - constructor (fs) { - this._fs = fs; - } - - pack (path, options) { - - } - - extract (path, options) { - - } - -} - -export default VirtualTar; diff --git a/mytarball.tar b/mytarball.tar deleted file mode 100644 index 080b8b98546cd30cfd890c573dd8e669812dc84f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4096 zcmeHIO$)*>4DCp@|63Qt{|uv1sk?B%2*yek=oonY^uGzy|KC<#{Le5N i6&q+{a{^kM{h^KZ5w^{)=71IqQ9u+B1w?^AD)0g*$~pW1 diff --git a/package-lock.json b/package-lock.json index fd63bee..25f2db3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,2640 +1,8639 @@ { - "name": "tar-fs", - "version": "1.16.2", - "lockfileVersion": 1, + "name": "@matrixai/js-virtualtar", + "version": "0.0.1", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "acorn": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", - "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", - "dev": true - }, - "acorn-to-esprima": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/acorn-to-esprima/-/acorn-to-esprima-2.0.8.tgz", - "integrity": "sha1-AD8MZC65ITL0F9NwjxStqCrfLrE=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "3.2.2", - "longest": "1.0.1", - "repeat-string": "1.6.1" + "packages": { + "": { + "name": "@matrixai/js-virtualtar", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "@matrixai/logger": "^4.0.3", + "threads": "^1.7.0", + "uuid": "^11.0.5" + }, + "devDependencies": { + "@swc/core": "^1.3.62", + "@swc/jest": "^0.2.26", + "@types/jest": "^28.1.3", + "@types/node": "^18.15.0", + "@typescript-eslint/eslint-plugin": "^5.45.1", + "@typescript-eslint/parser": "^5.45.1", + "eslint": "^8.15.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^28.1.1", + "jest-extended": "^3.0.1", + "jest-junit": "^14.0.0", + "jest-mock-process": "^2.0.0", + "node-gyp-build": "^4.4.0", + "pkg": "^5.8.1", + "prettier": "^2.6.2", + "shx": "^0.3.4", + "ts-jest": "^28.0.5", + "ts-node": "^10.9.1", + "tsconfig-paths": "^3.9.0", + "typedoc": "^0.23.21", + "typescript": "^4.9.3" } }, - "alter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/alter/-/alter-0.2.0.tgz", - "integrity": "sha1-x1iICGF1cgNKrmJICvJrHU0cs80=", + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, - "requires": { - "stable": "0.1.8" + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, - "requires": { - "sprintf-js": "1.0.3" - } - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, - "ast-traverse": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ast-traverse/-/ast-traverse-0.1.1.tgz", - "integrity": "sha1-ac8rg4bxnc2hux4F1o/jWdiJfeY=", - "dev": true - }, - "ast-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", - "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=", - "dev": true - }, - "babel-code-frame": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", - "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "esutils": "2.0.2", - "js-tokens": "3.0.2" - }, - "dependencies": { - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - } + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "babel-core": { - "version": "5.8.38", - "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-5.8.38.tgz", - "integrity": "sha1-H8ruedfmG3ULALjlT238nQr4ZVg=", - "dev": true, - "requires": { - "babel-plugin-constant-folding": "1.0.1", - "babel-plugin-dead-code-elimination": "1.0.2", - "babel-plugin-eval": "1.0.1", - "babel-plugin-inline-environment-variables": "1.0.1", - "babel-plugin-jscript": "1.0.4", - "babel-plugin-member-expression-literals": "1.0.1", - "babel-plugin-property-literals": "1.0.1", - "babel-plugin-proto-to-assign": "1.0.4", - "babel-plugin-react-constant-elements": "1.0.3", - "babel-plugin-react-display-name": "1.0.3", - "babel-plugin-remove-console": "1.0.1", - "babel-plugin-remove-debugger": "1.0.1", - "babel-plugin-runtime": "1.0.7", - "babel-plugin-undeclared-variables-check": "1.0.2", - "babel-plugin-undefined-to-void": "1.1.6", - "babylon": "5.8.38", - "bluebird": "2.11.0", - "chalk": "1.1.3", - "convert-source-map": "1.5.1", - "core-js": "1.2.7", - "debug": "2.6.9", - "detect-indent": "3.0.1", - "esutils": "2.0.2", - "fs-readdir-recursive": "0.1.2", - "globals": "6.4.1", - "home-or-tmp": "1.0.0", - "is-integer": "1.0.7", - "js-tokens": "1.0.1", - "json5": "0.4.0", - "lodash": "3.10.1", - "minimatch": "2.0.10", - "output-file-sync": "1.1.2", - "path-exists": "1.0.0", - "path-is-absolute": "1.0.1", - "private": "0.1.8", - "regenerator": "0.8.40", - "regexpu": "1.3.0", - "repeating": "1.1.3", - "resolve": "1.8.1", - "shebang-regex": "1.0.0", - "slash": "1.0.0", - "source-map": "0.5.7", - "source-map-support": "0.2.10", - "to-fast-properties": "1.0.3", - "trim-right": "1.0.1", - "try-resolve": "1.0.1" - }, - "dependencies": { - "core-js": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", - "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "globals": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-6.4.1.tgz", - "integrity": "sha1-hJgDKzttHMge68X3lpDY/in6v08=", - "dev": true - }, - "minimatch": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", - "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - } + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "babel-messages": { - "version": "6.23.0", - "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", - "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=", + "node_modules/@babel/core": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.8.tgz", + "integrity": "sha512-l+lkXCHS6tQEc5oUpK28xBOZ6+HwaH7YwoYQbLFiYb4nS2/l1tKnZEtEWkD0GuiYdvArf9qBS0XlQGXzPMsNqQ==", "dev": true, - "requires": { - "babel-runtime": "6.26.0" + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.8", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.7", + "@babel/parser": "^7.26.8", + "@babel/template": "^7.26.8", + "@babel/traverse": "^7.26.8", + "@babel/types": "^7.26.8", + "@types/gensync": "^1.0.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "babel-plugin-constant-folding": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-constant-folding/-/babel-plugin-constant-folding-1.0.1.tgz", - "integrity": "sha1-g2HTZMmORJw2kr26Ue/whEKQqo4=", - "dev": true - }, - "babel-plugin-dead-code-elimination": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-dead-code-elimination/-/babel-plugin-dead-code-elimination-1.0.2.tgz", - "integrity": "sha1-X3xFEnTc18zNv7s+C4XdKBIfD2U=", - "dev": true - }, - "babel-plugin-eval": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-eval/-/babel-plugin-eval-1.0.1.tgz", - "integrity": "sha1-ovrtJc5r5preS/7CY/cBaRlZUNo=", - "dev": true - }, - "babel-plugin-inline-environment-variables": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-inline-environment-variables/-/babel-plugin-inline-environment-variables-1.0.1.tgz", - "integrity": "sha1-H1jOkSB61qgmqL9kX6/mj/X+P/4=", - "dev": true - }, - "babel-plugin-jscript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/babel-plugin-jscript/-/babel-plugin-jscript-1.0.4.tgz", - "integrity": "sha1-jzQsOCduh6R9X6CovT1etsytj8w=", - "dev": true - }, - "babel-plugin-member-expression-literals": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-member-expression-literals/-/babel-plugin-member-expression-literals-1.0.1.tgz", - "integrity": "sha1-zF7bD6qNyScXDnTW0cAkQAIWJNM=", - "dev": true - }, - "babel-plugin-property-literals": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-property-literals/-/babel-plugin-property-literals-1.0.1.tgz", - "integrity": "sha1-AlIwGQAZKYCxwRjv6kjOk6q4MzY=", - "dev": true + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, - "babel-plugin-proto-to-assign": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/babel-plugin-proto-to-assign/-/babel-plugin-proto-to-assign-1.0.4.tgz", - "integrity": "sha1-xJ56/QL1d7xNoF6i3wAiUM980SM=", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "lodash": "3.10.1" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "babel-plugin-react-constant-elements": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/babel-plugin-react-constant-elements/-/babel-plugin-react-constant-elements-1.0.3.tgz", - "integrity": "sha1-lGc26DeEKcvDSdz/YvUcFDs041o=", - "dev": true - }, - "babel-plugin-react-display-name": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/babel-plugin-react-display-name/-/babel-plugin-react-display-name-1.0.3.tgz", - "integrity": "sha1-dU/jiSboQkpOexWrbqYTne4FFPw=", - "dev": true - }, - "babel-plugin-remove-console": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-remove-console/-/babel-plugin-remove-console-1.0.1.tgz", - "integrity": "sha1-2PJFVsOgUAXUKqqv0neH9T/wE6c=", - "dev": true - }, - "babel-plugin-remove-debugger": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-remove-debugger/-/babel-plugin-remove-debugger-1.0.1.tgz", - "integrity": "sha1-/S6jzWGkKK0fO5yJiC/0KT6MFMc=", - "dev": true - }, - "babel-plugin-runtime": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/babel-plugin-runtime/-/babel-plugin-runtime-1.0.7.tgz", - "integrity": "sha1-v3x9lm3Vbs1cF/ocslPJrLflSq8=", - "dev": true - }, - "babel-plugin-undeclared-variables-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-undeclared-variables-check/-/babel-plugin-undeclared-variables-check-1.0.2.tgz", - "integrity": "sha1-XPGqU52BP/ZOmWQSkK9iCWX2Xe4=", - "dev": true, - "requires": { - "leven": "1.0.2" - } - }, - "babel-plugin-undefined-to-void": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/babel-plugin-undefined-to-void/-/babel-plugin-undefined-to-void-1.1.6.tgz", - "integrity": "sha1-f1eO+LeN+uYAM4XYQXph7aBuL4E=", - "dev": true - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "2.5.7", - "regenerator-runtime": "0.11.1" - } - }, - "babel-traverse": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz", - "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=", - "dev": true, - "requires": { - "babel-code-frame": "6.26.0", - "babel-messages": "6.23.0", - "babel-runtime": "6.26.0", - "babel-types": "6.26.0", - "babylon": "6.18.0", - "debug": "2.6.9", - "globals": "9.18.0", - "invariant": "2.2.4", - "lodash": "4.17.10" - }, - "dependencies": { - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - }, - "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", - "dev": true - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true - } + "node_modules/@babel/generator": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.8.tgz", + "integrity": "sha512-ef383X5++iZHWAXX0SXQR6ZyQhw/0KtTkrTz61WXRhFM6dhpHulO/RJz79L8S6ugZHJkOOkUrUdxgdF2YiPFnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.8", + "@babel/types": "^7.26.8", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" } }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, - "requires": { - "babel-runtime": "6.26.0", - "esutils": "2.0.2", - "lodash": "4.17.10", - "to-fast-properties": "1.0.3" - }, + "license": "MIT", "dependencies": { - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "lodash": { - "version": "4.17.10", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", - "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", - "dev": true - } + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "babylon": { - "version": "5.8.38", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-5.8.38.tgz", - "integrity": "sha1-7JsSCxG/bM1Bc6GL8hfmC3mFn/0=", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "bitset": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/bitset/-/bitset-5.0.3.tgz", - "integrity": "sha512-sUf+oh2tLafEOCliDSAsCYYj8m9ebq22Efw74eaP2AwV1JdWktrtFOt73unr/YoThFOYYANJhBvD7UZyYtUBsA==" - }, - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "2.3.6", - "safe-buffer": "5.1.2" + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "bluebird": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz", - "integrity": "sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, - "requires": { - "balanced-match": "1.0.0", - "concat-map": "0.0.1" + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "breakable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/breakable/-/breakable-1.0.0.tgz", - "integrity": "sha1-eEp5eRWjjq0nutRWtVcstLuqeME=", - "dev": true - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "1.1.0", - "buffer-fill": "1.0.0" + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "buffer-from": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", - "integrity": "sha512-c5mRlguI/Pe2dSZmpER62rSCu0ryKmWddzRYsuXc50U2/g8jMOulc31VZMa4mYx31U5xsmSOpDCgH88Vl9cDGQ==", - "dev": true - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, - "requires": { - "align-text": "0.1.4", - "lazy-cache": "1.0.4" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "chownr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", - "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" - }, - "cli-width": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz", - "integrity": "sha1-pNKT72frt7iNSk1CwMzwDE0eNm0=", - "dev": true - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, - "requires": { - "center-align": "0.1.3", - "right-align": "0.1.3", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true - } + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "commoner": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/commoner/-/commoner-0.10.8.tgz", - "integrity": "sha1-NPw2cs0kOT6LtH5wyqApOBH08sU=", - "dev": true, - "requires": { - "commander": "2.15.1", - "detective": "4.7.1", - "glob": "5.0.15", - "graceful-fs": "4.1.11", - "iconv-lite": "0.4.23", - "mkdirp": "0.5.1", - "private": "0.1.8", - "q": "1.5.1", - "recast": "0.11.23" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "recast": { - "version": "0.11.23", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", - "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", - "dev": true, - "requires": { - "ast-types": "0.9.6", - "esprima": "3.1.3", - "private": "0.1.8", - "source-map": "0.5.7" - } - } + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", "dev": true, - "requires": { - "buffer-from": "1.1.0", - "inherits": "2.0.3", - "readable-stream": "2.3.6", - "typedarray": "0.0.6" + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "config-chain": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", + "node_modules/@babel/parser": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.8.tgz", + "integrity": "sha512-TZIQ25pkSoaKEYYaHbbxkfL36GNsQ6iFiBbeuzAkLnXayKR1yP1zFe+NxuZWWsUyvt8icPU9CCq0sgWGXR1GEw==", "dev": true, - "requires": { - "ini": "1.3.5", - "proto-list": "1.2.4" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "convert-source-map": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz", - "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=", - "dev": true - }, - "core-js": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz", - "integrity": "sha512-RszJCAxg/PP6uzXVXL6BsxSXx/B05oJAQ2vkJRjyjrEcNVycaqOmNb5OTxZPE3xa5gwZduqza6L9JOCenh/Ecw==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, - "requires": { - "es5-ext": "0.10.45" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, - "requires": { - "ms": "2.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "debug-log": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debug-log/-/debug-log-1.0.1.tgz", - "integrity": "sha1-IwdjLUwEOCuN+KMvcLiVBG1SdF8=", - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "deep-equal": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-0.2.2.tgz", - "integrity": "sha1-hLdFiW80xoTpjyzg5Cq69Du6AX0=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", - "dev": true - }, - "defs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/defs/-/defs-1.1.1.tgz", - "integrity": "sha1-siYJ8sehG6ej2xFoBcE5scr/qdI=", - "dev": true, - "requires": { - "alter": "0.2.0", - "ast-traverse": "0.1.1", - "breakable": "1.0.0", - "esprima-fb": "15001.1001.0-dev-harmony-fb", - "simple-fmt": "0.1.0", - "simple-is": "0.2.0", - "stringmap": "0.2.2", - "stringset": "0.2.1", - "tryor": "0.1.2", - "yargs": "3.27.0" - }, - "dependencies": { - "esprima-fb": { - "version": "15001.1001.0-dev-harmony-fb", - "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz", - "integrity": "sha1-Q761fsJujPI3092LM+QlM1d/Jlk=", - "dev": true - } + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "deglob": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/deglob/-/deglob-1.1.2.tgz", - "integrity": "sha1-dtV3wl/j9zKUEqK1nq3qV6xQDj8=", - "dev": true, - "requires": { - "find-root": "1.1.0", - "glob": "7.1.2", - "ignore": "3.3.8", - "pkg-config": "1.1.1", - "run-parallel": "1.1.9", - "uniq": "1.0.1", - "xtend": "4.0.1" - }, - "dependencies": { - "find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true - } + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "detect-indent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-3.0.1.tgz", - "integrity": "sha1-ncXl3bzu+DJXZLlFGwK8bVQIT3U=", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", "dev": true, - "requires": { - "get-stdin": "4.0.1", - "minimist": "1.2.0", - "repeating": "1.1.3" - }, + "license": "MIT", "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "detective": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", - "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, - "requires": { - "acorn": "5.7.1", - "defined": "1.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "dezalgo": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", - "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, - "requires": { - "asap": "2.0.6", - "wrappy": "1.0.2" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "diff": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", - "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", - "dev": true - }, - "disparity": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/disparity/-/disparity-2.0.0.tgz", - "integrity": "sha1-V92stHMkrl9Y0swNqIbbTOnutxg=", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, - "requires": { - "ansi-styles": "2.2.1", - "diff": "1.4.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "doctrine": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.6.4.tgz", - "integrity": "sha1-gUKEkalC7xiwSSBW7aOADu5X1h0=", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, - "requires": { - "esutils": "1.1.6", - "isarray": "0.0.1" - }, + "license": "MIT", "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "dom-walk": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", - "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=" - }, - "editorconfig": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.13.3.tgz", - "integrity": "sha512-WkjsUNVCu+ITKDj73QDvi0trvpdDWdkDyHybDGSXPfekLCqwmpD7CP7iPbvBgosNuLcI96XTDwNa75JyFl7tEQ==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "requires": { - "bluebird": "3.5.1", - "commander": "2.15.1", - "lru-cache": "3.2.0", - "semver": "5.5.0", - "sigmund": "1.0.1" - }, + "license": "MIT", "dependencies": { - "bluebird": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "1.4.0" + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "requires": { - "prr": "1.0.1" + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "es5-ext": { - "version": "0.10.45", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.45.tgz", - "integrity": "sha512-FkfM6Vxxfmztilbxxz5UKSD4ICMf5tSpRFtDNtkAhOxZ0EKtX6qwmXNyH/sFyIbX2P/nU5AMiA9jilWsUGJzCQ==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "requires": { - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1", - "next-tick": "1.0.0" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45", - "es6-symbol": "3.1.1" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45", - "es6-iterator": "2.0.3", - "es6-set": "0.1.5", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1", - "event-emitter": "0.3.5" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "node_modules/@babel/template": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.8.tgz", + "integrity": "sha512-iNKaX3ZebKIsCvJ+0jd6embf+Aulaa3vNBqZ41kM7iTWjx5qzWKXGHiJUW3+nTpQ18SG11hdF8OAzKrpXkb96Q==", "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45" + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.8", + "@babel/types": "^7.26.8" + }, + "engines": { + "node": ">=6.9.0" } }, - "es6-weak-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", - "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", + "node_modules/@babel/traverse": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.8.tgz", + "integrity": "sha512-nic9tRkjYH0oB2dzr/JoGIm+4Q6SuYeLEiIiZDwBscRMYFJ+tMAz98fuel9ZnbXViA2I0HVSSRRK8DW5fjXStA==", "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45", - "es6-iterator": "2.0.3", - "es6-symbol": "3.1.1" + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.8", + "@babel/parser": "^7.26.8", + "@babel/template": "^7.26.8", + "@babel/types": "^7.26.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "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 - }, - "escope": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", - "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", - "dev": true, - "requires": { - "es6-map": "0.1.5", - "es6-weak-map": "2.0.2", - "esrecurse": "4.2.1", - "estraverse": "4.2.0" - } - }, - "esformatter": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/esformatter/-/esformatter-0.8.2.tgz", - "integrity": "sha1-e6mIKqPtMIOfivds3fTxLaM3084=", - "dev": true, - "requires": { - "debug": "0.7.4", - "disparity": "2.0.0", - "espree": "2.2.5", - "glob": "5.0.15", - "minimist": "1.2.0", - "mout": "1.1.0", - "npm-run": "2.0.0", - "resolve": "1.8.1", - "rocambole": "0.7.0", - "rocambole-indent": "2.0.4", - "rocambole-linebreak": "1.0.2", - "rocambole-node": "1.0.0", - "rocambole-token": "1.2.1", - "rocambole-whitespace": "1.0.0", - "stdin": "0.0.1", - "strip-json-comments": "0.1.3", - "supports-color": "1.3.1", - "user-home": "2.0.0" - }, - "dependencies": { - "debug": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz", - "integrity": "sha1-BuHqgILCyxTjmAbiLi9vdX+Srzk=", - "dev": true - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "strip-json-comments": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-0.1.3.tgz", - "integrity": "sha1-Fkxk43Coo8wAyeAbU55WmCPw7lQ=", - "dev": true - }, - "supports-color": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz", - "integrity": "sha1-FXWN8J2P87SswwdTn6vicJXhBC0=", - "dev": true - }, - "user-home": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", - "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", - "dev": true, - "requires": { - "os-homedir": "1.0.2" - } - } + "node_modules/@babel/traverse/node_modules/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, + "license": "MIT", + "engines": { + "node": ">=4" } }, - "esformatter-eol-last": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esformatter-eol-last/-/esformatter-eol-last-1.0.0.tgz", - "integrity": "sha1-RaeP9GIrHUnkT1a0mQV2amMpDAc=", + "node_modules/@babel/types": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.8.tgz", + "integrity": "sha512-eUuWapzEGWFEpHFxgEaBG8e3n6S8L3MSu0oda755rOfabWPnh0Our1AozNFVUxGFIhbKgd1ksprsoDGMinTOTA==", "dev": true, - "requires": { - "string.prototype.endswith": "0.2.0" + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "esformatter-ignore": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/esformatter-ignore/-/esformatter-ignore-0.1.3.tgz", - "integrity": "sha1-BNO4db+knd4ATMWN9va7w8BWfx4=", - "dev": true + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" }, - "esformatter-jsx": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/esformatter-jsx/-/esformatter-jsx-2.3.11.tgz", - "integrity": "sha1-QRxE7TJHVK+VquXe2FbVp+78td8=", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, - "requires": { - "babel-core": "5.8.38", - "esformatter-ignore": "0.1.3", - "extend": "2.0.1", - "fresh-falafel": "1.2.0", - "js-beautify": "1.7.5" + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" } }, - "esformatter-literal-notation": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esformatter-literal-notation/-/esformatter-literal-notation-1.0.1.tgz", - "integrity": "sha1-cQ57QgF1/j9+WvrVu60ykQOELi8=", + "node_modules/@cspotcode/source-map-support/node_modules/@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": { - "rocambole": "0.3.6", - "rocambole-token": "1.2.1" - }, + "license": "MIT", "dependencies": { - "esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", - "dev": true - }, - "rocambole": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/rocambole/-/rocambole-0.3.6.tgz", - "integrity": "sha1-Teu/WUMUS8e2AG2Vvo+swLdDUqc=", - "dev": true, - "requires": { - "esprima": "1.0.4" - } - } + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "esformatter-parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esformatter-parser/-/esformatter-parser-1.0.0.tgz", - "integrity": "sha1-CFQHLQSHU57TnK442KVDLBfsEdM=", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "dev": true, - "requires": { - "acorn-to-esprima": "2.0.8", - "babel-traverse": "6.26.0", - "babylon": "6.18.0", - "rocambole": "0.7.0" - }, + "license": "MIT", "dependencies": { - "babylon": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz", - "integrity": "sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ==", - "dev": true - } + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "esformatter-quotes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/esformatter-quotes/-/esformatter-quotes-1.1.0.tgz", - "integrity": "sha1-4ixsRFx/MGBB2BybnlH8psv6yoI=", - "dev": true + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } }, - "esformatter-semicolon-first": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/esformatter-semicolon-first/-/esformatter-semicolon-first-1.2.0.tgz", - "integrity": "sha1-47US0dTgcxDqvKv1cnfqfIpW4kI=", + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "requires": { - "esformatter-parser": "1.0.0", - "rocambole": "0.7.0", - "rocambole-linebreak": "1.0.2", - "rocambole-token": "1.2.1" + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "esformatter-spaced-lined-comment": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esformatter-spaced-lined-comment/-/esformatter-spaced-lined-comment-2.0.1.tgz", - "integrity": "sha1-3F80B/k8KV4eVkRr00RWDaXm3Kw=", - "dev": true - }, - "eslint": { - "version": "0.24.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-0.24.1.tgz", - "integrity": "sha1-VKUICYVbllVyHG8u5Xs1HtzigQE=", - "dev": true, - "requires": { - "chalk": "1.1.3", - "concat-stream": "1.6.2", - "debug": "2.6.9", - "doctrine": "0.6.4", - "escape-string-regexp": "1.0.5", - "escope": "3.6.0", - "espree": "2.2.5", - "estraverse": "4.2.0", - "estraverse-fb": "1.3.2", - "globals": "8.18.0", - "inquirer": "0.8.5", - "is-my-json-valid": "2.17.2", - "js-yaml": "3.12.0", - "minimatch": "2.0.10", - "mkdirp": "0.5.1", - "object-assign": "2.1.1", - "optionator": "0.5.0", - "path-is-absolute": "1.0.1", - "strip-json-comments": "1.0.4", - "text-table": "0.2.0", - "user-home": "1.1.1", - "xml-escape": "1.0.0" - }, - "dependencies": { - "minimatch": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", - "integrity": "sha1-jQh8OcazjAAbl/ynzm0OHoCvusc=", - "dev": true, - "requires": { - "brace-expansion": "1.1.11" - } - } + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "eslint-config-standard": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-3.4.1.tgz", - "integrity": "sha1-vxorY2zeyTPR6eQ4PXT11AvZAsM=", - "dev": true + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } }, - "eslint-config-standard-react": { + "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-standard-react/-/eslint-config-standard-react-1.0.1.tgz", - "integrity": "sha1-3V2ZMYWOJojdja/iyJzgTWxnkNc=", - "dev": true - }, - "eslint-plugin-react": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-2.7.1.tgz", - "integrity": "sha1-XW8bylB9E4e2WTwjCZivBPC5rtY=", - "dev": true - }, - "espree": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/espree/-/espree-2.2.5.tgz", - "integrity": "sha1-32kbkxCIlAKuspzAZnCMVmkLhUs=", - "dev": true - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "requires": { - "estraverse": "4.2.0" + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "estraverse-fb": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/estraverse-fb/-/estraverse-fb-1.3.2.tgz", - "integrity": "sha1-0yOky15awzHOoDNBOpJT4WQ+B8Q=", - "dev": true - }, - "esutils": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", - "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", - "dev": true + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "requires": { - "d": "1.0.0", - "es5-ext": "0.10.45" + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "extend": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-2.0.1.tgz", - "integrity": "sha1-HugBBonnOV/5RIJByYZSvHWagmA=", - "dev": true - }, - "fast-levenshtein": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.0.7.tgz", - "integrity": "sha1-AXjc3uAjuSkFGTrwlZ6KdjnP3Lk=", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "requires": { - "escape-string-regexp": "1.0.5", - "object-assign": "4.1.1" - }, + "license": "MIT", "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } + "sprintf-js": "~1.0.2" } }, - "find-root": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-0.1.2.tgz", - "integrity": "sha1-mNImfP8ZFsyvJ0OzoO6oHXnX3NE=", - "dev": true - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", - "dev": true - }, - "fresh-falafel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fresh-falafel/-/fresh-falafel-1.2.0.tgz", - "integrity": "sha1-WWbe6V+zXSopsS0vJRaLFyJeS2w=", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, - "requires": { - "acorn": "1.2.2", - "foreach": "2.0.5", - "isarray": "0.0.1", - "object-keys": "1.0.12" - }, + "license": "MIT", "dependencies": { - "acorn": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", - "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=", - "dev": true - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs-readdir-recursive": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-0.1.2.tgz", - "integrity": "sha1-MVtPuMHKW4xH3v7zGdBz2tNWgFk=", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, - "requires": { - "is-property": "1.0.2" + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "get-random-values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-random-values/-/get-random-values-1.2.0.tgz", - "integrity": "sha1-MpIO3oG+2YJl/0A3HPSSmb1YHvE=", - "requires": { - "global": "4.3.2" + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "global": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", - "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", - "requires": { - "min-document": "2.19.0", - "process": "0.5.2" - } - }, - "globals": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-8.18.0.tgz", - "integrity": "sha1-k9SmK9ysOM+vr8R9awNHaMsP/LQ=", - "dev": true - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "requires": { - "ansi-regex": "2.1.1" + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "home-or-tmp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-1.0.0.tgz", - "integrity": "sha1-S58eQIAMPlDGwn94FnavzOcfOYU=", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "requires": { - "os-tmpdir": "1.0.2", - "user-home": "1.1.1" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "iconv-lite": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", - "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "requires": { - "safer-buffer": "2.1.2" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "ignore": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.8.tgz", - "integrity": "sha512-pUh+xUQQhQzevjRHHFqqcTy0/dP/kS9I8HSrUydhihjuD09W6ldVWFtIrwhXdUJHis3i2rZNqEHpZH/cbinFbg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "node_modules/@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "inquirer": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.8.5.tgz", - "integrity": "sha1-29dAz2yjtzEpamPOb22WGFHzNt8=", - "dev": true, - "requires": { - "ansi-regex": "1.1.1", - "chalk": "1.1.3", - "cli-width": "1.1.1", - "figures": "1.7.0", - "lodash": "3.10.1", - "readline2": "0.1.1", - "rx": "2.5.3", - "through": "2.3.8" - }, - "dependencies": { - "ansi-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz", - "integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=", - "dev": true + "node_modules/@jest/core": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", + "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/reporters": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^28.1.3", + "jest-config": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-regex-util": "^28.0.2", + "jest-resolve": "^28.1.3", + "jest-resolve-dependencies": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "jest-watcher": "^28.1.3", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true } } }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", "dev": true, - "requires": { - "loose-envify": "1.3.1" + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "invert-kv": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", - "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", - "dev": true - }, - "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-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, - "requires": { - "number-is-nan": "1.0.1" + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "is-integer": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-integer/-/is-integer-1.0.7.tgz", - "integrity": "sha1-a96Bqs3feLZZtmKdYpytxRqIbVw=", + "node_modules/@jest/environment": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", + "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", "dev": true, - "requires": { - "is-finite": "1.0.2" + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "jest-mock": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "node_modules/@jest/expect": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz", + "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==", "dev": true, - "requires": { - "generate-function": "2.0.0", - "generate-object-property": "1.2.0", - "is-my-ip-valid": "1.0.0", - "jsonpointer": "4.0.1", - "xtend": "4.0.1" + "license": "MIT", + "dependencies": { + "expect": "^28.1.3", + "jest-snapshot": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "js-beautify": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.7.5.tgz", - "integrity": "sha512-9OhfAqGOrD7hoQBLJMTA+BKuKmoEtTJXzZ7WDF/9gvjtey1koVLuZqIY6c51aPDjbNdNtIXAkiWKVhziawE9Og==", + "node_modules/@jest/expect-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", + "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", "dev": true, - "requires": { - "config-chain": "1.1.11", - "editorconfig": "0.13.3", - "mkdirp": "0.5.1", - "nopt": "3.0.6" + "license": "MIT", + "dependencies": { + "jest-get-type": "^28.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "js-tokens": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.1.tgz", - "integrity": "sha1-zENaXIuUrRWst5gxQPyAGCyJrq4=", - "dev": true + "node_modules/@jest/fake-timers": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", + "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "@sinonjs/fake-timers": "^9.1.2", + "@types/node": "*", + "jest-message-util": "^28.1.3", + "jest-mock": "^28.1.3", + "jest-util": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } }, - "js-yaml": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", - "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", + "node_modules/@jest/globals": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz", + "integrity": "sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==", "dev": true, - "requires": { - "argparse": "1.0.10", - "esprima": "4.0.0" + "license": "MIT", + "dependencies": { + "@jest/environment": "^28.1.3", + "@jest/expect": "^28.1.3", + "@jest/types": "^28.1.3" }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", + "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", + "dev": true, + "license": "MIT", "dependencies": { - "esprima": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", - "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", - "dev": true + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@jridgewell/trace-mapping": "^0.3.13", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "jest-worker": "^28.1.3", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true } } }, - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - }, - "json5": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz", - "integrity": "sha1-BUNS5MTIDIbAkjh31EneF2pzLI0=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, - "requires": { - "is-buffer": "1.1.6" + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true - }, - "lcid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", - "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "node_modules/@jest/source-map": { + "version": "28.1.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz", + "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==", "dev": true, - "requires": { - "invert-kv": "1.0.0" + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.13", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "leven": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/leven/-/leven-1.0.2.tgz", - "integrity": "sha1-kUS27ryl8dBoAWnxpncNzqYLdcM=", - "dev": true - }, - "levn": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.2.5.tgz", - "integrity": "sha1-uo0znQykphDjo/FFucr0iAcVUFQ=", + "node_modules/@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", "dev": true, - "requires": { - "prelude-ls": "1.1.2", - "type-check": "0.3.2" + "license": "MIT", + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "loose-envify": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz", - "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=", + "node_modules/@jest/test-sequencer": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", + "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", "dev": true, - "requires": { - "js-tokens": "3.0.2" - }, + "license": "MIT", "dependencies": { - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - } + "@jest/test-result": "^28.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "lru-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz", - "integrity": "sha1-cXibO39Tmb7IVl3aOKow0qCX7+4=", + "node_modules/@jest/transform": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", + "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", "dev": true, - "requires": { - "pseudomap": "1.0.2" + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^28.1.3", + "@jridgewell/trace-mapping": "^0.3.13", + "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.9", + "jest-haste-map": "^28.1.3", + "jest-regex-util": "^28.0.2", + "jest-util": "^28.1.3", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", - "requires": { - "dom-walk": "0.1.1" + "node_modules/@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "node_modules/@jest/types/node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", "dev": true, - "requires": { - "brace-expansion": "1.1.11" + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "node_modules/@jest/types/node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "dev": true, + "license": "MIT" }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" } }, - "mout": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mout/-/mout-1.1.0.tgz", - "integrity": "sha512-XsP0vf4As6BfqglxZqbqQ8SR6KQot2AgxvR0gG+WtUkf90vUXchMOZQtPf/Hml1rEffJupqL/tIrU6EYhsUQjw==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "mute-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha1-qSGZYKbV1dBGWXruUSUsZlX3F34=", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "requires": { - "abbrev": "1.1.1" + "license": "MIT", + "engines": { + "node": ">=6.0.0" } }, - "npm-path": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-1.1.0.tgz", - "integrity": "sha1-BHSuAEGcMn1UcBt88s0F3Ii+EUA=", + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, - "requires": { - "which": "1.3.1" + "license": "MIT", + "engines": { + "node": ">=6.0.0" } }, - "npm-run": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/npm-run/-/npm-run-2.0.0.tgz", - "integrity": "sha1-KN/ArV4uRv4ISOK9WN3wAue3PBU=", - "dev": true, - "requires": { - "minimist": "1.2.0", - "npm-path": "1.1.0", - "npm-which": "2.0.0", - "serializerr": "1.0.3", - "spawn-sync": "1.0.15", - "sync-exec": "0.5.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" }, - "npm-which": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-2.0.0.tgz", - "integrity": "sha1-DEaYIWC3gwk2YdHQG9RJbS/qu6w=", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, - "requires": { - "commander": "2.15.1", - "npm-path": "1.1.0", - "which": "1.3.1" + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", - "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=", - "dev": true - }, - "object-inspect": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-0.4.0.tgz", - "integrity": "sha1-9RV8EWwUVbJDsG7pdwM5LFrYn+w=", - "dev": true - }, - "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", - "dev": true + "node_modules/@matrixai/logger": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@matrixai/logger/-/logger-4.0.3.tgz", + "integrity": "sha512-cu7e82iwN32H+K8HxsrvrWEYSEj7+RP/iVFhJ4RuacC8/BSOLFOYxry3EchVjrx4FP5G7QP1HnKYXAGpZN/46w==", + "license": "Apache-2.0" }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1.0.2" + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" } }, - "optionator": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.5.0.tgz", - "integrity": "sha1-t1qJlaLUF98ltuTjhi9QqohlE2g=", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, - "requires": { - "deep-is": "0.1.3", - "fast-levenshtein": "1.0.7", - "levn": "0.2.5", - "prelude-ls": "1.1.2", - "type-check": "0.3.2", - "wordwrap": "0.0.3" + "license": "MIT", + "engines": { + "node": ">= 8" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", - "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, - "requires": { - "lcid": "1.0.0" + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, - "os-shim": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", - "dev": true + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" }, - "output-file-sync": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", - "integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY=", + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "mkdirp": "0.5.1", - "object-assign": "4.1.1" - }, + "license": "BSD-3-Clause", "dependencies": { - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - } + "type-detect": "4.0.8" } }, - "path-exists": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz", - "integrity": "sha1-1aiZjrce83p0w06w2eum6HjuoIE=", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-parse": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true - }, - "permaproxy": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/permaproxy/-/permaproxy-0.0.2.tgz", - "integrity": "sha1-HvRmli1dBhHVUJ4NjB29x6jA4Rs=" + "node_modules/@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } }, - "pkg-config": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-config/-/pkg-config-1.1.1.tgz", - "integrity": "sha1-VX7yLXPaPIg3EHdmxS6tq94pj+Q=", + "node_modules/@swc/core": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.15.tgz", + "integrity": "sha512-/iFeQuNaGdK7mfJbQcObhAhsMqLT7qgMYl7jX2GEIO+VDTejESpzAyKwaMeYXExN8D6e5BRHBCe7M5YlsuzjDA==", "dev": true, - "requires": { - "debug-log": "1.0.1", - "find-root": "1.1.0", - "xtend": "4.0.1" - }, + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.17" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.10.15", + "@swc/core-darwin-x64": "1.10.15", + "@swc/core-linux-arm-gnueabihf": "1.10.15", + "@swc/core-linux-arm64-gnu": "1.10.15", + "@swc/core-linux-arm64-musl": "1.10.15", + "@swc/core-linux-x64-gnu": "1.10.15", + "@swc/core-linux-x64-musl": "1.10.15", + "@swc/core-win32-arm64-msvc": "1.10.15", + "@swc/core-win32-ia32-msvc": "1.10.15", + "@swc/core-win32-x64-msvc": "1.10.15" + }, + "peerDependencies": { + "@swc/helpers": "*" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true } } }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", - "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=" - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true - }, - "protochain": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/protochain/-/protochain-1.0.5.tgz", - "integrity": "sha1-mRxAfpneJkqt+PgVBLXn+ve/omA=", - "dev": true - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true - }, - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "requires": { - "end-of-stream": "1.4.1", - "once": "1.4.0" + "node_modules/@swc/core-darwin-arm64": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.15.tgz", + "integrity": "sha512-zFdZ6/yHqMCPk7OhLFqHy/MQ1EqJhcZMpNHd1gXYT7VRU3FaqvvKETrUlG3VYl65McPC7AhMRfXPyJ0JO/jARQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" } }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "2.0.0", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" - } - }, - "readline2": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/readline2/-/readline2-0.1.1.tgz", - "integrity": "sha1-mUQ7pug7gw7zBRv9fcJBqCco1Wg=", - "dev": true, - "requires": { - "mute-stream": "0.0.4", - "strip-ansi": "2.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz", - "integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=", - "dev": true - }, - "strip-ansi": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz", - "integrity": "sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=", - "dev": true, - "requires": { - "ansi-regex": "1.1.1" - } - } + "node_modules/@swc/core-darwin-x64": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.15.tgz", + "integrity": "sha512-8g4yiQwbr8fxOOjKXdot0dEkE5zgE8uNZudLy/ZyAhiwiZ8pbJ8/wVrDOu6dqbX7FBXAoDnvZ7fwN1jk4C8jdA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" } }, - "recast": { - "version": "0.10.33", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.10.33.tgz", - "integrity": "sha1-lCgI96oBbx+nFCxGHX5XBKqo1pc=", + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.15.tgz", + "integrity": "sha512-rl+eVOltl2+7WXOnvmWBpMgh6aO13G5x0U0g8hjwlmD6ku3Y9iRcThpOhm7IytMEarUp5pQxItNoPq+VUGjVHg==", + "cpu": [ + "arm" + ], "dev": true, - "requires": { - "ast-types": "0.8.12", - "esprima-fb": "15001.1001.0-dev-harmony-fb", - "private": "0.1.8", - "source-map": "0.5.7" - }, - "dependencies": { - "ast-types": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.8.12.tgz", - "integrity": "sha1-oNkOQ1G7iHcWyD/WN+v4GK9K38w=", - "dev": true - }, - "esprima-fb": { - "version": "15001.1001.0-dev-harmony-fb", - "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz", - "integrity": "sha1-Q761fsJujPI3092LM+QlM1d/Jlk=", - "dev": true - } + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerator": { - "version": "0.8.40", - "resolved": "https://registry.npmjs.org/regenerator/-/regenerator-0.8.40.tgz", - "integrity": "sha1-oORXxY69uuV1yfjNdRJ+k3VkNdg=", - "dev": true, - "requires": { - "commoner": "0.10.8", - "defs": "1.1.1", - "esprima-fb": "15001.1001.0-dev-harmony-fb", - "private": "0.1.8", - "recast": "0.10.33", - "through": "2.3.8" - }, - "dependencies": { - "esprima-fb": { - "version": "15001.1001.0-dev-harmony-fb", - "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1001.0-dev-harmony-fb.tgz", - "integrity": "sha1-Q761fsJujPI3092LM+QlM1d/Jlk=", - "dev": true - } + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.15.tgz", + "integrity": "sha512-qxWEQeyAJMWJqjaN4hi58WMpPdt3Tn0biSK9CYRegQtvZWCbewr6v2agtSu5AZ2rudeH6OfCWAMDQQeSgn6PJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - }, - "regexpu": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexpu/-/regexpu-1.3.0.tgz", - "integrity": "sha1-5TTcmRqeWEYFDJjebX3UpVyeoW0=", - "dev": true, - "requires": { - "esprima": "2.7.3", - "recast": "0.10.33", - "regenerate": "1.4.0", - "regjsgen": "0.2.0", - "regjsparser": "0.1.5" - }, - "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - } + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.15.tgz", + "integrity": "sha512-QcELd9/+HjZx0WCxRrKcyKGWTiQ0485kFb5w8waxcSNd0d9Lgk4EFfWWVyvIb5gIHpDQmhrgzI/yRaWQX4YSZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.15.tgz", + "integrity": "sha512-S1+ZEEn3+a/MiMeQqQypbwTGoBG8/sPoCvpNbk+uValyygT+jSn3U0xVr45FbukpmMB+NhBMqfedMLqKA0QnJA==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "jsesc": "0.5.0" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "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 - }, - "repeating": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", - "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.15.tgz", + "integrity": "sha512-qW+H9g/2zTJ4jP7NDw4VAALY0ZlNEKzYsEoSj/HKi7k3tYEHjMzsxjfsY9I8WZCft23bBdV3RTCPoxCshaj1CQ==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "is-finite": "1.0.2" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" } }, - "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.15.tgz", + "integrity": "sha512-AhRB11aA6LxjIqut+mg7qsu/7soQDmbK6MKR9nP3hgBszpqtXbRba58lr24xIbBCMr+dpo6kgEapWt+t5Po6Zg==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "path-parse": "1.0.5" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, - "resource-counter": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/resource-counter/-/resource-counter-1.2.4.tgz", - "integrity": "sha512-DGJChvE5r4smqPE+xYNv9r1u/I9cCfRR5yfm7D6EQckdKqMyVpJ5z0s40yn0EM0puFxHg6mPORrQLQdEbJ/RnQ==", - "requires": { - "babel-runtime": "6.26.0", - "bitset": "5.0.3" + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.15.tgz", + "integrity": "sha512-UGdh430TQwbDn6KjgvRTg1fO022sbQ4yCCHUev0+5B8uoBwi9a89qAz3emy2m56C8TXxUoihW9Y9OMfaRwPXUw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, - "resumer": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", - "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.10.15", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.15.tgz", + "integrity": "sha512-XJzBCqO1m929qbJsOG7FZXQWX26TnEoMctS3QjuCoyBmkHxxQmZsy78KjMes1aomTcKHCyFYgrRGWgVmk7tT4Q==", + "cpu": [ + "x64" + ], "dev": true, - "requires": { - "through": "2.3.8" + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" } }, - "right-align": { + "node_modules/@swc/counter": { "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true, - "requires": { - "align-text": "0.1.4" - } + "license": "Apache-2.0" }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "node_modules/@swc/jest": { + "version": "0.2.37", + "resolved": "https://registry.npmjs.org/@swc/jest/-/jest-0.2.37.tgz", + "integrity": "sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==", "dev": true, - "requires": { - "glob": "7.1.2" + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@swc/counter": "^0.1.3", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "npm": ">= 7.0.0" + }, + "peerDependencies": { + "@swc/core": "*" } }, - "rocambole": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rocambole/-/rocambole-0.7.0.tgz", - "integrity": "sha1-9seVBVF9xCtvuECEK4uVOw+WhYU=", + "node_modules/@swc/types": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", + "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", "dev": true, - "requires": { - "esprima": "2.7.3" - }, + "license": "Apache-2.0", "dependencies": { - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - } + "@swc/counter": "^0.1.3" } }, - "rocambole-indent": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/rocambole-indent/-/rocambole-indent-2.0.4.tgz", - "integrity": "sha1-oYokl3ygQAuGHapGMehh3LUtCFw=", + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "dev": true, - "requires": { - "debug": "2.6.9", - "mout": "0.11.1", - "rocambole-token": "1.2.1" - }, - "dependencies": { - "mout": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz", - "integrity": "sha1-ujYR318OWx/7/QEWa48C0fX6K5k=", - "dev": true - } - } + "license": "MIT" }, - "rocambole-linebreak": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/rocambole-linebreak/-/rocambole-linebreak-1.0.2.tgz", - "integrity": "sha1-A2IVFbQ7RyHJflocG8paA2Y2jy8=", + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", "dev": true, - "requires": { - "debug": "2.6.9", - "rocambole-token": "1.2.1", - "semver": "4.3.6" - } + "license": "MIT" }, - "rocambole-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rocambole-node/-/rocambole-node-1.0.0.tgz", - "integrity": "sha1-21tJ3nQHsAgN1RSHLyjjk9D3/z8=", - "dev": true + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" }, - "rocambole-token": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/rocambole-token/-/rocambole-token-1.2.1.tgz", - "integrity": "sha1-x4XfdCjcPLJ614lwR71SOMwHDTU=", - "dev": true + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" }, - "rocambole-whitespace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rocambole-whitespace/-/rocambole-whitespace-1.0.0.tgz", - "integrity": "sha1-YzMJSSVrKZQfWbGQRZ+ZnGsdO/k=", + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, - "requires": { - "debug": "2.6.9", - "repeat-string": "1.6.1", - "rocambole-token": "1.2.1" + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "run-parallel": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.9.tgz", - "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", - "dev": true + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } }, - "rx": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/rx/-/rx-2.5.3.tgz", - "integrity": "sha1-Ia3H2A8CACr1Da6X/Z2/JIdV9WY=", - "dev": true + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/gensync": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.4.tgz", + "integrity": "sha512-C3YYeRQWp2fmq9OryX+FoDy8nXS6scQ7dPptD8LnFDAUNcKWJjXQKDNJD3HVm+kOUsXhTOkpi69vI4EuAr95bA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "28.1.8", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.8.tgz", + "integrity": "sha512-8TJkV++s7B6XqnDrzR1m/TT0A0h948Pnl/097veySPN67VRAgQ4gZ7n2KfJo2rVq6njQjdxU3GCCyDvAeuHoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^28.0.0", + "pretty-format": "^28.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.19.75", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.75.tgz", + "integrity": "sha512-UIksWtThob6ZVSyxcOqCLOUNg/dyO1Qvx4McgeuhrEtHTLFTf7BBhEazaE4K806FGTPtzd/2sE90qn4fVr7cyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.3.tgz", + "integrity": "sha512-+fksAx9eG3Ab6LDnLs3ZqZa8KVJ/jYnX+D4Qe1azX+LFGFAXqynCQLOdLpNYN/l9e7l6hMWwZbrnctqr6eSQSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz", + "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^28.1.3", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^28.1.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "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, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz", + "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz", + "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^28.1.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001699", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001699.tgz", + "integrity": "sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.97", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz", + "integrity": "sha512-HKLtaH02augM7ZOdYRuO19rWDeY+QSJ1VxnXFa/XDFLf07HvM90pALIJFgrO+UVaajI3+aJMMpojoUTLZyQ7JQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", + "integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "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" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/expect": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", + "integrity": "sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^28.1.3", + "jest-get-type": "^28.0.2", + "jest-matcher-utils": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "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, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/into-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-6.0.0.tgz", + "integrity": "sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-observable": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz", + "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "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, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", + "integrity": "sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^28.1.3", + "@jest/types": "^28.1.3", + "import-local": "^3.0.2", + "jest-cli": "^28.1.3" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz", + "integrity": "sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-circus": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz", + "integrity": "sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^28.1.3", + "@jest/expect": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^28.1.3", + "jest-matcher-utils": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "p-limit": "^3.1.0", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-cli": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz", + "integrity": "sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz", + "integrity": "sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^28.1.3", + "@jest/types": "^28.1.3", + "babel-jest": "^28.1.3", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^28.1.3", + "jest-environment-node": "^28.1.3", + "jest-get-type": "^28.0.2", + "jest-regex-util": "^28.0.2", + "jest-resolve": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz", + "integrity": "sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-each": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz", + "integrity": "sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "chalk": "^4.0.0", + "jest-get-type": "^28.0.2", + "jest-util": "^28.1.3", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz", + "integrity": "sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^28.1.3", + "@jest/fake-timers": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "jest-mock": "^28.1.3", + "jest-util": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-extended": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/jest-extended/-/jest-extended-3.2.4.tgz", + "integrity": "sha512-lSEYhSmvXZG/7YXI7KO3LpiUiQ90gi5giwCJNDMMsX5a+/NZhdbQF2G4ALOBN+KcXVT3H6FPVPohAuMXooaLTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-diff": "^29.0.0", + "jest-get-type": "^29.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "jest": ">=27.2.5" + }, + "peerDependenciesMeta": { + "jest": { + "optional": true + } + } + }, + "node_modules/jest-extended/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz", + "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz", + "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^28.0.2", + "jest-util": "^28.1.3", + "jest-worker": "^28.1.3", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-junit": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-14.0.1.tgz", + "integrity": "sha512-h7/wwzPbllgpQhhVcRzRC76/cc89GlazThoV1fDxcALkf26IIlRsu/AcTG64f4nR2WPE3Cbd+i/sVf+NCUHrWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-junit/node_modules/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, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/jest-leak-detector": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz", + "integrity": "sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz", + "integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^28.1.3", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz", + "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^28.1.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-mock": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz", + "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-mock-process": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jest-mock-process/-/jest-mock-process-2.0.0.tgz", + "integrity": "sha512-bybzszPfvrYhplymvUNFc130ryvjSCW1JSCrLA0LiV0Sv9TrI+cz90n3UYUPoT2nhNL6c6IV9LxUSFJF9L9tHQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jest": ">=23.4" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", + "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz", + "integrity": "sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz", + "integrity": "sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^28.0.2", + "jest-snapshot": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-runner": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz", + "integrity": "sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^28.1.3", + "@jest/environment": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "graceful-fs": "^4.2.9", + "jest-docblock": "^28.1.1", + "jest-environment-node": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-leak-detector": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-resolve": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-util": "^28.1.3", + "jest-watcher": "^28.1.3", + "jest-worker": "^28.1.3", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz", + "integrity": "sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^28.1.3", + "@jest/fake-timers": "^28.1.3", + "@jest/globals": "^28.1.3", + "@jest/source-map": "^28.1.2", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-mock": "^28.1.3", + "jest-regex-util": "^28.0.2", + "jest-resolve": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz", + "integrity": "sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^28.1.3", + "graceful-fs": "^4.2.9", + "jest-diff": "^28.1.3", + "jest-get-type": "^28.0.2", + "jest-haste-map": "^28.1.3", + "jest-matcher-utils": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "natural-compare": "^1.4.0", + "pretty-format": "^28.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "28.1.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz", + "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz", + "integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^28.1.1", + "jest-get-type": "^28.0.2", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-util": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", + "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-validate": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz", + "integrity": "sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^28.1.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^28.0.2", + "leven": "^3.1.0", + "pretty-format": "^28.1.3" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz", + "integrity": "sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^28.1.3", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-worker": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", + "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/jest-worker/node_modules/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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "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, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/multistream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-4.1.0.tgz", + "integrity": "sha512-J1XDiAmmNpRCBfIWJv+n0ymC4ABcf/Pl+5YvC5B/D2f/2+8PtHvCNxMPKiQcZyi922Hq69J2YOpb1pTywfifyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "once": "^1.4.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/multistream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/observable-fns": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz", + "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==", + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/pkg/-/pkg-5.8.1.tgz", + "integrity": "sha512-CjBWtFStCfIiT4Bde9QpJy0KeH19jCfwZRJqHFDFXfhUklCx8JoFmMj3wgnEYIwGmZVNkhsStPHEOnrtrQhEXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "7.18.2", + "@babel/parser": "7.18.4", + "@babel/types": "7.19.0", + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "globby": "^11.1.0", + "into-stream": "^6.0.0", + "is-core-module": "2.9.0", + "minimist": "^1.2.6", + "multistream": "^4.1.0", + "pkg-fetch": "3.4.2", + "prebuild-install": "7.1.1", + "resolve": "^1.22.0", + "stream-meter": "^1.0.4" + }, + "bin": { + "pkg": "lib-es5/bin.js" + }, + "peerDependencies": { + "node-notifier": ">=9.0.1" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-fetch": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-3.4.2.tgz", + "integrity": "sha512-0+uijmzYcnhC0hStDjm/cl2VYdrmVVBpe7Q8k9YBojxmR5tG8mvR9/nooQq3QSXiQqORDVOTY3XqMEqJVIzkHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "fs-extra": "^9.1.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.6", + "progress": "^2.0.3", + "semver": "^7.3.5", + "tar-fs": "^2.1.1", + "yargs": "^16.2.0" + }, + "bin": { + "pkg-fetch": "lib-es5/bin.js" + } + }, + "node_modules/pkg-fetch/node_modules/cliui": { + "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, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/pkg-fetch/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pkg-fetch/node_modules/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, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/pkg/node_modules/@babel/generator": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.2.tgz", + "integrity": "sha512-W1lG5vUwFvfMd8HVXqdfbuG7RuaSrTCCD8cl8fP8wOivdbtbIg2Db3IWUcgvfxKbbn6ZBGYRW/Zk1MIwK49mgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/pkg/node_modules/@babel/parser": { + "version": "7.18.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.4.tgz", + "integrity": "sha512-FDge0dFazETFcxGw/EXzOkN8uJp0PC7Qbm+Pe9T+av2zlBpOgunFHkQPPn+eRuClU73JF+98D531UgayY89tow==", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pkg/node_modules/@babel/types": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.0.tgz", + "integrity": "sha512-YuGopBq3ke25BVSiS6fgF49Ul9gH1x70Bcr6bqRLjWCkcX8Hre1/5+z+IiWOIerRMSSEfGZVB9z9kyq7wVs9YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.18.10", + "@babel/helper-validator-identifier": "^7.18.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/pkg/node_modules/is-core-module": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/pkg/node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz", + "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^28.1.3", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/pretty-format/node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.24.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + } + }, + "node_modules/pretty-format/node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format/node_modules/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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.1.tgz", + "integrity": "sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shiki": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stream-meter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", + "integrity": "sha512-4sOEtrbgFotXwnEuzzsQBYEV1elAeFSO8rSGeTwabuX1RRn/kEq9JVH7I0MRBhKVRR0sJkr0M0QCH7yOLf9fhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.1.4" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } }, - "secure-random-bytes": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/secure-random-bytes/-/secure-random-bytes-1.0.1.tgz", - "integrity": "sha1-Df3kRtCKVRN/jkXCYt5M+TlsWtQ=", - "requires": { - "secure-random-octet": "1.0.2" + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/threads": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.7.0.tgz", + "integrity": "sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==", + "license": "MIT", + "dependencies": { + "callsites": "^3.1.0", + "debug": "^4.2.0", + "is-observable": "^2.1.0", + "observable-fns": "^0.6.1" + }, + "funding": { + "url": "https://github.com/andywer/threads.js?sponsor=1" + }, + "optionalDependencies": { + "tiny-worker": ">= 2" } }, - "secure-random-octet": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/secure-random-octet/-/secure-random-octet-1.0.2.tgz", - "integrity": "sha1-fIdC7l7CxODZJjvQdSjyMT7cG80=", - "requires": { - "get-random-values": "1.2.0" + "node_modules/tiny-worker": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz", + "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "esm": "^3.2.25" } }, - "semver": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz", - "integrity": "sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto=", - "dev": true + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" }, - "serializerr": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/serializerr/-/serializerr-1.0.3.tgz", - "integrity": "sha1-EtTFqhw/+49tHcXzlaqUVVacP5E=", + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, - "requires": { - "protochain": "1.0.5" + "license": "MIT", + "engines": { + "node": ">=4" } }, - "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 - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true - }, - "simple-fmt": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/simple-fmt/-/simple-fmt-0.1.0.tgz", - "integrity": "sha1-GRv1ZqWeZTBILLJatTtKjchcOms=", - "dev": true + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } }, - "simple-is": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/simple-is/-/simple-is-0.2.0.tgz", - "integrity": "sha1-Krt1qt453rXMgVzhDmGRFkhQuvA=", - "dev": true + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "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 - }, - "source-map-support": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.2.10.tgz", - "integrity": "sha1-6lo5AKHByyUJagrozFwrSxDe09w=", - "dev": true, - "requires": { - "source-map": "0.1.32" - }, - "dependencies": { - "source-map": { - "version": "0.1.32", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.32.tgz", - "integrity": "sha1-yLbBZ3l7pHQKjqMyUhYv8IWRsmY=", - "dev": true, - "requires": { - "amdefine": "1.0.1" - } + "node_modules/ts-jest": { + "version": "28.0.8", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz", + "integrity": "sha512-5FaG0lXmRPzApix8oFG8RKjAz4ehtm8yMKOTy5HX3fY6W8kmvOrmcY0hKDElW52FJov+clhUbrKAqofnj4mXTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^28.0.0", + "json5": "^2.2.1", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^28.0.0", + "babel-jest": "^28.0.0", + "jest": "^28.0.0", + "typescript": ">=4.3" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true } } }, - "spawn-sync": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "requires": { - "concat-stream": "1.6.2", - "os-shim": "0.1.3" + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } } }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } }, - "standard": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/standard/-/standard-4.5.4.tgz", - "integrity": "sha1-Aj53ISbevAxFlmInDEVwOVSR4CE=", - "dev": true, - "requires": { - "deglob": "1.1.2", - "dezalgo": "1.0.3", - "eslint": "0.24.1", - "eslint-config-standard": "3.4.1", - "eslint-config-standard-react": "1.0.1", - "eslint-plugin-react": "2.7.1", - "find-root": "0.1.2", - "get-stdin": "4.0.1", - "minimist": "1.2.0", - "pkg-config": "1.1.1", - "standard-format": "1.6.10", - "xtend": "4.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, - "standard-format": { - "version": "1.6.10", - "resolved": "https://registry.npmjs.org/standard-format/-/standard-format-1.6.10.tgz", - "integrity": "sha1-sYPI+DfI05OHmPPQlD5dgHoboD8=", - "dev": true, - "requires": { - "deglob": "1.1.2", - "esformatter": "0.8.2", - "esformatter-eol-last": "1.0.0", - "esformatter-jsx": "2.3.11", - "esformatter-literal-notation": "1.0.1", - "esformatter-quotes": "1.1.0", - "esformatter-semicolon-first": "1.2.0", - "esformatter-spaced-lined-comment": "2.0.1", - "minimist": "1.2.0", - "stdin": "0.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "stdin": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/stdin/-/stdin-0.0.1.tgz", - "integrity": "sha1-0wQZgarsPf28d6GzjWNy449ftx4=", - "dev": true + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } }, - "string.prototype.endswith": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/string.prototype.endswith/-/string.prototype.endswith-0.2.0.tgz", - "integrity": "sha1-oZwg3uUamHd+mkfhDwm+OTubunU=", - "dev": true + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "5.1.2" + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" } }, - "stringmap": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stringmap/-/stringmap-0.2.2.tgz", - "integrity": "sha1-VWwTeyWPlCuHdvWy71gqoGnX0bE=", - "dev": true + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "stringset": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/stringset/-/stringset-0.2.1.tgz", - "integrity": "sha1-7yWcTjSTRDd/zRyRPdLoSMnAQrU=", - "dev": true + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, - "requires": { - "ansi-regex": "2.1.1" + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "strip-json-comments": { + "node_modules/typed-array-byte-offset": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "sync-exec": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/sync-exec/-/sync-exec-0.5.0.tgz", - "integrity": "sha1-P3JY5KW6FyRTgZCfpqb2z1BuFmE=", - "dev": true - }, - "tape": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/tape/-/tape-3.6.1.tgz", - "integrity": "sha1-SJPdU+KApfWMDOswwsDrs7zVHh8=", - "dev": true, - "requires": { - "deep-equal": "0.2.2", - "defined": "0.0.0", - "glob": "3.2.11", - "inherits": "2.0.3", - "object-inspect": "0.4.0", - "resumer": "0.0.0", - "through": "2.3.8" - }, - "dependencies": { - "defined": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-0.0.0.tgz", - "integrity": "sha1-817qfXBekzuvE7LwOz+D2SFAOz4=", - "dev": true - }, - "glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", - "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", - "dev": true, - "requires": { - "inherits": "2.0.3", - "minimatch": "0.3.0" - } + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedoc": { + "version": "0.23.28", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.23.28.tgz", + "integrity": "sha512-9x1+hZWTHEQcGoP7qFmlo4unUoVJLB0H/8vfO/7wqTnZxg4kPuji9y3uRzEu0ZKez63OJAUmiGhUrtukC6Uj3w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.2.12", + "minimatch": "^7.1.3", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 14.14" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x" + } + }, + "node_modules/typedoc/node_modules/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", + "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" }, - "lru-cache": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", - "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", - "dev": true + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" }, - "minimatch": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", - "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", - "dev": true, - "requires": { - "lru-cache": "2.7.3", - "sigmund": "1.0.1" - } + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "tar-stream": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.1.tgz", - "integrity": "sha512-IFLM5wp3QrJODQFPm6/to3LJZrONdBY/otxcvDIQzu217zKye6yVR3hhi9lAjrC2Z+m/j5oDxMPb1qcd8cIvpA==", - "requires": { - "bl": "1.2.2", - "buffer-alloc": "1.2.0", - "end-of-stream": "1.4.1", - "fs-constants": "1.0.0", - "readable-stream": "2.3.6", - "to-buffer": "1.1.1", - "xtend": "4.0.1" + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" } }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" }, - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, - "try-resolve": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/try-resolve/-/try-resolve-1.0.1.tgz", - "integrity": "sha1-z95vq9ctY+V5fPqrhzq76OcA6RI=", - "dev": true + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true, + "license": "MIT" }, - "tryor": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tryor/-/tryor-0.1.2.tgz", - "integrity": "sha1-gUXkynyv9ArN48z5Rui4u3W0Fys=", - "dev": true + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true, + "license": "MIT" }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, - "requires": { - "prelude-ls": "1.1.2" + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } }, - "user-home": { + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", - "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=", - "dev": true + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "virtualfs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/virtualfs/-/virtualfs-2.1.1.tgz", - "integrity": "sha512-rFm9MX8unuZFqOUHVwFBNaOukoKvAptmYC99j46T5zt8SPX2YoCGE58nVbFRK5qzDYD9XN0ss7dPZ5iZlEm3PQ==", - "requires": { - "babel-runtime": "6.26.0", - "errno": "0.1.7", - "permaproxy": "0.0.2", - "readable-stream": "2.3.6", - "resource-counter": "1.2.4", - "secure-random-bytes": "1.0.1" + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "dev": true, - "requires": { - "isexe": "2.0.0" + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "window-size": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.4.tgz", - "integrity": "sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=", - "dev": true + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true + "node_modules/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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" }, - "xml-escape": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.0.0.tgz", - "integrity": "sha1-AJY9aXsq3wwYXE4E5zF0upsojrI=", - "dev": true + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true - }, - "yargs": { - "version": "3.27.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.27.0.tgz", - "integrity": "sha1-ISBUaTFuk5Ex1Z8toMbX+YIh6kA=", - "dev": true, - "requires": { - "camelcase": "1.2.1", - "cliui": "2.1.0", - "decamelize": "1.2.0", - "os-locale": "1.4.0", - "window-size": "0.1.4", - "y18n": "3.2.1" + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } } diff --git a/package.json b/package.json index b584673..b1853b5 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { - "name": "virtualtar", - "version": "1.16.3", - "description": "virtualfs bindings for tar-stream", - "dependencies": { - "pump": "^1.0.0", - "tar-stream": "^1.1.2" - }, + "name": "@matrixai/js-virtualtar", + "version": "0.0.1", + "author": "Matrix AI", + "contributors": [ + { + "name": "Aryan Jassal" + } + ], + "description": "Virtualised bindings for generating a tar", "keywords": [ "tar", "fs", @@ -14,26 +16,53 @@ "directory", "stream" ], - "devDependencies": { - "rimraf": "^2.2.8", - "standard": "^4.5.4", - "tape": "^3.0.0" + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/MatrixAI/js-virtualtar.git" }, + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { - "test": "standard && tape test/index.js" + "prepare": "tsc -p ./tsconfig.build.json", + "build": "shx rm -rf ./dist && tsc -p ./tsconfig.build.json", + "postversion": "npm install --package-lock-only --ignore-scripts --silent", + "ts-node": "ts-node", + "test": "jest", + "lint": "eslint '{src,tests,scripts,benches}/**/*.{js,ts}'", + "lintfix": "eslint '{src,tests,scripts,benches}/**/*.{js,ts}' --fix", + "lint-shell": "find ./src ./tests ./scripts -type f -regextype posix-extended -regex '.*\\.(sh)' -exec shellcheck {} +", + "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", + "bench": "shx rm -rf ./benches/results && ts-node ./benches" }, - "bugs": { - "url": "https://github.com/mafintosh/tar-fs/issues" - }, - "homepage": "https://github.com/mafintosh/tar-fs", - "main": "index.js", - "directories": { - "test": "test" + "dependencies": { + "@matrixai/logger": "^4.0.3", + "threads": "^1.7.0", + "uuid": "^11.0.5" }, - "author": "Mathias Buus", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/mafintosh/tar-fs.git" + "devDependencies": { + "@swc/core": "^1.3.62", + "@swc/jest": "^0.2.26", + "@types/jest": "^28.1.3", + "@types/node": "^18.15.0", + "@typescript-eslint/eslint-plugin": "^5.45.1", + "@typescript-eslint/parser": "^5.45.1", + "eslint": "^8.15.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^28.1.1", + "jest-extended": "^3.0.1", + "jest-junit": "^14.0.0", + "jest-mock-process": "^2.0.0", + "node-gyp-build": "^4.4.0", + "pkg": "^5.8.1", + "prettier": "^2.6.2", + "shx": "^0.3.4", + "ts-jest": "^28.0.5", + "ts-node": "^10.9.1", + "tsconfig-paths": "^3.9.0", + "typedoc": "^0.23.21", + "typescript": "^4.9.3" } } diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 0e12de2..0000000 --- a/shell.nix +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env nix-shell -{ - pkgs ? import (fetchTarball https://github.com/NixOS/nixpkgs-channels/archive/00e56fbbee06088bf3bf82169032f5f5778588b7.tar.gz) {} -}: - with pkgs; - stdenv.mkDerivation { - name = "js-virtualtar"; - buildInputs = [ python2 nodejs-8_x flow ]; - } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..65b3dba --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +// todo diff --git a/test/fixtures/a/hello.txt b/test/fixtures/a/hello.txt deleted file mode 100644 index 3b18e51..0000000 --- a/test/fixtures/a/hello.txt +++ /dev/null @@ -1 +0,0 @@ -hello world diff --git a/test/fixtures/b/a/test.txt b/test/fixtures/b/a/test.txt deleted file mode 100644 index 9daeafb..0000000 --- a/test/fixtures/b/a/test.txt +++ /dev/null @@ -1 +0,0 @@ -test diff --git a/test/fixtures/c/.gitignore b/test/fixtures/c/.gitignore deleted file mode 100644 index 2b2328d..0000000 --- a/test/fixtures/c/.gitignore +++ /dev/null @@ -1 +0,0 @@ -link diff --git a/test/fixtures/d/file1 b/test/fixtures/d/file1 deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/d/file2 b/test/fixtures/d/file2 deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/d/sub-dir/file5 b/test/fixtures/d/sub-dir/file5 deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/d/sub-files/file3 b/test/fixtures/d/sub-files/file3 deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/d/sub-files/file4 b/test/fixtures/d/sub-files/file4 deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/e/directory/.ignore b/test/fixtures/e/directory/.ignore deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/e/file b/test/fixtures/e/file deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/e/symlink b/test/fixtures/e/symlink deleted file mode 120000 index 97fb028..0000000 --- a/test/fixtures/e/symlink +++ /dev/null @@ -1 +0,0 @@ -symlink \ No newline at end of file diff --git a/test/fixtures/invalid.tar b/test/fixtures/invalid.tar deleted file mode 100644 index a645e9ce59e35b70c748a3c47c5b791c86d61ae3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2560 zcmYex&u5@DFfcGMGci$M0MbB!PD2C@jg8C=jZKZr3{4aa3=IrT3>6rR^z`&;?KXze z;*!K7pwlR|2<#0c&rxa~o%E;W=j$f{1AvwQ7&HXp-&6rD{!PqHf$4;nj)Ivz!s9<9 bH7AGmB>*h2N9oZJ7!85Z5Eu=C!4d)hnE)h9 diff --git a/test/index.js b/test/index.js deleted file mode 100644 index a03844e..0000000 --- a/test/index.js +++ /dev/null @@ -1,346 +0,0 @@ -var test = require('tape') -var rimraf = require('rimraf') -var tar = require('../index') -var tarStream = require('tar-stream') -var path = require('path') -var fs = require('fs') -var os = require('os') - -var win32 = os.platform() === 'win32' - -var mtime = function (st) { - return Math.floor(st.mtime.getTime() / 1000) -} - -test('copy a -> copy/a', function (t) { - t.plan(5) - - var a = path.join(__dirname, 'fixtures', 'a') - var b = path.join(__dirname, 'fixtures', 'copy', 'a') - - rimraf.sync(b) - tar.pack(a) - .pipe(tar.extract(b)) - .on('finish', function () { - var files = fs.readdirSync(b) - t.same(files.length, 1) - t.same(files[0], 'hello.txt') - var fileB = path.join(b, files[0]) - var fileA = path.join(a, files[0]) - t.same(fs.readFileSync(fileB, 'utf-8'), fs.readFileSync(fileA, 'utf-8')) - t.same(fs.statSync(fileB).mode, fs.statSync(fileA).mode) - t.same(mtime(fs.statSync(fileB)), mtime(fs.statSync(fileA))) - }) -}) - -test('copy b -> copy/b', function (t) { - t.plan(8) - - var a = path.join(__dirname, 'fixtures', 'b') - var b = path.join(__dirname, 'fixtures', 'copy', 'b') - - rimraf.sync(b) - tar.pack(a) - .pipe(tar.extract(b)) - .on('finish', function () { - var files = fs.readdirSync(b) - t.same(files.length, 1) - t.same(files[0], 'a') - var dirB = path.join(b, files[0]) - var dirA = path.join(a, files[0]) - t.same(fs.statSync(dirB).mode, fs.statSync(dirA).mode) - t.same(mtime(fs.statSync(dirB)), mtime(fs.statSync(dirA))) - t.ok(fs.statSync(dirB).isDirectory()) - var fileB = path.join(dirB, 'test.txt') - var fileA = path.join(dirA, 'test.txt') - t.same(fs.readFileSync(fileB, 'utf-8'), fs.readFileSync(fileA, 'utf-8')) - t.same(fs.statSync(fileB).mode, fs.statSync(fileA).mode) - t.same(mtime(fs.statSync(fileB)), mtime(fs.statSync(fileA))) - }) -}) - -test('symlink', function (t) { - if (win32) { // no symlink support on win32 currently. TODO: test if this can be enabled somehow - t.plan(1) - t.ok(true) - return - } - - t.plan(5) - - var a = path.join(__dirname, 'fixtures', 'c') - - rimraf.sync(path.join(a, 'link')) - fs.symlinkSync('.gitignore', path.join(a, 'link')) - - var b = path.join(__dirname, 'fixtures', 'copy', 'c') - - rimraf.sync(b) - tar.pack(a) - .pipe(tar.extract(b)) - .on('finish', function () { - var files = fs.readdirSync(b).sort() - t.same(files.length, 2) - t.same(files[0], '.gitignore') - t.same(files[1], 'link') - - var linkA = path.join(a, 'link') - var linkB = path.join(b, 'link') - - t.same(mtime(fs.lstatSync(linkB)), mtime(fs.lstatSync(linkA))) - t.same(fs.readlinkSync(linkB), fs.readlinkSync(linkA)) - }) -}) - -test('follow symlinks', function (t) { - if (win32) { // no symlink support on win32 currently. TODO: test if this can be enabled somehow - t.plan(1) - t.ok(true) - return - } - - t.plan(5) - - var a = path.join(__dirname, 'fixtures', 'c') - - rimraf.sync(path.join(a, 'link')) - fs.symlinkSync('.gitignore', path.join(a, 'link')) - - var b = path.join(__dirname, 'fixtures', 'copy', 'c-dereference') - - rimraf.sync(b) - tar.pack(a, {dereference: true}) - .pipe(tar.extract(b)) - .on('finish', function () { - var files = fs.readdirSync(b).sort() - t.same(files.length, 2) - t.same(files[0], '.gitignore') - t.same(files[1], 'link') - - var file1 = path.join(b, '.gitignore') - var file2 = path.join(b, 'link') - - t.same(mtime(fs.lstatSync(file1)), mtime(fs.lstatSync(file2))) - t.same(fs.readFileSync(file1), fs.readFileSync(file2)) - }) -}) - -test('strip', function (t) { - t.plan(2) - - var a = path.join(__dirname, 'fixtures', 'b') - var b = path.join(__dirname, 'fixtures', 'copy', 'b-strip') - - rimraf.sync(b) - - tar.pack(a) - .pipe(tar.extract(b, {strip: 1})) - .on('finish', function () { - var files = fs.readdirSync(b).sort() - t.same(files.length, 1) - t.same(files[0], 'test.txt') - }) -}) - -test('strip + map', function (t) { - t.plan(2) - - var a = path.join(__dirname, 'fixtures', 'b') - var b = path.join(__dirname, 'fixtures', 'copy', 'b-strip') - - rimraf.sync(b) - - var uppercase = function (header) { - header.name = header.name.toUpperCase() - return header - } - - tar.pack(a) - .pipe(tar.extract(b, {strip: 1, map: uppercase})) - .on('finish', function () { - var files = fs.readdirSync(b).sort() - t.same(files.length, 1) - t.same(files[0], 'TEST.TXT') - }) -}) - -test('map + dir + permissions', function (t) { - t.plan(win32 ? 1 : 2) // skip chmod test, it's not working like unix - - var a = path.join(__dirname, 'fixtures', 'b') - var b = path.join(__dirname, 'fixtures', 'copy', 'a-perms') - - rimraf.sync(b) - - var aWithMode = function (header) { - if (header.name === 'a') { - header.mode = parseInt(700, 8) - } - return header - } - - tar.pack(a) - .pipe(tar.extract(b, {map: aWithMode})) - .on('finish', function () { - var files = fs.readdirSync(b).sort() - var stat = fs.statSync(path.join(b, 'a')) - t.same(files.length, 1) - if (!win32) { - t.same(stat.mode & parseInt(777, 8), parseInt(700, 8)) - } - }) -}) - -test('specific entries', function (t) { - t.plan(6) - - var a = path.join(__dirname, 'fixtures', 'd') - var b = path.join(__dirname, 'fixtures', 'copy', 'd-entries') - - var entries = [ 'file1', 'sub-files/file3', 'sub-dir' ] - - rimraf.sync(b) - tar.pack(a, {entries: entries}) - .pipe(tar.extract(b)) - .on('finish', function () { - var files = fs.readdirSync(b) - t.same(files.length, 3) - t.notSame(files.indexOf('file1'), -1) - t.notSame(files.indexOf('sub-files'), -1) - t.notSame(files.indexOf('sub-dir'), -1) - var subFiles = fs.readdirSync(path.join(b, 'sub-files')) - t.same(subFiles, ['file3']) - var subDir = fs.readdirSync(path.join(b, 'sub-dir')) - t.same(subDir, ['file5']) - }) -}) - -test('check type while mapping header on packing', function (t) { - t.plan(3) - - var e = path.join(__dirname, 'fixtures', 'e') - - var checkHeaderType = function (header) { - if (header.name.indexOf('.') === -1) t.same(header.type, header.name) - } - - tar.pack(e, { map: checkHeaderType }) -}) - -test('finish callbacks', function (t) { - t.plan(3) - - var a = path.join(__dirname, 'fixtures', 'a') - var b = path.join(__dirname, 'fixtures', 'copy', 'a') - - rimraf.sync(b) - - var packEntries = 0 - var extractEntries = 0 - - var countPackEntry = function (header) { packEntries++ } - var countExtractEntry = function (header) { extractEntries++ } - - var pack - var onPackFinish = function (passedPack) { - t.equal(packEntries, 2, 'All entries have been packed') // 2 entries - the file and base directory - t.equal(passedPack, pack, 'The finish hook passes the pack') - } - - var onExtractFinish = function () { t.equal(extractEntries, 2) } - - pack = tar.pack(a, {map: countPackEntry, finish: onPackFinish}) - - pack.pipe(tar.extract(b, {map: countExtractEntry, finish: onExtractFinish})) - .on('finish', function () { - t.end() - }) -}) - -test('not finalizing the pack', function (t) { - t.plan(2) - - var a = path.join(__dirname, 'fixtures', 'a') - var b = path.join(__dirname, 'fixtures', 'b') - - var out = path.join(__dirname, 'fixtures', 'copy', 'merged-packs') - - rimraf.sync(out) - - var prefixer = function (prefix) { - return function (header) { - header.name = path.join(prefix, header.name) - return header - } - } - - tar.pack(a, { - map: prefixer('a-files'), - finalize: false, - finish: packB - }) - - function packB (pack) { - tar.pack(b, {pack: pack, map: prefixer('b-files')}) - .pipe(tar.extract(out)) - .on('finish', assertResults) - } - - function assertResults () { - var containers = fs.readdirSync(out) - t.deepEqual(containers, ['a-files', 'b-files']) - var aFiles = fs.readdirSync(path.join(out, 'a-files')) - t.deepEqual(aFiles, ['hello.txt']) - } -}) - -test('do not extract invalid tar', function (t) { - var a = path.join(__dirname, 'fixtures', 'invalid.tar') - - var out = path.join(__dirname, 'fixtures', 'invalid') - - rimraf.sync(out) - - fs.createReadStream(a) - .pipe(tar.extract(out)) - .on('error', function (err) { - t.ok(/is not a valid path/i.test(err.message)) - fs.stat(path.join(out, '../bar'), function (err) { - t.ok(err) - t.end() - }) - }) -}) - -test('no abs hardlink targets', function (t) { - var out = path.join(__dirname, 'fixtures', 'invalid') - var outside = path.join(__dirname, 'fixtures', 'outside') - - rimraf.sync(out) - - var s = tarStream.pack() - - fs.writeFileSync(outside, 'something') - - s.entry({ - type: 'link', - name: 'link', - linkname: outside - }) - - s.entry({ - name: 'link' - }, 'overwrite') - - s.finalize() - - s.pipe(tar.extract(out)) - .on('error', function (err) { - t.ok(err, 'had error') - fs.readFile(outside, 'utf-8', function (err, str) { - t.error(err, 'no error') - t.same(str, 'something') - t.end() - }) - }) -}) diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..724de44 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "noEmit": false, + "stripInternal": true + }, + "exclude": [ + "./tests/**/*", + "./scripts/**/*", + "./benches/**/*" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a120436 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "tsBuildInfoFile": "./dist/tsbuildinfo", + "incremental": true, + "sourceMap": true, + "declaration": true, + "allowJs": true, + "strictNullChecks": true, + "noImplicitAny": false, + "experimentalDecorators": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "module": "CommonJS", + "target": "ES2022", + "baseUrl": "./src", + "paths": { + "@": ["index"], + "@/*": ["*"] + }, + "noEmit": true + }, + "include": [ + "./src/**/*", + "./src/**/*.json", + "./tests/**/*", + "./scripts/**/*", + "./benches/**/*" + ], + "ts-node": { + "require": ["tsconfig-paths/register"], + "transpileOnly": true, + "swc": true + } +} From 486b553d86c3d2669f0d82e9ea4f5788e0d63119 Mon Sep 17 00:00:00 2001 From: Aryan Jassal <84617406+aryanjassal@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:23:01 +1100 Subject: [PATCH 02/19] chore: updated LICENSE --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++ flake.lock | 78 ++++++++++++++++ flake.nix | 46 +++++++++ package.json | 3 +- src/index.ts | 2 +- tests/global.d.ts | 12 +++ tests/globalSetup.ts | 6 ++ tests/globalTeardown.ts | 6 ++ tests/index.test.ts | 5 + tests/setup.ts | 0 tests/setupAfterEnv.ts | 4 + 11 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 LICENSE create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 tests/global.d.ts create mode 100644 tests/globalSetup.ts create mode 100644 tests/globalTeardown.ts create mode 100644 tests/index.test.ts create mode 100644 tests/setup.ts create mode 100644 tests/setupAfterEnv.ts diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..5c7bdc9 --- /dev/null +++ b/flake.lock @@ -0,0 +1,78 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1736139540, + "narHash": "sha256-39Iclrd+9tPLmvuFVyoG63WnHZJ9kCOC6eRytRYLAWw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8ab83a21276434aaf44969b8dd0bc0e65b97a240", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8ab83a21276434aaf44969b8dd0bc0e65b97a240", + "type": "github" + } + }, + "nixpkgs-matrix": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1736140072, + "narHash": "sha256-MgtcAA+xPldS0WlV16TjJ0qgFzGvKuGM9p+nPUxpUoA=", + "owner": "MatrixAI", + "repo": "nixpkgs-matrix", + "rev": "029084026bc4a35bce81bac898aa695f41993e18", + "type": "github" + }, + "original": { + "id": "nixpkgs-matrix", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs-matrix": "nixpkgs-matrix" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c29cb23 --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + inputs = { + nixpkgs-matrix = { + type = "indirect"; + id = "nixpkgs-matrix"; + }; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { nixpkgs-matrix, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs-matrix.legacyPackages.${system}; + shell = { ci ? false }: + with pkgs; + pkgs.mkShell { + nativeBuildInputs = [ nodejs_20 shellcheck gitAndTools.gh ]; + PKG_IGNORE_TAG = 1; + shellHook = '' + echo "Entering $(npm pkg get name)" + set -o allexport + . <(polykey secrets env js-virtualtar) + set +o allexport + set -v + ${lib.optionalString ci '' + set -o errexit + set -o nounset + set -o pipefail + shopt -s inherit_errexit + ''} + mkdir --parents "$(pwd)/tmp" + + export PATH="$(pwd)/dist/bin:$(npm root)/.bin:$PATH" + + npm install --ignore-scripts + + set +v + ''; + }; + in { + devShells = { + default = shell { ci = false; }; + ci = shell { ci = true; }; + }; + }); +} diff --git a/package.json b/package.json index b1853b5..1b294dd 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,7 @@ "lint": "eslint '{src,tests,scripts,benches}/**/*.{js,ts}'", "lintfix": "eslint '{src,tests,scripts,benches}/**/*.{js,ts}' --fix", "lint-shell": "find ./src ./tests ./scripts -type f -regextype posix-extended -regex '.*\\.(sh)' -exec shellcheck {} +", - "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src", - "bench": "shx rm -rf ./benches/results && ts-node ./benches" + "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src" }, "dependencies": { "@matrixai/logger": "^4.0.3", diff --git a/src/index.ts b/src/index.ts index 65b3dba..6fced85 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1 @@ -// todo +// Starting project soon â„¢ diff --git a/tests/global.d.ts b/tests/global.d.ts new file mode 100644 index 0000000..5342a19 --- /dev/null +++ b/tests/global.d.ts @@ -0,0 +1,12 @@ +/* eslint-disable no-var */ + +/// + +/** + * Follows the globals in jest.config.ts + * @module + */ +declare var projectDir: string; +declare var testDir: string; +declare var defaultTimeout: number; +declare var maxTimeout: number; diff --git a/tests/globalSetup.ts b/tests/globalSetup.ts new file mode 100644 index 0000000..2cb0f05 --- /dev/null +++ b/tests/globalSetup.ts @@ -0,0 +1,6 @@ +async function setup() { + // eslint-disable-next-line no-console + console.log('\nGLOBAL SETUP'); +} + +export default setup; diff --git a/tests/globalTeardown.ts b/tests/globalTeardown.ts new file mode 100644 index 0000000..173ae41 --- /dev/null +++ b/tests/globalTeardown.ts @@ -0,0 +1,6 @@ +async function teardown() { + // eslint-disable-next-line no-console + console.log('GLOBAL TEARDOWN'); +} + +export default teardown; diff --git a/tests/index.test.ts b/tests/index.test.ts new file mode 100644 index 0000000..90b9f60 --- /dev/null +++ b/tests/index.test.ts @@ -0,0 +1,5 @@ +describe('index', () => { + test('test', () => { + expect(true).toBeTruthy(); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/setupAfterEnv.ts b/tests/setupAfterEnv.ts new file mode 100644 index 0000000..8ea8279 --- /dev/null +++ b/tests/setupAfterEnv.ts @@ -0,0 +1,4 @@ +// Default timeout per test +// some tests may take longer in which case you should specify the timeout +// explicitly for each test by using the third parameter of test function +jest.setTimeout(globalThis.defaultTimeout); From 034f89fca2c459ff1d49ab9518afb5ba8c370552 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 12 Feb 2025 18:17:02 +1100 Subject: [PATCH 03/19] feat: added generators for archiving a single file --- .gitignore | 4 ++ src/Generator.ts | 109 ++++++++++++++++++++++++++++++++++++++++++++ src/Parser.ts | 0 src/types.ts | 5 ++ src/utils.ts | 0 tests/index.test.ts | 11 ++++- 6 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/Generator.ts create mode 100644 src/Parser.ts create mode 100644 src/types.ts create mode 100644 src/utils.ts diff --git a/.gitignore b/.gitignore index 84856b0..78a1b31 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,7 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# editor +.vscode/ +.idea/ diff --git a/src/Generator.ts b/src/Generator.ts new file mode 100644 index 0000000..0cfda41 --- /dev/null +++ b/src/Generator.ts @@ -0,0 +1,109 @@ +import type { TarType } from './types'; +import fs from 'fs'; + +/** + * The size for each tar block. This is usually 512 bytes. + */ +const BLOCK_SIZE = 512; + +function computeChecksum(header: Buffer): number { + let sum = 0; + for (let i = 0; i < BLOCK_SIZE; i++) { + sum += i >= 148 && i < 156 ? 32 : header[i]; // Fill checksum with spaces + } + return sum; +} + +function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer { + const size = type === '0' ? stat.size : 0; + + const header = Buffer.alloc(BLOCK_SIZE, 0); + + // The TAR headers follow this structure + // Start Size Description + // ------------------------------ + // 0 100 File name (first 100 bytes) + // 100 8 File permissions (null-padded octal) + // 108 8 Owner UID (null-padded octal) + // 116 8 Owner GID (null-padded octal) + // 124 12 File size (null-padded octal, 0 for directories) + // 136 12 Mtime (null-padded octal) + // 148 8 Checksum (fill with ASCII spaces for computation) + // 156 1 Type flag (0 for file, 5 for directory) + // 157 100 File owner name (null-terminated ASCII/UTF-8) + // 257 6 'ustar\0' (magic string) + // 263 2 '00' (ustar version) + // 265 32 Owner user name (null-terminated ASCII/UTF-8) + // 297 32 Owner group name (null-terminated ASCII/UTF-8) + // 329 8 Device major (unset in this implementation) + // 337 8 Device minor (unset in this implementation) + // 345 155 File name (last 155 bytes, total 255 bytes, null-padded) + // 500 12 '\0' (unused) + + // FIXME: Assuming file path is under 100 characters long + header.write(filePath, 0, 100, 'utf8'); + // File permissions name will be null + // Owner uid will be null + // Owner gid will be null + header.write(size.toString(8).padStart(7, '0') + '\0', 124, 12, 'ascii'); + // Mtime will be null + header.write(' ', 148, 8, 'ascii'); // Placeholder for checksum + header.write(type, 156, 1, 'ascii'); + // File owner name will be null + header.write('ustar\0', 257, 'ascii'); + header.write('00', 263, 2, 'ascii'); + // Owner user name will be null + // Owner group name will be null + // Device major will be null + // Device minor will be null + // Extended file name will be null + + // Updating with the new checksum + const checksum = computeChecksum(header); + header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii'); + + return header; +} + +async function* readFile(filePath: string): AsyncGenerator { + const fileHandle = await fs.promises.open(filePath, 'r'); + const buffer = Buffer.alloc(BLOCK_SIZE); + let bytesRead = -1; // Initialisation value + + try { + while (bytesRead !== 0) { + buffer.fill(0); + const result = await fileHandle.read(buffer, 0, BLOCK_SIZE, null); + bytesRead = result.bytesRead; + + if (bytesRead === 0) break; // EOF reached + if (bytesRead < 512) buffer.fill(0, bytesRead, BLOCK_SIZE); + + yield buffer; + } + } finally { + await fileHandle.close(); + } +} + +// TODO: change path from filepath to a basedir (plus get a fs) +async function* createTar(filePath: string): AsyncGenerator { + // Create header + const stat = await fs.promises.stat(filePath); + yield createHeader(filePath, stat, '0'); + // Get file contents + yield *readFile(filePath); + // End-of-archive marker + yield Buffer.alloc(BLOCK_SIZE, 0); + yield Buffer.alloc(BLOCK_SIZE, 0); +} + +async function writeArchive(inputFile: string, outputFile: string) { + const fileHandle = await fs.promises.open(outputFile, 'w+'); + for await (const chunk of createTar(inputFile)) { + await fileHandle.write(chunk); + } + await fileHandle.close(); +} + +export { createHeader, readFile, createTar, writeArchive }; diff --git a/src/Parser.ts b/src/Parser.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..68fcbf1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,5 @@ +// 0 = FILE +// 5 = DIRECTORY +type TarType = '0' | '5'; + +export { TarType }; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..e69de29 diff --git a/tests/index.test.ts b/tests/index.test.ts index 90b9f60..b887a90 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,5 +1,12 @@ +import { writeArchive } from '@/Generator'; + describe('index', () => { - test('test', () => { - expect(true).toBeTruthy(); + test('test', async () => { + await expect( + writeArchive( + '/home/aryanj/Downloads/tar/FILE.txt', + '/home/aryanj/Downloads/tar/FILE.tar', + ), + ).toResolve(); }); }); From 1a96be6a6432c9f3ffc103f8a57d15f026f0844e Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 12 Feb 2025 19:29:28 +1100 Subject: [PATCH 04/19] feat: added recursive directory walking --- package-lock.json | 19 ++++++++++++ package.json | 1 + src/Generator.ts | 75 ++++++++++++++++++++++++++++++--------------- src/errors.ts | 19 ++++++++++++ src/types.ts | 20 +++++++++--- src/utils.ts | 7 +++++ tests/index.test.ts | 28 ++++++++++++----- 7 files changed, 133 insertions(+), 36 deletions(-) create mode 100644 src/errors.ts diff --git a/package-lock.json b/package-lock.json index 25f2db3..913ece3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "Apache-2.0", "dependencies": { + "@matrixai/errors": "^1.1.7", "@matrixai/logger": "^4.0.3", "threads": "^1.7.0", "uuid": "^11.0.5" @@ -1200,6 +1201,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@matrixai/errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@matrixai/errors/-/errors-1.2.0.tgz", + "integrity": "sha512-eZHPHFla5GFmi0O0yGgbtkca+ZjwpDbMz+60NC3y+DzQq6BMoe4gHmPjDalAHTxyxv0+Q+AWJTuV8Ows+IqBfQ==", + "license": "Apache-2.0", + "dependencies": { + "ts-custom-error": "3.2.2" + } + }, "node_modules/@matrixai/logger": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@matrixai/logger/-/logger-4.0.3.tgz", @@ -7896,6 +7906,15 @@ "dev": true, "license": "MIT" }, + "node_modules/ts-custom-error": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.2.tgz", + "integrity": "sha512-u0YCNf2lf6T/vHm+POKZK1yFKWpSpJitcUN3HxqyEcFuNnHIDbyuIQC7QDy/PsBX3giFyk9rt6BFqBAh2lsDZQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-jest": { "version": "28.0.8", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz", diff --git a/package.json b/package.json index 1b294dd..16734a4 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "docs": "shx rm -rf ./docs && typedoc --gitRevision master --tsconfig ./tsconfig.build.json --out ./docs src" }, "dependencies": { + "@matrixai/errors": "^1.1.7", "@matrixai/logger": "^4.0.3", "threads": "^1.7.0", "uuid": "^11.0.5" diff --git a/src/Generator.ts b/src/Generator.ts index 0cfda41..7133bbb 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -1,5 +1,8 @@ -import type { TarType } from './types'; +import type { TarType, DirectoryContent } from './types'; import fs from 'fs'; +import path from 'path'; +import { TarTypes } from './types'; +import * as errors from './errors'; /** * The size for each tar block. This is usually 512 bytes. @@ -15,8 +18,13 @@ function computeChecksum(header: Buffer): number { } function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer { - const size = type === '0' ? stat.size : 0; + if (filePath.length < 1 || filePath.length > 255) { + throw new errors.ErrorVirtualTarInvalidFileName( + 'The file name must be longer than 1 character and shorter than 255 characters', + ); + } + const size = type === TarTypes.FILE ? stat.size : 0; const header = Buffer.alloc(BLOCK_SIZE, 0); // The TAR headers follow this structure @@ -40,11 +48,10 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer { // 345 155 File name (last 155 bytes, total 255 bytes, null-padded) // 500 12 '\0' (unused) - // FIXME: Assuming file path is under 100 characters long - header.write(filePath, 0, 100, 'utf8'); - // File permissions name will be null - // Owner uid will be null - // Owner gid will be null + header.write(filePath.slice(0, 99).padEnd(100, '\0'), 0, 100, 'utf8'); + header.write(stat.mode.toString(8).padStart(7, '0') + '\0', 100, 12, 'ascii'); + header.write(stat.uid.toString(8).padStart(7, '0') + '\0', 108, 12, 'ascii'); + header.write(stat.gid.toString(8).padStart(7, '0') + '\0', 116, 12, 'ascii'); header.write(size.toString(8).padStart(7, '0') + '\0', 124, 12, 'ascii'); // Mtime will be null header.write(' ', 148, 8, 'ascii'); // Placeholder for checksum @@ -56,7 +63,7 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer { // Owner group name will be null // Device major will be null // Device minor will be null - // Extended file name will be null + header.write(filePath.slice(100).padEnd(155, '\0'), 345, 155, 'utf8'); // Updating with the new checksum const checksum = computeChecksum(header); @@ -86,24 +93,44 @@ async function* readFile(filePath: string): AsyncGenerator { } } -// TODO: change path from filepath to a basedir (plus get a fs) -async function* createTar(filePath: string): AsyncGenerator { - // Create header - const stat = await fs.promises.stat(filePath); - yield createHeader(filePath, stat, '0'); - // Get file contents - yield *readFile(filePath); - // End-of-archive marker - yield Buffer.alloc(BLOCK_SIZE, 0); - yield Buffer.alloc(BLOCK_SIZE, 0); +/** + * Traverse a directory recursively and yield file entries. + */ +async function* walkDirectory( + baseDir: string, + relativePath: string = '', +): AsyncGenerator { + const entries = await fs.promises.readdir(path.join(baseDir, relativePath)); + + // Sort the entries lexicographically + for (const entry of entries.sort()) { + const fullPath = path.join(baseDir, relativePath, entry); + const stat = await fs.promises.stat(fullPath); + const tarPath = path.join(relativePath, entry); + + if (stat.isDirectory()) { + yield { path: tarPath + '/', stat: stat, type: TarTypes.DIRECTORY }; + yield* walkDirectory(baseDir, path.join(relativePath, entry)); + } else if (stat.isFile()) { + yield { path: tarPath, stat: stat, type: TarTypes.FILE }; + } + } } -async function writeArchive(inputFile: string, outputFile: string) { - const fileHandle = await fs.promises.open(outputFile, 'w+'); - for await (const chunk of createTar(inputFile)) { - await fileHandle.write(chunk); +async function* createTar(baseDir: string): AsyncGenerator { + for await (const entry of walkDirectory(baseDir)) { + // Create header + yield createHeader(entry.path, entry.stat, entry.type); + + if (entry.type === TarTypes.FILE) { + // Get file contents + yield* readFile(path.join(baseDir, entry.path)); + } } - await fileHandle.close(); + + // End-of-archive marker - two 512-byte null blocks + yield Buffer.alloc(BLOCK_SIZE, 0); + yield Buffer.alloc(BLOCK_SIZE, 0); } -export { createHeader, readFile, createTar, writeArchive }; +export { createHeader, readFile, createTar }; diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..cd0af52 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,19 @@ +import { AbstractError } from '@matrixai/errors'; + +class ErrorVirtualTar extends AbstractError { + static description = 'VirtualTar errors'; +} + +class ErrorVirtualTarUndefinedBehaviour extends ErrorVirtualTar { + static description = 'You should never see this error'; +} + +class ErrorVirtualTarInvalidFileName extends ErrorVirtualTar { + static description = 'The provided file name is invalid'; +} + +export { + ErrorVirtualTar, + ErrorVirtualTarUndefinedBehaviour, + ErrorVirtualTarInvalidFileName, +}; diff --git a/src/types.ts b/src/types.ts index 68fcbf1..dba3d18 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,17 @@ -// 0 = FILE -// 5 = DIRECTORY -type TarType = '0' | '5'; +import type { Stats } from 'fs'; -export { TarType }; +const TarTypes = { + FILE: '0', + DIRECTORY: '5', +} as const; + +type TarType = (typeof TarTypes)[keyof typeof TarTypes]; + +type DirectoryContent = { + path: string; + stat: Stats; + type: TarType; +}; + +export type { TarType, DirectoryContent }; +export { TarTypes }; diff --git a/src/utils.ts b/src/utils.ts index e69de29..c5886e7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -0,0 +1,7 @@ +import * as errors from './errors'; + +function never(message: string): never { + throw new errors.ErrorVirtualTarUndefinedBehaviour(message); +} + +export { never }; diff --git a/tests/index.test.ts b/tests/index.test.ts index b887a90..bc6a5e1 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,12 +1,24 @@ -import { writeArchive } from '@/Generator'; +import fs from 'fs'; +import { createTar } from '@/Generator'; +// TODO: actually write tests describe('index', () => { test('test', async () => { - await expect( - writeArchive( - '/home/aryanj/Downloads/tar/FILE.txt', - '/home/aryanj/Downloads/tar/FILE.tar', - ), - ).toResolve(); - }); + if (process.env['CI'] != null) { + // Skip this test if on CI + expect(true).toEqual(true); + } else { + // Otherwise, run the test which creates a test archive + const writeArchive = async (inputFile: string, outputFile: string) => { + const fileHandle = await fs.promises.open(outputFile, 'w+'); + for await (const chunk of createTar(inputFile)) { + await fileHandle.write(chunk); + } + await fileHandle.close(); + }; + await expect( + writeArchive('/home/aryanj/Downloads', '/home/aryanj/archive.tar'), + ).toResolve(); + } + }, 60000); }); From 301d6a6f70e377c8a129d9f4b1116b2d752d3160 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 12 Feb 2025 22:08:19 +1100 Subject: [PATCH 05/19] feat: added options to tar generator --- src/Generator.ts | 126 ++++++++++++++++++++++++++++++++++------------- src/errors.ts | 5 ++ src/types.ts | 42 ++++++++++++++-- 3 files changed, 135 insertions(+), 38 deletions(-) diff --git a/src/Generator.ts b/src/Generator.ts index 7133bbb..be8ec2d 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -1,31 +1,62 @@ -import type { TarType, DirectoryContent } from './types'; +import type { + EntryType, + DirectoryContent, + HeaderOptions, + ReadFileOptions, + WalkDirectoryOptions, + TarOptions, +} from './types'; import fs from 'fs'; import path from 'path'; -import { TarTypes } from './types'; +import { EntryTypes } from './types'; import * as errors from './errors'; -/** - * The size for each tar block. This is usually 512 bytes. - */ -const BLOCK_SIZE = 512; +// Set defaults to the options used by the generators +const defaultHeaderOptions: HeaderOptions = { + fileNameEncoding: 'utf8', + blockSize: 512, +}; +const defaultReadFileOptions: ReadFileOptions = { + fs: fs.promises, + blockSize: 512, +}; +const defaultWalkDirectoryOptions: WalkDirectoryOptions = { + fs: fs.promises, + blockSize: 512, +}; +const defaultTarOptions: TarOptions = { + fs: fs.promises, + blockSize: 512, + fileNameEncoding: 'utf8', +}; function computeChecksum(header: Buffer): number { - let sum = 0; - for (let i = 0; i < BLOCK_SIZE; i++) { - sum += i >= 148 && i < 156 ? 32 : header[i]; // Fill checksum with spaces + if (!header.subarray(148, 156).every((byte) => byte === 32)) { + throw new errors.ErrorVirtualTarInvalidHeader( + 'Checksum field is not properly initialized with spaces', + ); } - return sum; + return header.reduce((sum, byte) => sum + byte, 0); } -function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer { +function createHeader( + filePath: string, + stat: fs.Stats, + type: EntryType, + options: Partial = defaultHeaderOptions, +): Buffer { if (filePath.length < 1 || filePath.length > 255) { throw new errors.ErrorVirtualTarInvalidFileName( 'The file name must be longer than 1 character and shorter than 255 characters', ); } - const size = type === TarTypes.FILE ? stat.size : 0; - const header = Buffer.alloc(BLOCK_SIZE, 0); + // Merge the defaults with the provided options + const opts: HeaderOptions = { ...defaultHeaderOptions, ...options }; + + const size = type === EntryTypes.FILE ? stat.size : 0; + const time = parseInt((stat.mtime.getTime() / 1000).toFixed(0)); // Unix time + const header = Buffer.alloc(opts.blockSize, 0); // The TAR headers follow this structure // Start Size Description @@ -48,12 +79,17 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer { // 345 155 File name (last 155 bytes, total 255 bytes, null-padded) // 500 12 '\0' (unused) - header.write(filePath.slice(0, 99).padEnd(100, '\0'), 0, 100, 'utf8'); + header.write( + filePath.slice(0, 99).padEnd(100, '\0'), + 0, + 100, + opts.fileNameEncoding, + ); header.write(stat.mode.toString(8).padStart(7, '0') + '\0', 100, 12, 'ascii'); header.write(stat.uid.toString(8).padStart(7, '0') + '\0', 108, 12, 'ascii'); header.write(stat.gid.toString(8).padStart(7, '0') + '\0', 116, 12, 'ascii'); header.write(size.toString(8).padStart(7, '0') + '\0', 124, 12, 'ascii'); - // Mtime will be null + header.write(time.toString(8).padStart(7, '0') + '\0', 136, 12, 'ascii'); header.write(' ', 148, 8, 'ascii'); // Placeholder for checksum header.write(type, 156, 1, 'ascii'); // File owner name will be null @@ -63,7 +99,12 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer { // Owner group name will be null // Device major will be null // Device minor will be null - header.write(filePath.slice(100).padEnd(155, '\0'), 345, 155, 'utf8'); + header.write( + filePath.slice(100).padEnd(155, '\0'), + 345, + 155, + opts.fileNameEncoding, + ); // Updating with the new checksum const checksum = computeChecksum(header); @@ -72,19 +113,23 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer { return header; } -async function* readFile(filePath: string): AsyncGenerator { - const fileHandle = await fs.promises.open(filePath, 'r'); - const buffer = Buffer.alloc(BLOCK_SIZE); +async function* readFile( + filePath: string, + options: Partial = defaultReadFileOptions, +): AsyncGenerator { + const opts: ReadFileOptions = { ...defaultReadFileOptions, ...options }; + const fileHandle = await opts.fs.open(filePath, 'r'); + const buffer = Buffer.alloc(opts.blockSize); let bytesRead = -1; // Initialisation value try { while (bytesRead !== 0) { buffer.fill(0); - const result = await fileHandle.read(buffer, 0, BLOCK_SIZE, null); + const result = await fileHandle.read(buffer, 0, opts.blockSize, null); bytesRead = result.bytesRead; if (bytesRead === 0) break; // EOF reached - if (bytesRead < 512) buffer.fill(0, bytesRead, BLOCK_SIZE); + if (bytesRead < 512) buffer.fill(0, bytesRead, opts.blockSize); yield buffer; } @@ -99,38 +144,53 @@ async function* readFile(filePath: string): AsyncGenerator { async function* walkDirectory( baseDir: string, relativePath: string = '', + options: Partial = defaultWalkDirectoryOptions, ): AsyncGenerator { - const entries = await fs.promises.readdir(path.join(baseDir, relativePath)); + const opts: WalkDirectoryOptions = { + ...defaultWalkDirectoryOptions, + ...options, + }; + const entries = await opts.fs.readdir(path.join(baseDir, relativePath)); // Sort the entries lexicographically for (const entry of entries.sort()) { const fullPath = path.join(baseDir, relativePath, entry); - const stat = await fs.promises.stat(fullPath); + const stat = await opts.fs.stat(fullPath); const tarPath = path.join(relativePath, entry); if (stat.isDirectory()) { - yield { path: tarPath + '/', stat: stat, type: TarTypes.DIRECTORY }; + yield { path: tarPath + '/', stat: stat, type: EntryTypes.DIRECTORY }; yield* walkDirectory(baseDir, path.join(relativePath, entry)); } else if (stat.isFile()) { - yield { path: tarPath, stat: stat, type: TarTypes.FILE }; + yield { path: tarPath, stat: stat, type: EntryTypes.FILE }; } } } -async function* createTar(baseDir: string): AsyncGenerator { - for await (const entry of walkDirectory(baseDir)) { - // Create header +async function* createTar( + baseDir: string, + options: Partial = defaultTarOptions, +): AsyncGenerator { + const opts = { ...defaultTarOptions, ...options }; + const entryGen = walkDirectory(baseDir, '', { + fs: opts.fs, + blockSize: opts.blockSize, + }); + + for await (const entry of entryGen) { yield createHeader(entry.path, entry.stat, entry.type); - if (entry.type === TarTypes.FILE) { - // Get file contents - yield* readFile(path.join(baseDir, entry.path)); + if (entry.type === EntryTypes.FILE) { + yield* readFile(path.join(baseDir, entry.path), { + fs: opts.fs, + blockSize: opts.blockSize, + }); } } // End-of-archive marker - two 512-byte null blocks - yield Buffer.alloc(BLOCK_SIZE, 0); - yield Buffer.alloc(BLOCK_SIZE, 0); + yield Buffer.alloc(opts.blockSize, 0); + yield Buffer.alloc(opts.blockSize, 0); } export { createHeader, readFile, createTar }; diff --git a/src/errors.ts b/src/errors.ts index cd0af52..bf71ac8 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -12,8 +12,13 @@ class ErrorVirtualTarInvalidFileName extends ErrorVirtualTar { static description = 'The provided file name is invalid'; } +class ErrorVirtualTarInvalidHeader extends ErrorVirtualTar { + static description = 'The header has invalid data'; +} + export { ErrorVirtualTar, ErrorVirtualTarUndefinedBehaviour, ErrorVirtualTarInvalidFileName, + ErrorVirtualTarInvalidHeader, }; diff --git a/src/types.ts b/src/types.ts index dba3d18..a6cf24b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,17 +1,49 @@ import type { Stats } from 'fs'; -const TarTypes = { +const EntryTypes = { FILE: '0', DIRECTORY: '5', } as const; -type TarType = (typeof TarTypes)[keyof typeof TarTypes]; +type EntryType = (typeof EntryTypes)[keyof typeof EntryTypes]; type DirectoryContent = { path: string; stat: Stats; - type: TarType; + type: EntryType; }; -export type { TarType, DirectoryContent }; -export { TarTypes }; +type HeaderOptions = { + fileNameEncoding: 'ascii' | 'utf8'; + blockSize: number; +}; + +// An actual type for `fs` doesn't exist +type ReadFileOptions = { + fs: any; + blockSize: number; +}; + +// An actual type for `fs` doesn't exist +type WalkDirectoryOptions = { + fs: any; + blockSize: number; +}; + +// An actual type for `fs` doesn't exist +type TarOptions = { + fs: any; + blockSize: number; + fileNameEncoding: 'ascii' | 'utf8'; +}; + +export type { + EntryType, + DirectoryContent, + HeaderOptions, + ReadFileOptions, + WalkDirectoryOptions, + TarOptions, +}; + +export { EntryTypes }; From 23a3a6b501eca62e4e59bf6a14977af9d068bbd8 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 19 Feb 2025 15:02:49 +1100 Subject: [PATCH 06/19] feat: restructured project to be more functional --- src/Generator.ts | 299 +++++++++++++++++++++++--------------------- src/constants.ts | 4 + src/errors.ts | 5 + src/types.ts | 97 +++++++------- src/utils.ts | 28 ++++- tests/index.test.ts | 76 +++++++++-- 6 files changed, 315 insertions(+), 194 deletions(-) create mode 100644 src/constants.ts diff --git a/src/Generator.ts b/src/Generator.ts index be8ec2d..aa164a8 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -1,35 +1,10 @@ -import type { - EntryType, - DirectoryContent, - HeaderOptions, - ReadFileOptions, - WalkDirectoryOptions, - TarOptions, -} from './types'; -import fs from 'fs'; -import path from 'path'; -import { EntryTypes } from './types'; +import type { FileStat } from './types'; +import { EntryType, HeaderSize, HeaderOffset } from './types'; import * as errors from './errors'; +import * as utils from './utils'; +import * as constants from './constants'; -// Set defaults to the options used by the generators -const defaultHeaderOptions: HeaderOptions = { - fileNameEncoding: 'utf8', - blockSize: 512, -}; -const defaultReadFileOptions: ReadFileOptions = { - fs: fs.promises, - blockSize: 512, -}; -const defaultWalkDirectoryOptions: WalkDirectoryOptions = { - fs: fs.promises, - blockSize: 512, -}; -const defaultTarOptions: TarOptions = { - fs: fs.promises, - blockSize: 512, - fileNameEncoding: 'utf8', -}; - +// Computes the checksum by adding the value of every single byte in the header function computeChecksum(header: Buffer): number { if (!header.subarray(148, 156).every((byte) => byte === 32)) { throw new errors.ErrorVirtualTarInvalidHeader( @@ -39,24 +14,41 @@ function computeChecksum(header: Buffer): number { return header.reduce((sum, byte) => sum + byte, 0); } +// TODO: Should logging be included? function createHeader( filePath: string, - stat: fs.Stats, + stat: FileStat, type: EntryType, - options: Partial = defaultHeaderOptions, ): Buffer { + // TODO: implement long-file-name headers if (filePath.length < 1 || filePath.length > 255) { throw new errors.ErrorVirtualTarInvalidFileName( 'The file name must be longer than 1 character and shorter than 255 characters', ); } - // Merge the defaults with the provided options - const opts: HeaderOptions = { ...defaultHeaderOptions, ...options }; + // The file path must not contain any directories, and must only contain a + // file name. This guard checks that. + if (filePath.includes('/')) { + throw new errors.ErrorVirtualTarInvalidFileName( + 'File name must not contain /', + ); + } + + // As the size does not matter for directories, it can be undefined. However, + // if the header is being generated for a file, then it needs to have a valid + // size. This guard checks that. + if (stat.size == null && type === EntryType.FILE) { + throw new errors.ErrorVirtualTarInvalidStat('Size must be set for files'); + } + const size = type === EntryType.FILE ? stat.size : 0; + + // The time can be undefined, which would be referring to epoch 0. + const time = utils.dateToUnixTime(stat.mtime ?? new Date()); - const size = type === EntryTypes.FILE ? stat.size : 0; - const time = parseInt((stat.mtime.getTime() / 1000).toFixed(0)); // Unix time - const header = Buffer.alloc(opts.blockSize, 0); + // Make sure to initialise the header with zeros to avoid writing nullish + // blocks. + const header = Buffer.alloc(constants.BLOCK_SIZE, 0); // The TAR headers follow this structure // Start Size Description @@ -68,7 +60,7 @@ function createHeader( // 124 12 File size (null-padded octal, 0 for directories) // 136 12 Mtime (null-padded octal) // 148 8 Checksum (fill with ASCII spaces for computation) - // 156 1 Type flag (0 for file, 5 for directory) + // 156 1 Type flag ('0' for file, '5' for directory) // 157 100 File owner name (null-terminated ASCII/UTF-8) // 257 6 'ustar\0' (magic string) // 263 2 '00' (ustar version) @@ -78,119 +70,146 @@ function createHeader( // 337 8 Device minor (unset in this implementation) // 345 155 File name (last 155 bytes, total 255 bytes, null-padded) // 500 12 '\0' (unused) + // + // Note that all values are in ASCII format, which is different from the + // default formatting of UTF-8 for Buffer.write(). All numbers are also in + // octal format as opposed to decimal or hexadecimal. + + // The first half of the file name (upto 100 bytes) is stored here. + header.write( + utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME), + HeaderOffset.FILE_NAME, + HeaderSize.FILE_NAME, + constants.HEADER_ENCODING, + ); + + // The file permissions, or the mode, is stored in the next chunk. This is + // stored in an octal number format. + header.write( + utils.pad(stat.mode ?? '', HeaderSize.FILE_MODE, '0', '\0'), + HeaderOffset.FILE_MODE, + HeaderSize.FILE_MODE, + constants.HEADER_ENCODING, + ); + + // The owner UID is stored in this chunk + header.write( + utils.pad(stat.uid ?? '', HeaderSize.OWNER_UID, '0', '\0'), + HeaderOffset.OWNER_UID, + HeaderSize.OWNER_UID, + constants.HEADER_ENCODING, + ); + + // The owner GID is stored in this chunk + header.write( + utils.pad(stat.gid ?? '', HeaderSize.OWNER_GID, '0', '\0'), + HeaderOffset.OWNER_GID, + HeaderSize.OWNER_GID, + constants.HEADER_ENCODING, + ); + + // The file size is stored in this chunk. The file size must be zero for + // directories, and it must be set for files. + header.write( + utils.pad(size ?? '', HeaderSize.FILE_SIZE, '0', '\0'), + HeaderOffset.FILE_SIZE, + HeaderSize.FILE_SIZE, + constants.HEADER_ENCODING, + ); + + // The file mtime is stored in this chunk. As the mtime is not modified when + // extracting a TAR file, the mtime can be preserved while still getting + // deterministic archives. + header.write( + utils.pad(time, HeaderSize.FILE_MTIME, '0', '\0'), + HeaderOffset.FILE_MTIME, + HeaderSize.FILE_MTIME, + constants.HEADER_ENCODING, + ); + + // The checksum is calculated as the sum of all bytes in the header. It is + // padded using ASCII spaces, as we currently don't have all the data yet. + header.write( + utils.pad('', HeaderSize.CHECKSUM, ' '), + HeaderOffset.CHECKSUM, + HeaderSize.CHECKSUM, + constants.HEADER_ENCODING, + ); + // The type of file is written as a single byte in the header. header.write( - filePath.slice(0, 99).padEnd(100, '\0'), - 0, - 100, - opts.fileNameEncoding, + type, + HeaderOffset.TYPE_FLAG, + HeaderSize.TYPE_FLAG, + constants.HEADER_ENCODING, ); - header.write(stat.mode.toString(8).padStart(7, '0') + '\0', 100, 12, 'ascii'); - header.write(stat.uid.toString(8).padStart(7, '0') + '\0', 108, 12, 'ascii'); - header.write(stat.gid.toString(8).padStart(7, '0') + '\0', 116, 12, 'ascii'); - header.write(size.toString(8).padStart(7, '0') + '\0', 124, 12, 'ascii'); - header.write(time.toString(8).padStart(7, '0') + '\0', 136, 12, 'ascii'); - header.write(' ', 148, 8, 'ascii'); // Placeholder for checksum - header.write(type, 156, 1, 'ascii'); - // File owner name will be null - header.write('ustar\0', 257, 'ascii'); - header.write('00', 263, 2, 'ascii'); - // Owner user name will be null - // Owner group name will be null - // Device major will be null - // Device minor will be null + + // File owner name will be null, as regular stat-ing cannot extract that + // information. + + // This value is the USTAR magic string which makes this file appear as + // a tar file. Without this, the file cannot be parsed and extracted. + header.write( + constants.USTAR_NAME, + HeaderOffset.USTAR_NAME, + HeaderSize.USTAR_NAME, + constants.HEADER_ENCODING, + ); + + // This chunk stores the version of USTAR, which is '00' in this case. + header.write( + constants.USTAR_VERSION, + HeaderOffset.USTAR_VERSION, + HeaderSize.USTAR_VERSION, + constants.HEADER_ENCODING, + ); + + // Owner user name will be null, as regular stat-ing cannot extract this + // information. + + // Owner group name will be null, as regular stat-ing cannot extract this + // information. + + // Device major will be null, as this specific to linux kernel knowing what + // drivers to use for executing certain files, and is irrelevant here. + + // Device minor will be null, as this specific to linux kernel knowing what + // drivers to use for executing certain files, and is irrelevant here. + + // The second half of the file name is entered here. This chunk handles file + // names ranging 100 to 255 characters. header.write( - filePath.slice(100).padEnd(155, '\0'), - 345, - 155, - opts.fileNameEncoding, + utils.splitFileName( + filePath, + HeaderSize.FILE_NAME, + HeaderSize.FILE_NAME_EXTRA, + ), + HeaderOffset.FILE_NAME_EXTRA, + HeaderSize.FILE_NAME_EXTRA, + constants.HEADER_ENCODING, ); // Updating with the new checksum const checksum = computeChecksum(header); - header.write(checksum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii'); - - return header; -} -async function* readFile( - filePath: string, - options: Partial = defaultReadFileOptions, -): AsyncGenerator { - const opts: ReadFileOptions = { ...defaultReadFileOptions, ...options }; - const fileHandle = await opts.fs.open(filePath, 'r'); - const buffer = Buffer.alloc(opts.blockSize); - let bytesRead = -1; // Initialisation value - - try { - while (bytesRead !== 0) { - buffer.fill(0); - const result = await fileHandle.read(buffer, 0, opts.blockSize, null); - bytesRead = result.bytesRead; - - if (bytesRead === 0) break; // EOF reached - if (bytesRead < 512) buffer.fill(0, bytesRead, opts.blockSize); - - yield buffer; - } - } finally { - await fileHandle.close(); - } -} + // Note the extra space in the padding for the checksum value. It is + // intentionally placed there. The padding for checksum is ASCII spaces + // instead of null, which is why it is used like this here. + header.write( + utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0 '), + HeaderOffset.CHECKSUM, + HeaderSize.CHECKSUM, + constants.HEADER_ENCODING, + ); -/** - * Traverse a directory recursively and yield file entries. - */ -async function* walkDirectory( - baseDir: string, - relativePath: string = '', - options: Partial = defaultWalkDirectoryOptions, -): AsyncGenerator { - const opts: WalkDirectoryOptions = { - ...defaultWalkDirectoryOptions, - ...options, - }; - const entries = await opts.fs.readdir(path.join(baseDir, relativePath)); - - // Sort the entries lexicographically - for (const entry of entries.sort()) { - const fullPath = path.join(baseDir, relativePath, entry); - const stat = await opts.fs.stat(fullPath); - const tarPath = path.join(relativePath, entry); - - if (stat.isDirectory()) { - yield { path: tarPath + '/', stat: stat, type: EntryTypes.DIRECTORY }; - yield* walkDirectory(baseDir, path.join(relativePath, entry)); - } else if (stat.isFile()) { - yield { path: tarPath, stat: stat, type: EntryTypes.FILE }; - } - } + return header; } -async function* createTar( - baseDir: string, - options: Partial = defaultTarOptions, -): AsyncGenerator { - const opts = { ...defaultTarOptions, ...options }; - const entryGen = walkDirectory(baseDir, '', { - fs: opts.fs, - blockSize: opts.blockSize, - }); - - for await (const entry of entryGen) { - yield createHeader(entry.path, entry.stat, entry.type); - - if (entry.type === EntryTypes.FILE) { - yield* readFile(path.join(baseDir, entry.path), { - fs: opts.fs, - blockSize: opts.blockSize, - }); - } - } - - // End-of-archive marker - two 512-byte null blocks - yield Buffer.alloc(opts.blockSize, 0); - yield Buffer.alloc(opts.blockSize, 0); +// Creates blocks marking the ned of the header. Returns one buffer of 1024 +// bytes filled with nulls. This aligns with the tar end-of-archive marker +// being two null-filled blocks. +function generateEndMarker() { + return [Buffer.alloc(512, 0), Buffer.alloc(512, 0)]; } -export { createHeader, readFile, createTar }; +export { createHeader, generateEndMarker }; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..a465f9d --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,4 @@ +export const BLOCK_SIZE = 512; +export const USTAR_NAME = 'ustar\0'; +export const USTAR_VERSION = '00'; +export const HEADER_ENCODING = 'ascii'; diff --git a/src/errors.ts b/src/errors.ts index bf71ac8..7dbf9a0 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -16,9 +16,14 @@ class ErrorVirtualTarInvalidHeader extends ErrorVirtualTar { static description = 'The header has invalid data'; } +class ErrorVirtualTarInvalidStat extends ErrorVirtualTar { + static description = 'The stat contains invalid data'; +} + export { ErrorVirtualTar, ErrorVirtualTarUndefinedBehaviour, ErrorVirtualTarInvalidFileName, ErrorVirtualTarInvalidHeader, + ErrorVirtualTarInvalidStat, }; diff --git a/src/types.ts b/src/types.ts index a6cf24b..4cf3ca5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,49 +1,54 @@ -import type { Stats } from 'fs'; - -const EntryTypes = { - FILE: '0', - DIRECTORY: '5', -} as const; - -type EntryType = (typeof EntryTypes)[keyof typeof EntryTypes]; - -type DirectoryContent = { - path: string; - stat: Stats; - type: EntryType; -}; - -type HeaderOptions = { - fileNameEncoding: 'ascii' | 'utf8'; - blockSize: number; -}; - -// An actual type for `fs` doesn't exist -type ReadFileOptions = { - fs: any; - blockSize: number; -}; - -// An actual type for `fs` doesn't exist -type WalkDirectoryOptions = { - fs: any; - blockSize: number; +const enum EntryType { + FILE = '0', + DIRECTORY = '5', +} + +const enum HeaderOffset { + FILE_NAME = 0, + FILE_MODE = 100, + OWNER_UID = 108, + OWNER_GID = 116, + FILE_SIZE = 124, + FILE_MTIME = 136, + CHECKSUM = 148, + TYPE_FLAG = 156, + OWNER_NAME = 157, + USTAR_NAME = 257, + USTAR_VERSION = 263, + OWNER_USERNAME = 265, + OWNER_GROUPNAME = 297, + DEVICE_MAJOR = 329, + DEVICE_MINOR = 337, + FILE_NAME_EXTRA = 345, +} + +const enum HeaderSize { + FILE_NAME = 100, + FILE_MODE = 8, + OWNER_UID = 8, + OWNER_GID = 8, + FILE_SIZE = 12, + FILE_MTIME = 12, + CHECKSUM = 8, + TYPE_FLAG = 1, + OWNER_NAME = 100, + USTAR_NAME = 6, + USTAR_VERSION = 2, + OWNER_USERNAME = 32, + OWNER_GROUPNAME = 32, + DEVICE_MAJOR = 8, + DEVICE_MINOR = 8, + FILE_NAME_EXTRA = 155, +} + +type FileStat = { + mode?: number; + uid?: number; + gid?: number; + size?: number; + mtime?: Date; }; -// An actual type for `fs` doesn't exist -type TarOptions = { - fs: any; - blockSize: number; - fileNameEncoding: 'ascii' | 'utf8'; -}; - -export type { - EntryType, - DirectoryContent, - HeaderOptions, - ReadFileOptions, - WalkDirectoryOptions, - TarOptions, -}; +export type { FileStat }; -export { EntryTypes }; +export { EntryType, HeaderOffset, HeaderSize }; diff --git a/src/utils.ts b/src/utils.ts index c5886e7..12949eb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,4 +4,30 @@ function never(message: string): never { throw new errors.ErrorVirtualTarUndefinedBehaviour(message); } -export { never }; +function pad( + value: string | number, + length: number, + padValue: string, + end?: string, +): string { + if (end != null) { + return value.toString(8).padStart(length - end.length, padValue) + end; + } else { + return value.toString(8).padStart(length, padValue); + } +} + +function splitFileName( + fileName: string, + offset: number, + size: number, + padding: string = '\0', +) { + return fileName.slice(offset, offset + size).padEnd(size, padding); +} + +function dateToUnixTime(date: Date): number { + return Math.round(date.getTime() / 1000); +} + +export { never, pad, splitFileName, dateToUnixTime }; diff --git a/tests/index.test.ts b/tests/index.test.ts index bc6a5e1..2a20146 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,5 +1,8 @@ +import type { FileStat } from '@/types'; import fs from 'fs'; -import { createTar } from '@/Generator'; +import path from 'path'; +import { createHeader, generateEndMarker } from '@/Generator'; +import { EntryType } from '@/types'; // TODO: actually write tests describe('index', () => { @@ -9,15 +12,74 @@ describe('index', () => { expect(true).toEqual(true); } else { // Otherwise, run the test which creates a test archive - const writeArchive = async (inputFile: string, outputFile: string) => { - const fileHandle = await fs.promises.open(outputFile, 'w+'); - for await (const chunk of createTar(inputFile)) { - await fileHandle.write(chunk); + + const walkDir = async (walkPath: string, tokens: Array) => { + const dirContent = await fs.promises.readdir(walkPath); + + for (const dirPath of dirContent) { + const stat = await fs.promises.stat(path.join(walkPath, dirPath)); + const tarStat: FileStat = { + mtime: stat.mtime, + mode: stat.mode, + gid: stat.gid, + uid: stat.uid, + }; + + if (stat.isDirectory()) { + tokens.push(createHeader(dirPath, tarStat, EntryType.DIRECTORY)); + await walkDir(dirPath, tokens); + } else { + const tarStat: FileStat = { + mtime: stat.mtime, + mode: stat.mode, + gid: stat.gid, + uid: stat.uid, + size: stat.size, + }; + tokens.push( + createHeader( + dirPath, + { ...tarStat, size: stat.size }, + EntryType.FILE, + ), + ); + const file = await fs.promises.open( + path.join(walkPath, dirPath), + 'r', + ); + const buffer = Buffer.alloc(512, 0); + while (true) { + const { bytesRead } = await file.read(buffer, 0, 512, null); + if (bytesRead < 512) { + buffer.fill('\0', bytesRead); + tokens.push(buffer); + break; + } + tokens.push(Buffer.from(buffer)); + } + await file.close(); + } } - await fileHandle.close(); + + tokens.push(...generateEndMarker()); }; + + const writeArchive = async (inPath: string, outPath: string) => { + const tokens: Array = []; + await walkDir(inPath, tokens); + + const file = await fs.promises.open(outPath, 'w+'); + for (const block of tokens) { + await file.write(block); + } + await file.close(); + }; + await expect( - writeArchive('/home/aryanj/Downloads', '/home/aryanj/archive.tar'), + writeArchive( + '/home/aryanj/Downloads/Arifureta Shokugyou Saikyou/', + '/home/aryanj/Downloads/dir/archive.tar', + ), ).toResolve(); } }, 60000); From b9098ef1681de094a8894061828c73868af2dd3f Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 19 Feb 2025 19:48:16 +1100 Subject: [PATCH 07/19] feat: added simple parsing --- src/Generator.ts | 26 +++++----- src/Parser.ts | 122 ++++++++++++++++++++++++++++++++++++++++++++ src/constants.ts | 2 +- src/errors.ts | 10 ++++ src/types.ts | 27 +++++++++- src/utils.ts | 68 +++++++++++++++++++++++- tests/index.test.ts | 51 +++++++++++++++--- 7 files changed, 283 insertions(+), 23 deletions(-) diff --git a/src/Generator.ts b/src/Generator.ts index aa164a8..ff5a741 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -4,7 +4,7 @@ import * as errors from './errors'; import * as utils from './utils'; import * as constants from './constants'; -// Computes the checksum by adding the value of every single byte in the header +// Computes the checksum by summing up all the bytes in the header function computeChecksum(header: Buffer): number { if (!header.subarray(148, 156).every((byte) => byte === 32)) { throw new errors.ErrorVirtualTarInvalidHeader( @@ -80,7 +80,7 @@ function createHeader( utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME), HeaderOffset.FILE_NAME, HeaderSize.FILE_NAME, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // The file permissions, or the mode, is stored in the next chunk. This is @@ -89,7 +89,7 @@ function createHeader( utils.pad(stat.mode ?? '', HeaderSize.FILE_MODE, '0', '\0'), HeaderOffset.FILE_MODE, HeaderSize.FILE_MODE, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // The owner UID is stored in this chunk @@ -97,7 +97,7 @@ function createHeader( utils.pad(stat.uid ?? '', HeaderSize.OWNER_UID, '0', '\0'), HeaderOffset.OWNER_UID, HeaderSize.OWNER_UID, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // The owner GID is stored in this chunk @@ -105,7 +105,7 @@ function createHeader( utils.pad(stat.gid ?? '', HeaderSize.OWNER_GID, '0', '\0'), HeaderOffset.OWNER_GID, HeaderSize.OWNER_GID, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // The file size is stored in this chunk. The file size must be zero for @@ -114,7 +114,7 @@ function createHeader( utils.pad(size ?? '', HeaderSize.FILE_SIZE, '0', '\0'), HeaderOffset.FILE_SIZE, HeaderSize.FILE_SIZE, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // The file mtime is stored in this chunk. As the mtime is not modified when @@ -124,7 +124,7 @@ function createHeader( utils.pad(time, HeaderSize.FILE_MTIME, '0', '\0'), HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // The checksum is calculated as the sum of all bytes in the header. It is @@ -133,7 +133,7 @@ function createHeader( utils.pad('', HeaderSize.CHECKSUM, ' '), HeaderOffset.CHECKSUM, HeaderSize.CHECKSUM, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // The type of file is written as a single byte in the header. @@ -141,7 +141,7 @@ function createHeader( type, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // File owner name will be null, as regular stat-ing cannot extract that @@ -153,7 +153,7 @@ function createHeader( constants.USTAR_NAME, HeaderOffset.USTAR_NAME, HeaderSize.USTAR_NAME, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // This chunk stores the version of USTAR, which is '00' in this case. @@ -161,7 +161,7 @@ function createHeader( constants.USTAR_VERSION, HeaderOffset.USTAR_VERSION, HeaderSize.USTAR_VERSION, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // Owner user name will be null, as regular stat-ing cannot extract this @@ -186,7 +186,7 @@ function createHeader( ), HeaderOffset.FILE_NAME_EXTRA, HeaderSize.FILE_NAME_EXTRA, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); // Updating with the new checksum @@ -199,7 +199,7 @@ function createHeader( utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0 '), HeaderOffset.CHECKSUM, HeaderSize.CHECKSUM, - constants.HEADER_ENCODING, + constants.TEXT_ENCODING, ); return header; diff --git a/src/Parser.ts b/src/Parser.ts index e69de29..2332e90 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -0,0 +1,122 @@ +import type { ParserState, Header, Data, End } from './types'; +import { HeaderOffset, HeaderSize, EntryType } from './types'; +import * as constants from './constants'; +import * as errors from './errors'; +import * as utils from './utils'; + +class Parser { + protected state: ParserState = 'ready'; + protected remainingBytes = 0; + + write(data: ArrayBuffer): Header | Data | End | undefined { + if (data.byteLength !== constants.BLOCK_SIZE) { + throw new errors.ErrorVirtualTarBlockSize( + `Expected block size ${constants.BLOCK_SIZE} but received ${data.byteLength}`, + ); + } + + // TODO: test if the first block is header by checking magic value + const view = new DataView(data, 0, constants.BLOCK_SIZE); + + switch (this.state) { + case 'ready': { + if (utils.checkNullView(view)) { + this.state = 'null'; + return; + } + + const fileName = utils.parseFileName(view); + const fileSize = utils.extractOctal( + view, + HeaderOffset.FILE_SIZE, + HeaderSize.FILE_SIZE, + ); + const fileMtime = new Date( + utils.extractOctal( + view, + HeaderOffset.FILE_MTIME, + HeaderSize.FILE_MTIME, + ), + ); + const fileMode = utils.extractOctal( + view, + HeaderOffset.FILE_MODE, + HeaderSize.FILE_MODE, + ); + const ownerGid = utils.extractOctal( + view, + HeaderOffset.OWNER_GID, + HeaderSize.OWNER_GID, + ); + const ownerUid = utils.extractOctal( + view, + HeaderOffset.OWNER_UID, + HeaderSize.OWNER_UID, + ); + const ownerName = utils.extractChars( + view, + HeaderOffset.OWNER_NAME, + HeaderSize.OWNER_NAME, + ); + const ownerGroupName = utils.extractChars( + view, + HeaderOffset.OWNER_GROUPNAME, + HeaderSize.OWNER_GROUPNAME, + ); + const ownerUserName = utils.extractChars( + view, + HeaderOffset.OWNER_USERNAME, + HeaderSize.OWNER_USERNAME, + ); + const fileType = + utils.extractChars( + view, + HeaderOffset.TYPE_FLAG, + HeaderSize.TYPE_FLAG, + ) === EntryType.FILE + ? 'file' + : 'directory'; + + if (fileType === 'file') { + this.state = 'header'; + this.remainingBytes = fileSize; + } + + const parsedHeader: Header = { + type: 'header', + fileType, + fileName, + fileMode, + fileMtime, + fileSize, + ownerGid, + ownerUid, + ownerName, + ownerUserName, + ownerGroupName, + }; + + return parsedHeader; + } + case 'header': + if (this.remainingBytes > 512) { + this.remainingBytes -= 512; + return { type: 'data', data: utils.extractBytes(view) }; + } else { + const data = utils.extractBytes(view, 0, this.remainingBytes); + this.remainingBytes = 0; + this.state = 'ready'; + return { type: 'data', data: data }; + } + + case 'null': + if (utils.checkNullView(view)) return { type: 'end' }; + else throw new errors.ErrorVirtualTarEndOfArchive(); + + default: + utils.never('Unexpected state'); + } + } +} + +export default Parser; diff --git a/src/constants.ts b/src/constants.ts index a465f9d..ab53be6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ export const BLOCK_SIZE = 512; export const USTAR_NAME = 'ustar\0'; export const USTAR_VERSION = '00'; -export const HEADER_ENCODING = 'ascii'; +export const TEXT_ENCODING = 'ascii'; diff --git a/src/errors.ts b/src/errors.ts index 7dbf9a0..a05709c 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -20,10 +20,20 @@ class ErrorVirtualTarInvalidStat extends ErrorVirtualTar { static description = 'The stat contains invalid data'; } +class ErrorVirtualTarBlockSize extends ErrorVirtualTar { + static description = 'The block size is incorrect'; +} + +class ErrorVirtualTarEndOfArchive extends ErrorVirtualTar { + static description = 'No data can come after an end-of-archive marker'; +} + export { ErrorVirtualTar, ErrorVirtualTarUndefinedBehaviour, ErrorVirtualTarInvalidFileName, ErrorVirtualTarInvalidHeader, ErrorVirtualTarInvalidStat, + ErrorVirtualTarBlockSize, + ErrorVirtualTarEndOfArchive, }; diff --git a/src/types.ts b/src/types.ts index 4cf3ca5..6045cce 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,6 +49,31 @@ type FileStat = { mtime?: Date; }; -export type { FileStat }; +type ParserState = 'ready' | 'header' | 'null'; + +type Header = { + type: 'header'; + fileType: 'file' | 'directory'; + fileName: string; + fileMode: number; + ownerUid: number; + ownerGid: number; + fileSize: number; + fileMtime: Date; + ownerName: string; + ownerUserName: string; + ownerGroupName: string; +}; + +type Data = { + type: 'data'; + data: Uint8Array; +}; + +type End = { + type: 'end'; +}; + +export type { FileStat, ParserState, Header, Data, End }; export { EntryType, HeaderOffset, HeaderSize }; diff --git a/src/utils.ts b/src/utils.ts index 12949eb..5fff760 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,8 @@ +import { HeaderOffset, HeaderSize } from './types'; import * as errors from './errors'; +import * as constants from './constants'; + +const nullRegex = /\0/g; function never(message: string): never { throw new errors.ErrorVirtualTarUndefinedBehaviour(message); @@ -30,4 +34,66 @@ function dateToUnixTime(date: Date): number { return Math.round(date.getTime() / 1000); } -export { never, pad, splitFileName, dateToUnixTime }; +// PARSER + +const decoder = new TextDecoder(constants.TEXT_ENCODING); + +function extractBytes( + view: DataView, + offset?: number, + length?: number, +): Uint8Array { + return new Uint8Array(view.buffer, offset, length); +} + +function extractChars( + view: DataView, + offset?: number, + length?: number, +): string { + return decoder + .decode(extractBytes(view, offset, length)) + .replace(nullRegex, ''); +} + +function extractOctal( + view: DataView, + offset?: number, + length?: number, +): number { + const value = extractChars(view, offset, length); + return value.length > 0 ? parseInt(value, 8) : 0; +} + +function parseFileName(view: DataView) { + const fileNameLower = extractChars( + view, + HeaderOffset.FILE_NAME, + HeaderSize.FILE_NAME, + ); + const fileNameUpper = extractChars( + view, + HeaderOffset.FILE_NAME_EXTRA, + HeaderSize.FILE_NAME_EXTRA, + ); + return fileNameLower + fileNameUpper; +} + +function checkNullView(view: DataView): boolean { + for (let i = 0; i < constants.BLOCK_SIZE; i++) { + if (view.getUint8(i) !== 0) return false; + } + return true; +} + +export { + never, + pad, + splitFileName, + dateToUnixTime, + extractBytes, + extractChars, + extractOctal, + parseFileName, + checkNullView, +}; diff --git a/tests/index.test.ts b/tests/index.test.ts index 2a20146..bd9ba86 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -2,6 +2,7 @@ import type { FileStat } from '@/types'; import fs from 'fs'; import path from 'path'; import { createHeader, generateEndMarker } from '@/Generator'; +import Parser from '@/Parser'; import { EntryType } from '@/types'; // TODO: actually write tests @@ -36,13 +37,7 @@ describe('index', () => { uid: stat.uid, size: stat.size, }; - tokens.push( - createHeader( - dirPath, - { ...tarStat, size: stat.size }, - EntryType.FILE, - ), - ); + tokens.push(createHeader(dirPath, tarStat, EntryType.FILE)); const file = await fs.promises.open( path.join(walkPath, dirPath), 'r', @@ -83,4 +78,46 @@ describe('index', () => { ).toResolve(); } }, 60000); + test.only('parsing', async () => { + if (process.env['CI']) expect(true).toBeTruthy(); + const file = '/home/aryanj/Downloads/dir/archive.tar'; + + const fileHandle = await fs.promises.open(file, 'r'); + const buffer = Buffer.alloc(512, 0); + const parser = new Parser(); + let writeHandle: fs.promises.FileHandle | undefined = undefined; + const root = '/home/aryanj/Downloads/dir'; + + while (true) { + await fileHandle.read(buffer); + const output = parser.write(buffer.buffer); + + if (output == null) continue; + + if (output.type === 'header') { + if (writeHandle != null) await writeHandle.close(); + if (output.fileType === 'directory') { + await fs.promises.mkdir(path.join(root, output.fileName)); + } else { + writeHandle = await fs.promises.open( + path.join(root, output.fileName), + 'w+', + ); + } + } + + if (output.type === 'data') { + if (writeHandle == null) throw new Error('never'); + await writeHandle.write(output.data); + } + + if (output.type === 'end') { + if (writeHandle != null) await writeHandle.close(); + break; + } + } + await fileHandle.close(); + // Console.log(output); + // expect(output).toBeObject(); + }); }); From 5cd436345418bbc01797797da6ae7a1cd5caac53 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Mon, 24 Feb 2025 11:53:10 +1100 Subject: [PATCH 08/19] chore: switched Buffer usage to Uint8Arrays --- src/Generator.ts | 66 ++++++++++++++++++++++----------------------- src/Parser.ts | 13 +++++---- src/constants.ts | 1 - src/utils.ts | 44 +++++++++++++++++++++++++----- tests/index.test.ts | 16 +++++------ 5 files changed, 84 insertions(+), 56 deletions(-) diff --git a/src/Generator.ts b/src/Generator.ts index ff5a741..2554365 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -5,8 +5,8 @@ import * as utils from './utils'; import * as constants from './constants'; // Computes the checksum by summing up all the bytes in the header -function computeChecksum(header: Buffer): number { - if (!header.subarray(148, 156).every((byte) => byte === 32)) { +function computeChecksum(header: Uint8Array): number { + if (!header.slice(148, 156).every((byte) => byte === 32)) { throw new errors.ErrorVirtualTarInvalidHeader( 'Checksum field is not properly initialized with spaces', ); @@ -14,12 +14,11 @@ function computeChecksum(header: Buffer): number { return header.reduce((sum, byte) => sum + byte, 0); } -// TODO: Should logging be included? function createHeader( filePath: string, stat: FileStat, type: EntryType, -): Buffer { +): Uint8Array { // TODO: implement long-file-name headers if (filePath.length < 1 || filePath.length > 255) { throw new errors.ErrorVirtualTarInvalidFileName( @@ -48,7 +47,7 @@ function createHeader( // Make sure to initialise the header with zeros to avoid writing nullish // blocks. - const header = Buffer.alloc(constants.BLOCK_SIZE, 0); + const header = new Uint8Array(constants.BLOCK_SIZE); // The TAR headers follow this structure // Start Size Description @@ -71,77 +70,75 @@ function createHeader( // 345 155 File name (last 155 bytes, total 255 bytes, null-padded) // 500 12 '\0' (unused) // - // Note that all values are in ASCII format, which is different from the - // default formatting of UTF-8 for Buffer.write(). All numbers are also in - // octal format as opposed to decimal or hexadecimal. + // Note that all numbers are in stringified octal format. // The first half of the file name (upto 100 bytes) is stored here. - header.write( + utils.writeBytesToArray( + header, utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME), HeaderOffset.FILE_NAME, HeaderSize.FILE_NAME, - constants.TEXT_ENCODING, ); // The file permissions, or the mode, is stored in the next chunk. This is // stored in an octal number format. - header.write( + utils.writeBytesToArray( + header, utils.pad(stat.mode ?? '', HeaderSize.FILE_MODE, '0', '\0'), HeaderOffset.FILE_MODE, HeaderSize.FILE_MODE, - constants.TEXT_ENCODING, ); // The owner UID is stored in this chunk - header.write( + utils.writeBytesToArray( + header, utils.pad(stat.uid ?? '', HeaderSize.OWNER_UID, '0', '\0'), HeaderOffset.OWNER_UID, HeaderSize.OWNER_UID, - constants.TEXT_ENCODING, ); // The owner GID is stored in this chunk - header.write( + utils.writeBytesToArray( + header, utils.pad(stat.gid ?? '', HeaderSize.OWNER_GID, '0', '\0'), HeaderOffset.OWNER_GID, HeaderSize.OWNER_GID, - constants.TEXT_ENCODING, ); // The file size is stored in this chunk. The file size must be zero for // directories, and it must be set for files. - header.write( + utils.writeBytesToArray( + header, utils.pad(size ?? '', HeaderSize.FILE_SIZE, '0', '\0'), HeaderOffset.FILE_SIZE, HeaderSize.FILE_SIZE, - constants.TEXT_ENCODING, ); // The file mtime is stored in this chunk. As the mtime is not modified when // extracting a TAR file, the mtime can be preserved while still getting // deterministic archives. - header.write( + utils.writeBytesToArray( + header, utils.pad(time, HeaderSize.FILE_MTIME, '0', '\0'), HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME, - constants.TEXT_ENCODING, ); // The checksum is calculated as the sum of all bytes in the header. It is // padded using ASCII spaces, as we currently don't have all the data yet. - header.write( + utils.writeBytesToArray( + header, utils.pad('', HeaderSize.CHECKSUM, ' '), HeaderOffset.CHECKSUM, HeaderSize.CHECKSUM, - constants.TEXT_ENCODING, ); // The type of file is written as a single byte in the header. - header.write( + utils.writeBytesToArray( + header, type, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG, - constants.TEXT_ENCODING, ); // File owner name will be null, as regular stat-ing cannot extract that @@ -149,19 +146,19 @@ function createHeader( // This value is the USTAR magic string which makes this file appear as // a tar file. Without this, the file cannot be parsed and extracted. - header.write( + utils.writeBytesToArray( + header, constants.USTAR_NAME, HeaderOffset.USTAR_NAME, HeaderSize.USTAR_NAME, - constants.TEXT_ENCODING, ); // This chunk stores the version of USTAR, which is '00' in this case. - header.write( + utils.writeBytesToArray( + header, constants.USTAR_VERSION, HeaderOffset.USTAR_VERSION, HeaderSize.USTAR_VERSION, - constants.TEXT_ENCODING, ); // Owner user name will be null, as regular stat-ing cannot extract this @@ -178,7 +175,8 @@ function createHeader( // The second half of the file name is entered here. This chunk handles file // names ranging 100 to 255 characters. - header.write( + utils.writeBytesToArray( + header, utils.splitFileName( filePath, HeaderSize.FILE_NAME, @@ -186,7 +184,6 @@ function createHeader( ), HeaderOffset.FILE_NAME_EXTRA, HeaderSize.FILE_NAME_EXTRA, - constants.TEXT_ENCODING, ); // Updating with the new checksum @@ -195,11 +192,11 @@ function createHeader( // Note the extra space in the padding for the checksum value. It is // intentionally placed there. The padding for checksum is ASCII spaces // instead of null, which is why it is used like this here. - header.write( + utils.writeBytesToArray( + header, utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0 '), HeaderOffset.CHECKSUM, HeaderSize.CHECKSUM, - constants.TEXT_ENCODING, ); return header; @@ -209,7 +206,10 @@ function createHeader( // bytes filled with nulls. This aligns with the tar end-of-archive marker // being two null-filled blocks. function generateEndMarker() { - return [Buffer.alloc(512, 0), Buffer.alloc(512, 0)]; + return [ + new Uint8Array(constants.BLOCK_SIZE), + new Uint8Array(constants.BLOCK_SIZE), + ]; } export { createHeader, generateEndMarker }; diff --git a/src/Parser.ts b/src/Parser.ts index 2332e90..d471376 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -8,15 +8,14 @@ class Parser { protected state: ParserState = 'ready'; protected remainingBytes = 0; - write(data: ArrayBuffer): Header | Data | End | undefined { + write(data: Uint8Array): Header | Data | End | undefined { if (data.byteLength !== constants.BLOCK_SIZE) { throw new errors.ErrorVirtualTarBlockSize( `Expected block size ${constants.BLOCK_SIZE} but received ${data.byteLength}`, ); } - // TODO: test if the first block is header by checking magic value - const view = new DataView(data, 0, constants.BLOCK_SIZE); + const view = new DataView(data.buffer, 0, constants.BLOCK_SIZE); switch (this.state) { case 'ready': { @@ -53,23 +52,23 @@ class Parser { HeaderOffset.OWNER_UID, HeaderSize.OWNER_UID, ); - const ownerName = utils.extractChars( + const ownerName = utils.extractString( view, HeaderOffset.OWNER_NAME, HeaderSize.OWNER_NAME, ); - const ownerGroupName = utils.extractChars( + const ownerGroupName = utils.extractString( view, HeaderOffset.OWNER_GROUPNAME, HeaderSize.OWNER_GROUPNAME, ); - const ownerUserName = utils.extractChars( + const ownerUserName = utils.extractString( view, HeaderOffset.OWNER_USERNAME, HeaderSize.OWNER_USERNAME, ); const fileType = - utils.extractChars( + utils.extractString( view, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG, diff --git a/src/constants.ts b/src/constants.ts index ab53be6..34dc5db 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,3 @@ export const BLOCK_SIZE = 512; export const USTAR_NAME = 'ustar\0'; export const USTAR_VERSION = '00'; -export const TEXT_ENCODING = 'ascii'; diff --git a/src/utils.ts b/src/utils.ts index 5fff760..0fc9913 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -36,7 +36,16 @@ function dateToUnixTime(date: Date): number { // PARSER -const decoder = new TextDecoder(constants.TEXT_ENCODING); +const decoder = new TextDecoder('ascii'); + +// WARN: redundant? +function dataViewToUint8Array(dataView: DataView): Uint8Array { + return new Uint8Array( + dataView.buffer, + dataView.byteOffset, + dataView.byteLength, + ); +} function extractBytes( view: DataView, @@ -46,7 +55,7 @@ function extractBytes( return new Uint8Array(view.buffer, offset, length); } -function extractChars( +function extractString( view: DataView, offset?: number, length?: number, @@ -61,17 +70,17 @@ function extractOctal( offset?: number, length?: number, ): number { - const value = extractChars(view, offset, length); + const value = extractString(view, offset, length); return value.length > 0 ? parseInt(value, 8) : 0; } function parseFileName(view: DataView) { - const fileNameLower = extractChars( + const fileNameLower = extractString( view, HeaderOffset.FILE_NAME, HeaderSize.FILE_NAME, ); - const fileNameUpper = extractChars( + const fileNameUpper = extractString( view, HeaderOffset.FILE_NAME_EXTRA, HeaderSize.FILE_NAME_EXTRA, @@ -86,14 +95,37 @@ function checkNullView(view: DataView): boolean { return true; } +function writeBytesToArray( + array: Uint8Array, + bytes: string | ArrayLike, + offset: number, + length: number, +): number { + // Ensure indices are within valid bounds + const start = Math.max(0, Math.min(offset, array.length)); + const end = Math.min(array.length, start + Math.max(0, length)); + const maxLength = end - start; + + let i = 0; + for (; i < bytes.length && i < maxLength; i++) { + array[start + i] = + typeof bytes === 'string' ? bytes.charCodeAt(i) : bytes[i]; + } + + // Return number of bytes written + return i; +} + export { never, pad, splitFileName, dateToUnixTime, + dataViewToUint8Array, extractBytes, - extractChars, + extractString, extractOctal, parseFileName, checkNullView, + writeBytesToArray, }; diff --git a/tests/index.test.ts b/tests/index.test.ts index bd9ba86..c8e823f 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -6,15 +6,15 @@ import Parser from '@/Parser'; import { EntryType } from '@/types'; // TODO: actually write tests -describe('index', () => { - test('test', async () => { +describe('local test', () => { + test('gen', async () => { if (process.env['CI'] != null) { // Skip this test if on CI expect(true).toEqual(true); } else { // Otherwise, run the test which creates a test archive - const walkDir = async (walkPath: string, tokens: Array) => { + const walkDir = async (walkPath: string, tokens: Array) => { const dirContent = await fs.promises.readdir(walkPath); for (const dirPath of dirContent) { @@ -78,19 +78,19 @@ describe('index', () => { ).toResolve(); } }, 60000); - test.only('parsing', async () => { - if (process.env['CI']) expect(true).toBeTruthy(); + test('parsing', async () => { + if (process.env['CI'] != null) expect(true).toBeTruthy(); const file = '/home/aryanj/Downloads/dir/archive.tar'; const fileHandle = await fs.promises.open(file, 'r'); - const buffer = Buffer.alloc(512, 0); + const buffer = new Uint8Array(512); const parser = new Parser(); let writeHandle: fs.promises.FileHandle | undefined = undefined; const root = '/home/aryanj/Downloads/dir'; while (true) { await fileHandle.read(buffer); - const output = parser.write(buffer.buffer); + const output = parser.write(buffer); if (output == null) continue; @@ -117,7 +117,5 @@ describe('index', () => { } } await fileHandle.close(); - // Console.log(output); - // expect(output).toBeObject(); }); }); From 3bf4c2199b09af4eae99ac8be7fefaf3c513126c Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Thu, 27 Feb 2025 18:19:31 +1100 Subject: [PATCH 09/19] feat: added proper tests --- package-lock.json | 75 +++++++++++++ package.json | 2 + src/Generator.ts | 32 ++---- src/Parser.ts | 195 ++++++++++++++++++--------------- src/types.ts | 26 +++-- src/utils.ts | 14 +-- tests/Generator.test.ts | 43 ++++++++ tests/Parser.test.ts | 78 +++++++++++++ tests/index.test.ts | 236 ++++++++++++++++++++++------------------ tests/utils.ts | 212 ++++++++++++++++++++++++++++++++++++ 10 files changed, 675 insertions(+), 238 deletions(-) create mode 100644 tests/Generator.test.ts create mode 100644 tests/Parser.test.ts create mode 100644 tests/utils.ts diff --git a/package-lock.json b/package-lock.json index 913ece3..f92439e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "uuid": "^11.0.5" }, "devDependencies": { + "@fast-check/jest": "^1.1.0", "@swc/core": "^1.3.62", "@swc/jest": "^0.2.26", "@types/jest": "^28.1.3", @@ -25,6 +26,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", + "fast-check": "^3.0.1", "jest": "^28.1.1", "jest-extended": "^3.0.1", "jest-junit": "^14.0.0", @@ -649,6 +651,39 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fast-check/jest": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@fast-check/jest/-/jest-1.8.2.tgz", + "integrity": "sha512-+UgQKZ0og0olUZXWgZ5Zcw42eN+3OB0Nfw0CU9OnlHBhHFnd8xppUYviX5HriAyUsAko1t/li5LZ9mSIIakmhg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "fast-check": "^3.0.0" + }, + "peerDependencies": { + "@fast-check/worker": ">=0.0.7 <0.5.0", + "@jest/expect": ">=28.0.0", + "@jest/globals": ">=25.5.2" + }, + "peerDependenciesMeta": { + "@fast-check/worker": { + "optional": true + }, + "@jest/expect": { + "optional": true + } + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3637,6 +3672,29 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6904,6 +6962,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index 16734a4..417c3d2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "uuid": "^11.0.5" }, "devDependencies": { + "@fast-check/jest": "^1.1.0", "@swc/core": "^1.3.62", "@swc/jest": "^0.2.26", "@types/jest": "^28.1.3", @@ -51,6 +52,7 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-prettier": "^4.0.0", + "fast-check": "^3.0.1", "jest": "^28.1.1", "jest-extended": "^3.0.1", "jest-junit": "^14.0.0", diff --git a/src/Generator.ts b/src/Generator.ts index 2554365..c1923c1 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -6,18 +6,13 @@ import * as constants from './constants'; // Computes the checksum by summing up all the bytes in the header function computeChecksum(header: Uint8Array): number { - if (!header.slice(148, 156).every((byte) => byte === 32)) { - throw new errors.ErrorVirtualTarInvalidHeader( - 'Checksum field is not properly initialized with spaces', - ); - } return header.reduce((sum, byte) => sum + byte, 0); } -function createHeader( +function generateHeader( filePath: string, - stat: FileStat, type: EntryType, + stat: FileStat, ): Uint8Array { // TODO: implement long-file-name headers if (filePath.length < 1 || filePath.length > 255) { @@ -26,14 +21,6 @@ function createHeader( ); } - // The file path must not contain any directories, and must only contain a - // file name. This guard checks that. - if (filePath.includes('/')) { - throw new errors.ErrorVirtualTarInvalidFileName( - 'File name must not contain /', - ); - } - // As the size does not matter for directories, it can be undefined. However, // if the header is being generated for a file, then it needs to have a valid // size. This guard checks that. @@ -202,14 +189,11 @@ function createHeader( return header; } -// Creates blocks marking the ned of the header. Returns one buffer of 1024 -// bytes filled with nulls. This aligns with the tar end-of-archive marker -// being two null-filled blocks. -function generateEndMarker() { - return [ - new Uint8Array(constants.BLOCK_SIZE), - new Uint8Array(constants.BLOCK_SIZE), - ]; +// Creates a single null block. A null block is a block filled with all zeros. +// This is needed to end the archive, as two of these blocks mark the end of +// archive. +function generateNullChunk() { + return new Uint8Array(constants.BLOCK_SIZE); } -export { createHeader, generateEndMarker }; +export { generateHeader, generateNullChunk }; diff --git a/src/Parser.ts b/src/Parser.ts index d471376..df58eb1 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,116 +1,135 @@ -import type { ParserState, Header, Data, End } from './types'; +import type { HeaderToken, DataToken, EndToken } from './types'; +import { ParserState } from './types'; import { HeaderOffset, HeaderSize, EntryType } from './types'; import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; +function parseHeader(view: DataView): HeaderToken { + // TODO: confirm integrity by checking against checksum + const filePath = utils.parseFilePath(view); + const fileSize = utils.extractOctal( + view, + HeaderOffset.FILE_SIZE, + HeaderSize.FILE_SIZE, + ); + const fileMtime = new Date( + utils.extractOctal(view, HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME) * + 1000, + ); + const fileMode = utils.extractOctal( + view, + HeaderOffset.FILE_MODE, + HeaderSize.FILE_MODE, + ); + const ownerGid = utils.extractOctal( + view, + HeaderOffset.OWNER_GID, + HeaderSize.OWNER_GID, + ); + const ownerUid = utils.extractOctal( + view, + HeaderOffset.OWNER_UID, + HeaderSize.OWNER_UID, + ); + const ownerName = utils.extractString( + view, + HeaderOffset.OWNER_NAME, + HeaderSize.OWNER_NAME, + ); + const ownerGroupName = utils.extractString( + view, + HeaderOffset.OWNER_GROUPNAME, + HeaderSize.OWNER_GROUPNAME, + ); + const ownerUserName = utils.extractString( + view, + HeaderOffset.OWNER_USERNAME, + HeaderSize.OWNER_USERNAME, + ); + const fileType = + utils.extractString(view, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG) === + EntryType.FILE + ? 'file' + : 'directory'; + + return { + type: 'header', + filePath, + fileType, + fileMode, + fileMtime, + fileSize, + ownerGid, + ownerUid, + ownerName, + ownerUserName, + ownerGroupName, + }; +} + +function parseData(view: DataView, remainingBytes: number): DataToken { + if (remainingBytes > 512) { + return { type: 'data', data: utils.extractBytes(view) }; + } else { + const data = utils.extractBytes(view, 0, remainingBytes); + return { type: 'data', data: data }; + } +} + class Parser { - protected state: ParserState = 'ready'; + protected state: ParserState = ParserState.READY; protected remainingBytes = 0; - write(data: Uint8Array): Header | Data | End | undefined { + write(data: Uint8Array) { if (data.byteLength !== constants.BLOCK_SIZE) { throw new errors.ErrorVirtualTarBlockSize( - `Expected block size ${constants.BLOCK_SIZE} but received ${data.byteLength}`, + `Expected block size to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`, ); } const view = new DataView(data.buffer, 0, constants.BLOCK_SIZE); switch (this.state) { - case 'ready': { + case ParserState.ENDED: { + throw new errors.ErrorVirtualTarEndOfArchive( + 'Archive has already ended', + ); + } + + case ParserState.READY: { + // Check if we need to parse the end-of-archive marker if (utils.checkNullView(view)) { - this.state = 'null'; + this.state = ParserState.NULL; return; } - const fileName = utils.parseFileName(view); - const fileSize = utils.extractOctal( - view, - HeaderOffset.FILE_SIZE, - HeaderSize.FILE_SIZE, - ); - const fileMtime = new Date( - utils.extractOctal( - view, - HeaderOffset.FILE_MTIME, - HeaderSize.FILE_MTIME, - ), - ); - const fileMode = utils.extractOctal( - view, - HeaderOffset.FILE_MODE, - HeaderSize.FILE_MODE, - ); - const ownerGid = utils.extractOctal( - view, - HeaderOffset.OWNER_GID, - HeaderSize.OWNER_GID, - ); - const ownerUid = utils.extractOctal( - view, - HeaderOffset.OWNER_UID, - HeaderSize.OWNER_UID, - ); - const ownerName = utils.extractString( - view, - HeaderOffset.OWNER_NAME, - HeaderSize.OWNER_NAME, - ); - const ownerGroupName = utils.extractString( - view, - HeaderOffset.OWNER_GROUPNAME, - HeaderSize.OWNER_GROUPNAME, - ); - const ownerUserName = utils.extractString( - view, - HeaderOffset.OWNER_USERNAME, - HeaderSize.OWNER_USERNAME, - ); - const fileType = - utils.extractString( - view, - HeaderOffset.TYPE_FLAG, - HeaderSize.TYPE_FLAG, - ) === EntryType.FILE - ? 'file' - : 'directory'; - - if (fileType === 'file') { - this.state = 'header'; - this.remainingBytes = fileSize; + // Set relevant state if the header corresponds to a file + const headerToken = parseHeader(view); + if (headerToken.fileType === 'file') { + this.state = ParserState.DATA; + this.remainingBytes = headerToken.fileSize; } + return headerToken; + } - const parsedHeader: Header = { - type: 'header', - fileType, - fileName, - fileMode, - fileMtime, - fileSize, - ownerGid, - ownerUid, - ownerName, - ownerUserName, - ownerGroupName, - }; - - return parsedHeader; + case ParserState.DATA: { + const parsedData = parseData(view, this.remainingBytes); + this.remainingBytes -= 512; + if (this.remainingBytes < 0) this.state = ParserState.READY; + return parsedData; } - case 'header': - if (this.remainingBytes > 512) { - this.remainingBytes -= 512; - return { type: 'data', data: utils.extractBytes(view) }; + + case ParserState.NULL: { + if (utils.checkNullView(view)) { + this.state = ParserState.ENDED; + return { type: 'end' } as EndToken; } else { - const data = utils.extractBytes(view, 0, this.remainingBytes); - this.remainingBytes = 0; - this.state = 'ready'; - return { type: 'data', data: data }; + throw new errors.ErrorVirtualTarEndOfArchive( + 'Received garbage data after first end marker', + ); } - - case 'null': - if (utils.checkNullView(view)) return { type: 'end' }; - else throw new errors.ErrorVirtualTarEndOfArchive(); + } default: utils.never('Unexpected state'); diff --git a/src/types.ts b/src/types.ts index 6045cce..52f51dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,12 +49,10 @@ type FileStat = { mtime?: Date; }; -type ParserState = 'ready' | 'header' | 'null'; - -type Header = { +type HeaderToken = { type: 'header'; fileType: 'file' | 'directory'; - fileName: string; + filePath: string; fileMode: number; ownerUid: number; ownerGid: number; @@ -65,15 +63,27 @@ type Header = { ownerGroupName: string; }; -type Data = { +type DataToken = { type: 'data'; data: Uint8Array; }; -type End = { +type EndToken = { type: 'end'; }; -export type { FileStat, ParserState, Header, Data, End }; +const enum FileType { + FILE, + DIRECTORY, +} + +const enum ParserState { + READY, + DATA, + NULL, + ENDED, +} + +export type { FileStat, HeaderToken, DataToken, EndToken }; -export { EntryType, HeaderOffset, HeaderSize }; +export { EntryType, HeaderOffset, HeaderSize, FileType, ParserState }; diff --git a/src/utils.ts b/src/utils.ts index 0fc9913..c1184ea 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -38,15 +38,6 @@ function dateToUnixTime(date: Date): number { const decoder = new TextDecoder('ascii'); -// WARN: redundant? -function dataViewToUint8Array(dataView: DataView): Uint8Array { - return new Uint8Array( - dataView.buffer, - dataView.byteOffset, - dataView.byteLength, - ); -} - function extractBytes( view: DataView, offset?: number, @@ -74,7 +65,7 @@ function extractOctal( return value.length > 0 ? parseInt(value, 8) : 0; } -function parseFileName(view: DataView) { +function parseFilePath(view: DataView) { const fileNameLower = extractString( view, HeaderOffset.FILE_NAME, @@ -121,11 +112,10 @@ export { pad, splitFileName, dateToUnixTime, - dataViewToUint8Array, extractBytes, extractString, extractOctal, - parseFileName, + parseFilePath, checkNullView, writeBytesToArray, }; diff --git a/tests/Generator.test.ts b/tests/Generator.test.ts new file mode 100644 index 0000000..e14e754 --- /dev/null +++ b/tests/Generator.test.ts @@ -0,0 +1,43 @@ +import { test } from '@fast-check/jest'; +import { generateHeader } from '@/Generator'; +import { EntryType } from '@/types'; +import * as tarUtils from '@/utils'; +import { dirArb, fileArb, splitHeaderData } from './utils'; + +describe('archive generation', () => { + test.prop([fileArb()])('should generate a valid file header', (file) => { + // Generate and split the header + const header = generateHeader(file.path, EntryType.FILE, file.stat); + const { name, type, mode, uid, gid, size, mtime, format, version } = + splitHeaderData(header); + + // Compare the values to the expected ones + expect(name).toEqual(file.path); + expect(type).toEqual(EntryType.FILE); + expect(mode).toEqual(file.stat.mode); + expect(uid).toEqual(file.stat.uid); + expect(gid).toEqual(file.stat.gid); + expect(size).toEqual(file.stat.size); + expect(mtime).toEqual(tarUtils.dateToUnixTime(file.stat.mtime!)); + expect(format).toEqual('ustar'); + expect(version).toEqual('00'); + }); + + test.prop([dirArb(0)])('should generate a valid directory header', (file) => { + // Generate and split the header + const header = generateHeader(file.path, EntryType.DIRECTORY, file.stat); + const { name, type, mode, uid, gid, size, mtime, format, version } = + splitHeaderData(header); + + // Compare the values to the expected ones + expect(name).toEqual(file.path); + expect(type).toEqual(EntryType.DIRECTORY); + expect(mode).toEqual(file.stat.mode); + expect(uid).toEqual(file.stat.uid); + expect(gid).toEqual(file.stat.gid); + expect(size).toEqual(0); + expect(mtime).toEqual(tarUtils.dateToUnixTime(file.stat.mtime!)); + expect(format).toEqual('ustar'); + expect(version).toEqual('00'); + }); +}); diff --git a/tests/Parser.test.ts b/tests/Parser.test.ts new file mode 100644 index 0000000..c4ca1bc --- /dev/null +++ b/tests/Parser.test.ts @@ -0,0 +1,78 @@ +import { test } from '@fast-check/jest'; +import fc from 'fast-check'; +import { ParserState } from '@/types'; +import Parser from '@/Parser'; +import * as tarUtils from '@/utils'; +import * as tarConstants from '@/constants'; +import { tarHeaderArb, tarDataArb } from './utils'; + +describe('archive parsing', () => { + test.prop([tarHeaderArb])( + 'should parse headers with correct state', + ({ header, stat }) => { + const { type, path, uid, gid } = stat; + const parser = new Parser(); + const token = parser.write(header); + + expect(token?.type).toEqual('header'); + if (token?.type !== 'header') tarUtils.never('Token type'); + + // @ts-ignore: accessing protected member for state analysis + const state = parser.state; + + switch (type) { + case '0': + expect(state).toEqual(ParserState.DATA); + expect(token.fileType).toEqual('file'); + break; + case '5': + expect(state).toEqual(ParserState.READY); + expect(token.fileType).toEqual('directory'); + break; + default: + tarUtils.never('Invalid state'); + } + + expect(token.filePath).toEqual(path); + expect(token.ownerUid).toEqual(uid); + expect(token.ownerGid).toEqual(gid); + }, + ); + + test.prop([tarDataArb])( + 'should parse file with data', + ({ header, type, data, encodedData }) => { + // Make sure we are only testing against files + fc.pre(type === '0'); + + const parser = new Parser(); + const headerToken = parser.write(header); + + expect(headerToken?.type).toEqual('header'); + if (headerToken?.type !== 'header') tarUtils.never('Token type'); + + // @ts-ignore: accessing protected member for state analysis + const state = parser.state; + expect(state).toEqual(ParserState.DATA); + + let accumulator = ''; + const decoder = new TextDecoder(); + const totalBlocks = Math.ceil( + encodedData.length / tarConstants.BLOCK_SIZE, + ); + for (let i = 0; i < totalBlocks; i++) { + const offset = i * tarConstants.BLOCK_SIZE; + + const dataToken = parser.write( + encodedData.slice(offset, offset + tarConstants.BLOCK_SIZE), + ); + expect(dataToken?.type).toEqual('data'); + if (dataToken?.type !== 'data') tarUtils.never('Token type'); + + const content = decoder.decode(dataToken.data); + accumulator += content; + } + expect(data).toEqual(accumulator); + }, + ); +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index c8e823f..bd332d7 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,121 +1,145 @@ -import type { FileStat } from '@/types'; -import fs from 'fs'; +import type { FileType, DirectoryType } from './utils'; import path from 'path'; -import { createHeader, generateEndMarker } from '@/Generator'; -import Parser from '@/Parser'; +import { test } from '@fast-check/jest'; +import { generateHeader, generateNullChunk } from '@/Generator'; import { EntryType } from '@/types'; +import Parser from '@/Parser'; +import * as tarUtils from '@/utils'; +import * as tarConstants from '@/constants'; +import * as utils from './utils'; -// TODO: actually write tests -describe('local test', () => { - test('gen', async () => { - if (process.env['CI'] != null) { - // Skip this test if on CI - expect(true).toEqual(true); - } else { - // Otherwise, run the test which creates a test archive - - const walkDir = async (walkPath: string, tokens: Array) => { - const dirContent = await fs.promises.readdir(walkPath); - - for (const dirPath of dirContent) { - const stat = await fs.promises.stat(path.join(walkPath, dirPath)); - const tarStat: FileStat = { - mtime: stat.mtime, - mode: stat.mode, - gid: stat.gid, - uid: stat.uid, - }; - - if (stat.isDirectory()) { - tokens.push(createHeader(dirPath, tarStat, EntryType.DIRECTORY)); - await walkDir(dirPath, tokens); - } else { - const tarStat: FileStat = { - mtime: stat.mtime, - mode: stat.mode, - gid: stat.gid, - uid: stat.uid, - size: stat.size, - }; - tokens.push(createHeader(dirPath, tarStat, EntryType.FILE)); - const file = await fs.promises.open( - path.join(walkPath, dirPath), - 'r', - ); - const buffer = Buffer.alloc(512, 0); - while (true) { - const { bytesRead } = await file.read(buffer, 0, 512, null); - if (bytesRead < 512) { - buffer.fill('\0', bytesRead); - tokens.push(buffer); - break; - } - tokens.push(Buffer.from(buffer)); - } - await file.close(); +describe('integration testing', () => { + test.prop([utils.virtualFsArb])( + 'should archive and unarchive a virtual file system', + (vfs) => { + const blocks: Array = []; + + const generateArchive = (entry: FileType | DirectoryType) => { + switch (entry.type) { + case EntryType.FILE: { + // Generate the header + entry = entry as FileType; + blocks.push(generateHeader(entry.path, entry.type, entry.stat)); + + // Generate the data + const encoder = new TextEncoder(); + let content = entry.content; + do { + const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); + blocks.push( + encoder.encode(dataChunk.padEnd(tarConstants.BLOCK_SIZE, '\0')), + ); + content = content.slice(tarConstants.BLOCK_SIZE); + } while (content.length > 0); + break; } - } - tokens.push(...generateEndMarker()); - }; + case EntryType.DIRECTORY: { + // Generate the header + entry = entry as DirectoryType; + blocks.push(generateHeader(entry.path, entry.type, entry.stat)); - const writeArchive = async (inPath: string, outPath: string) => { - const tokens: Array = []; - await walkDir(inPath, tokens); + // Perform the same operation on all children + for (const file of entry.children) { + generateArchive(file); + } + break; + } - const file = await fs.promises.open(outPath, 'w+'); - for (const block of tokens) { - await file.write(block); + default: + tarUtils.never('Invalid type'); } - await file.close(); }; - await expect( - writeArchive( - '/home/aryanj/Downloads/Arifureta Shokugyou Saikyou/', - '/home/aryanj/Downloads/dir/archive.tar', - ), - ).toResolve(); - } - }, 60000); - test('parsing', async () => { - if (process.env['CI'] != null) expect(true).toBeTruthy(); - const file = '/home/aryanj/Downloads/dir/archive.tar'; - - const fileHandle = await fs.promises.open(file, 'r'); - const buffer = new Uint8Array(512); - const parser = new Parser(); - let writeHandle: fs.promises.FileHandle | undefined = undefined; - const root = '/home/aryanj/Downloads/dir'; - - while (true) { - await fileHandle.read(buffer); - const output = parser.write(buffer); - - if (output == null) continue; - - if (output.type === 'header') { - if (writeHandle != null) await writeHandle.close(); - if (output.fileType === 'directory') { - await fs.promises.mkdir(path.join(root, output.fileName)); - } else { - writeHandle = await fs.promises.open( - path.join(root, output.fileName), - 'w+', - ); - } + for (const entry of vfs) { + generateArchive(entry); } + blocks.push(generateNullChunk()); + blocks.push(generateNullChunk()); - if (output.type === 'data') { - if (writeHandle == null) throw new Error('never'); - await writeHandle.write(output.data); - } + // The tar archive should be inside the blocks array now. Each block is + // a single chunk aligned to 512-byte. Now we can parse it and check if + // the parsed virtual file system matches the input. + + const parser = new Parser(); + const decoder = new TextDecoder(); + const reconstructedVfs: Array = []; + const pathStack: Map = new Map(); + let currentEntry: FileType; + + for (const chunk of blocks) { + const token = parser.write(chunk); + if (token == null) continue; + + switch (token.type) { + case 'header': { + let parsedEntry: FileType | DirectoryType; - if (output.type === 'end') { - if (writeHandle != null) await writeHandle.close(); - break; + if (token.fileType === 'file') { + parsedEntry = { + type: EntryType.FILE, + path: token.filePath, + content: '', + stat: { + mode: token.fileMode, + uid: token.ownerUid, + gid: token.ownerGid, + size: token.fileSize, + mtime: token.fileMtime, + }, + }; + } else { + parsedEntry = { + type: EntryType.DIRECTORY, + path: token.filePath, + children: [], + stat: { + mode: token.fileMode, + uid: token.ownerUid, + gid: token.ownerGid, + size: token.fileSize, + mtime: token.fileMtime, + }, + }; + } + + const parentPath = path.dirname(token.filePath); + + // If this entry is a directory, then it is pushed to the root of + // the reconstructed virtual file system and into a map at the same + // time. This allows us to add new children to the directory by + // looking up the path in a map rather than modifying the value in + // the reconstructed file system. + + if (parentPath === '/' || parentPath === '.') { + reconstructedVfs.push(parsedEntry); + } else { + // It is guaranteed that in a valid tar file, the parent will + // always exist. + const parent: DirectoryType = pathStack.get(parentPath); + parent.children.push(parsedEntry); + } + + if (parsedEntry.type === EntryType.DIRECTORY) { + pathStack.set(token.filePath, parsedEntry); + } else { + // Type narrowing doesn't work well with manually specified types + currentEntry = parsedEntry as FileType; + } + + break; + } + + case 'data': { + // It is guaranteed that in a valid tar file, a data block will + // always come after a header block for a file. + currentEntry!['content'] += decoder.decode(token.data); + break; + } + } } - } - await fileHandle.close(); - }); + + expect(reconstructedVfs).toContainAllValues(vfs); + }, + ); }); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..10cd71f --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,212 @@ +import { FileStat, HeaderOffset } from '@/types'; +import fc from 'fast-check'; +import { EntryType } from '@/types'; +import * as tarUtils from '@/utils'; +import * as tarConstants from '@/constants'; + +type FileType = { + type: EntryType; + path: string; + stat: FileStat; + content: string; +}; + +type DirectoryType = { + type: EntryType; + path: string; + stat: FileStat; + children: Array; +}; + +function splitHeaderData(data: Uint8Array) { + const view = new DataView(data.buffer); + return { + name: tarUtils.parseFilePath(view), + type: tarUtils.extractString(view, 156, 1), + mode: tarUtils.extractOctal(view, 100, 8), + uid: tarUtils.extractOctal(view, 108, 8), + gid: tarUtils.extractOctal(view, 116, 8), + size: tarUtils.extractOctal(view, 124, 12), + mtime: tarUtils.extractOctal(view, 136, 12), + format: tarUtils.extractString(view, 257, 6), + version: tarUtils.extractString(view, 263, 2), + }; +} + +const filenameArb = fc + .string({ minLength: 1, maxLength: 32 }) + .filter((name) => !name.includes('/') && name !== '.' && name !== '..') + .noShrink(); + +const fileContentArb = fc.string({ minLength: 0, maxLength: 4096 }).noShrink(); + +// Dates are stored in 11 digits of octal number. This can store from 0 to +// 0o77777777777 or 8589934591 seconds. This comes up to 2242-03-16T12:56:31. +const statDataArb = ( + type: EntryType, + content: string = '', +): fc.Arbitrary => + fc + .record({ + mode: fc.constant(0o777), + uid: fc.integer({ min: 0, max: 65535 }), + gid: fc.integer({ min: 0, max: 65535 }), + size: fc.constant(type === EntryType.FILE ? content.length : 0), + mtime: fc + .date({ + min: new Date(0), + max: new Date(0o77777777777 * 1000), + noInvalidDate: true, + }) + .map((date) => new Date(Math.floor(date.getTime() / 1000) * 1000)), // Snap to whole seconds + }) + .noShrink(); + +const fileArb = (parentPath: string = ''): fc.Arbitrary => + fc + .record({ + type: fc.constant(EntryType.FILE), + path: filenameArb.map((name) => `${parentPath}/${name}`), + content: fileContentArb, + }) + .chain((file) => + statDataArb(EntryType.FILE, file.content).map((stat) => ({ + ...file, + stat, + })), + ) + .noShrink(); + +const dirArb = ( + depth: number, + parentPath: string = '', +): fc.Arbitrary => + fc + .record({ + type: fc.constant(EntryType.DIRECTORY), + path: filenameArb.map((name) => `${parentPath}/${name}`), + }) + .chain((dir) => + fc + .array( + fc.oneof( + { weight: 3, arbitrary: fileArb(dir.path) }, + { + weight: depth > 0 ? 1 : 0, + arbitrary: dirArb(depth - 1, dir.path), + }, + ), + { + minLength: 0, + maxLength: 4, + }, + ) + .map((children) => ({ ...dir, children })), + ) + .chain((dir) => + statDataArb(EntryType.DIRECTORY).map((stat) => ({ ...dir, stat })), + ) + .noShrink(); + +const virtualFsArb = fc + .array(fc.oneof(fileArb(), dirArb(5)), { + minLength: 1, + maxLength: 10, + }) + .noShrink(); + +const tarHeaderArb = fc + .record({ + path: filenameArb, + uid: fc.nat(65535), + gid: fc.nat(65535), + size: fc.nat(65536), + typeflag: fc.constantFrom('0', '5'), + }) + .map(({ path, uid, gid, size, typeflag }) => { + const header = new Uint8Array(tarConstants.BLOCK_SIZE); + const type = typeflag as '0' | '5'; + const encoder = new TextEncoder(); + + if (type === '5') size = 0; + + // Fill header fields + header.set(encoder.encode(path), HeaderOffset.FILE_NAME); + header.set(encoder.encode('0000777'), HeaderOffset.FILE_MODE); + header.set( + encoder.encode(uid.toString(8).padStart(7, '0')), + HeaderOffset.OWNER_UID, + ); + header.set( + encoder.encode(gid.toString(8).padStart(7, '0')), + HeaderOffset.OWNER_GID, + ); + header.set( + encoder.encode(size.toString(8).padStart(11, '0') + '\0'), + HeaderOffset.FILE_SIZE, + ); + header.set(encoder.encode(' '), HeaderOffset.CHECKSUM); + header.set(encoder.encode(type), HeaderOffset.TYPE_FLAG); + header.set(encoder.encode('ustar '), HeaderOffset.USTAR_NAME); + + // Compute and set checksum + const checksum = header.reduce((sum, byte) => sum + byte, 0); + header.set( + encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '), + HeaderOffset.CHECKSUM, + ); + + return { header, stat: { type, size, path, uid, gid } }; + }) + .noShrink(); + +const tarDataArb = tarHeaderArb + .chain((header) => + fc + .record({ + header: fc.constant(header), + data: fc.string({ + minLength: header.stat.size, + maxLength: header.stat.size, + }), + }) + .map(({ header, data }) => { + const { header: headerBlock, stat } = header; + const encoder = new TextEncoder(); + const encodedData = encoder.encode(data); + + // Directories don't have any data, so set their size to zero. + let dataBlock: Uint8Array; + if (stat.type === '0') { + // Make sure the data is aligned to 512-byte chunks + dataBlock = new Uint8Array( + Math.ceil(stat.size / tarConstants.BLOCK_SIZE) * + tarConstants.BLOCK_SIZE, + ); + dataBlock.set(encodedData); + } else { + dataBlock = new Uint8Array(0); + } + + return { + header: headerBlock, + data: data, + encodedData: dataBlock, + type: stat.type, + }; + }), + ) + .noShrink(); + +export type { FileType, DirectoryType }; +export { + splitHeaderData, + filenameArb, + fileContentArb, + statDataArb, + fileArb, + dirArb, + virtualFsArb, + tarHeaderArb, + tarDataArb, +}; From 309903f4de13eeebe0163b94b5ca00a6f6de9ce7 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Mon, 3 Mar 2025 16:56:01 +1100 Subject: [PATCH 10/19] chore: switched out dataview to uint8arrays --- src/Generator.ts | 23 ++----- src/Parser.ts | 82 +++++++++++++++++------- src/constants.ts | 2 +- src/errors.ts | 40 +++++++----- src/utils.ts | 59 +++++++++++++----- tests/Generator.test.ts | 6 +- tests/Parser.test.ts | 135 ++++++++++++++++++++++++++++++++-------- tests/utils.ts | 31 +++++---- 8 files changed, 264 insertions(+), 114 deletions(-) diff --git a/src/Generator.ts b/src/Generator.ts index c1923c1..4d530e5 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -4,11 +4,6 @@ import * as errors from './errors'; import * as utils from './utils'; import * as constants from './constants'; -// Computes the checksum by summing up all the bytes in the header -function computeChecksum(header: Uint8Array): number { - return header.reduce((sum, byte) => sum + byte, 0); -} - function generateHeader( filePath: string, type: EntryType, @@ -16,24 +11,22 @@ function generateHeader( ): Uint8Array { // TODO: implement long-file-name headers if (filePath.length < 1 || filePath.length > 255) { - throw new errors.ErrorVirtualTarInvalidFileName( + throw new errors.ErrorTarGeneratorInvalidFileName( 'The file name must be longer than 1 character and shorter than 255 characters', ); } // As the size does not matter for directories, it can be undefined. However, // if the header is being generated for a file, then it needs to have a valid - // size. This guard checks that. + // size. if (stat.size == null && type === EntryType.FILE) { - throw new errors.ErrorVirtualTarInvalidStat('Size must be set for files'); + throw new errors.ErrorTarGeneratorInvalidStat('Size must be set for files'); } const size = type === EntryType.FILE ? stat.size : 0; // The time can be undefined, which would be referring to epoch 0. const time = utils.dateToUnixTime(stat.mtime ?? new Date()); - // Make sure to initialise the header with zeros to avoid writing nullish - // blocks. const header = new Uint8Array(constants.BLOCK_SIZE); // The TAR headers follow this structure @@ -112,13 +105,7 @@ function generateHeader( ); // The checksum is calculated as the sum of all bytes in the header. It is - // padded using ASCII spaces, as we currently don't have all the data yet. - utils.writeBytesToArray( - header, - utils.pad('', HeaderSize.CHECKSUM, ' '), - HeaderOffset.CHECKSUM, - HeaderSize.CHECKSUM, - ); + // left blank for later calculation. // The type of file is written as a single byte in the header. utils.writeBytesToArray( @@ -174,7 +161,7 @@ function generateHeader( ); // Updating with the new checksum - const checksum = computeChecksum(header); + const checksum = utils.calculateChecksum(header); // Note the extra space in the padding for the checksum value. It is // intentionally placed there. The padding for checksum is ASCII spaces diff --git a/src/Parser.ts b/src/Parser.ts index df58eb1..0f6f7db 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -5,50 +5,86 @@ import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; -function parseHeader(view: DataView): HeaderToken { - // TODO: confirm integrity by checking against checksum - const filePath = utils.parseFilePath(view); +function parseHeader(array: Uint8Array): HeaderToken { + // Validate header by checking checksum and magic string + const headerChecksum = utils.extractOctal( + array, + HeaderOffset.CHECKSUM, + HeaderSize.CHECKSUM, + ); + const calculatedChecksum = utils.calculateChecksum(array); + + if (headerChecksum !== calculatedChecksum) { + throw new errors.ErrorTarParserInvalidHeader( + `Expected checksum to be ${calculatedChecksum} but received ${headerChecksum}`, + ); + } + + const ustarMagic = utils.extractString( + array, + HeaderOffset.USTAR_NAME, + HeaderSize.USTAR_NAME, + ); + if (ustarMagic !== constants.USTAR_NAME) { + throw new errors.ErrorTarParserInvalidHeader( + `Expected ustar magic to be '${constants.USTAR_NAME}', got '${ustarMagic}'`, + ); + } + + const ustarVersion = utils.extractString( + array, + HeaderOffset.USTAR_VERSION, + HeaderSize.USTAR_VERSION, + ); + if (ustarVersion !== constants.USTAR_VERSION) { + throw new errors.ErrorTarParserInvalidHeader( + `Expected ustar version to be '${constants.USTAR_VERSION}', got '${ustarVersion}'`, + ); + } + + // Extract the relevant metadata from the header + const filePath = utils.parseFilePath(array); const fileSize = utils.extractOctal( - view, + array, HeaderOffset.FILE_SIZE, HeaderSize.FILE_SIZE, ); const fileMtime = new Date( - utils.extractOctal(view, HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME) * + utils.extractOctal(array, HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME) * 1000, ); const fileMode = utils.extractOctal( - view, + array, HeaderOffset.FILE_MODE, HeaderSize.FILE_MODE, ); const ownerGid = utils.extractOctal( - view, + array, HeaderOffset.OWNER_GID, HeaderSize.OWNER_GID, ); const ownerUid = utils.extractOctal( - view, + array, HeaderOffset.OWNER_UID, HeaderSize.OWNER_UID, ); const ownerName = utils.extractString( - view, + array, HeaderOffset.OWNER_NAME, HeaderSize.OWNER_NAME, ); const ownerGroupName = utils.extractString( - view, + array, HeaderOffset.OWNER_GROUPNAME, HeaderSize.OWNER_GROUPNAME, ); const ownerUserName = utils.extractString( - view, + array, HeaderOffset.OWNER_USERNAME, HeaderSize.OWNER_USERNAME, ); const fileType = - utils.extractString(view, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG) === + utils.extractString(array, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG) === EntryType.FILE ? 'file' : 'directory'; @@ -68,11 +104,11 @@ function parseHeader(view: DataView): HeaderToken { }; } -function parseData(view: DataView, remainingBytes: number): DataToken { +function parseData(array: Uint8Array, remainingBytes: number): DataToken { if (remainingBytes > 512) { - return { type: 'data', data: utils.extractBytes(view) }; + return { type: 'data', data: utils.extractBytes(array) }; } else { - const data = utils.extractBytes(view, 0, remainingBytes); + const data = utils.extractBytes(array, 0, remainingBytes); return { type: 'data', data: data }; } } @@ -83,29 +119,27 @@ class Parser { write(data: Uint8Array) { if (data.byteLength !== constants.BLOCK_SIZE) { - throw new errors.ErrorVirtualTarBlockSize( + throw new errors.ErrorTarParserBlockSize( `Expected block size to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`, ); } - const view = new DataView(data.buffer, 0, constants.BLOCK_SIZE); - switch (this.state) { case ParserState.ENDED: { - throw new errors.ErrorVirtualTarEndOfArchive( + throw new errors.ErrorTarParserEndOfArchive( 'Archive has already ended', ); } case ParserState.READY: { // Check if we need to parse the end-of-archive marker - if (utils.checkNullView(view)) { + if (utils.isNullBlock(data)) { this.state = ParserState.NULL; return; } // Set relevant state if the header corresponds to a file - const headerToken = parseHeader(view); + const headerToken = parseHeader(data); if (headerToken.fileType === 'file') { this.state = ParserState.DATA; this.remainingBytes = headerToken.fileSize; @@ -114,18 +148,18 @@ class Parser { } case ParserState.DATA: { - const parsedData = parseData(view, this.remainingBytes); + const parsedData = parseData(data, this.remainingBytes); this.remainingBytes -= 512; if (this.remainingBytes < 0) this.state = ParserState.READY; return parsedData; } case ParserState.NULL: { - if (utils.checkNullView(view)) { + if (utils.isNullBlock(data)) { this.state = ParserState.ENDED; return { type: 'end' } as EndToken; } else { - throw new errors.ErrorVirtualTarEndOfArchive( + throw new errors.ErrorTarParserEndOfArchive( 'Received garbage data after first end marker', ); } diff --git a/src/constants.ts b/src/constants.ts index 34dc5db..dd7ae5c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,3 @@ export const BLOCK_SIZE = 512; -export const USTAR_NAME = 'ustar\0'; +export const USTAR_NAME = 'ustar'; export const USTAR_VERSION = '00'; diff --git a/src/errors.ts b/src/errors.ts index a05709c..0eecccd 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,39 +1,49 @@ import { AbstractError } from '@matrixai/errors'; -class ErrorVirtualTar extends AbstractError { +class ErrorTar extends AbstractError { static description = 'VirtualTar errors'; } -class ErrorVirtualTarUndefinedBehaviour extends ErrorVirtualTar { +class ErrorVirtualTarUndefinedBehaviour extends ErrorTar { static description = 'You should never see this error'; } -class ErrorVirtualTarInvalidFileName extends ErrorVirtualTar { - static description = 'The provided file name is invalid'; +class ErrorTarGenerator extends ErrorTar { + static description = 'VirtualTar genereator errors'; } -class ErrorVirtualTarInvalidHeader extends ErrorVirtualTar { - static description = 'The header has invalid data'; +class ErrorTarGeneratorInvalidFileName extends ErrorTarGenerator { + static description = 'The provided file name is invalid'; } -class ErrorVirtualTarInvalidStat extends ErrorVirtualTar { +class ErrorTarGeneratorInvalidStat extends ErrorTarGenerator { static description = 'The stat contains invalid data'; } -class ErrorVirtualTarBlockSize extends ErrorVirtualTar { +class ErrorTarParser extends ErrorTar { + static description = 'VirtualTar parsing errors'; +} + +class ErrorTarParserInvalidHeader extends ErrorTarParser { + static description = 'The checksum did not match the header'; +} + +class ErrorTarParserBlockSize extends ErrorTarParser { static description = 'The block size is incorrect'; } -class ErrorVirtualTarEndOfArchive extends ErrorVirtualTar { +class ErrorTarParserEndOfArchive extends ErrorTarParser { static description = 'No data can come after an end-of-archive marker'; } export { - ErrorVirtualTar, + ErrorTar, + ErrorTarGenerator, ErrorVirtualTarUndefinedBehaviour, - ErrorVirtualTarInvalidFileName, - ErrorVirtualTarInvalidHeader, - ErrorVirtualTarInvalidStat, - ErrorVirtualTarBlockSize, - ErrorVirtualTarEndOfArchive, + ErrorTarGeneratorInvalidFileName, + ErrorTarGeneratorInvalidStat, + ErrorTarParser, + ErrorTarParserInvalidHeader, + ErrorTarParserBlockSize, + ErrorTarParserEndOfArchive, }; diff --git a/src/utils.ts b/src/utils.ts index c1184ea..e0ecce9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,8 +2,6 @@ import { HeaderOffset, HeaderSize } from './types'; import * as errors from './errors'; import * as constants from './constants'; -const nullRegex = /\0/g; - function never(message: string): never { throw new errors.ErrorVirtualTarUndefinedBehaviour(message); } @@ -21,6 +19,20 @@ function pad( } } +function calculateChecksum(array: Uint8Array): number { + return array.reduce((sum, byte, index) => { + // Checksum placeholder is ASCII space, so assume checksum character is + // space while computing it. + if ( + index >= HeaderOffset.CHECKSUM && + index < HeaderOffset.CHECKSUM + HeaderSize.CHECKSUM + ) { + return sum + 32; + } + return sum + byte; + }); +} + function splitFileName( fileName: string, offset: number, @@ -38,50 +50,64 @@ function dateToUnixTime(date: Date): number { const decoder = new TextDecoder('ascii'); +// Returns a view of the array with the given offset and length. Note that the +// returned value is a view and not a copy, so any modifications to the data +// will affect the original data. function extractBytes( - view: DataView, + array: Uint8Array, offset?: number, length?: number, + stopOnNull: boolean = false, ): Uint8Array { - return new Uint8Array(view.buffer, offset, length); + const start = offset ?? 0; + let end = length != null ? start + length : array.length; + + if (stopOnNull) { + for (let i = start; i < end; i++) { + if (array[i] === 0) { + end = i; + break; + } + } + } + + return array.subarray(start, end); } function extractString( - view: DataView, + array: Uint8Array, offset?: number, length?: number, ): string { - return decoder - .decode(extractBytes(view, offset, length)) - .replace(nullRegex, ''); + return decoder.decode(extractBytes(array, offset, length, true)); } function extractOctal( - view: DataView, + array: Uint8Array, offset?: number, length?: number, ): number { - const value = extractString(view, offset, length); + const value = extractString(array, offset, length); return value.length > 0 ? parseInt(value, 8) : 0; } -function parseFilePath(view: DataView) { +function parseFilePath(array: Uint8Array) { const fileNameLower = extractString( - view, + array, HeaderOffset.FILE_NAME, HeaderSize.FILE_NAME, ); const fileNameUpper = extractString( - view, + array, HeaderOffset.FILE_NAME_EXTRA, HeaderSize.FILE_NAME_EXTRA, ); return fileNameLower + fileNameUpper; } -function checkNullView(view: DataView): boolean { +function isNullBlock(array: Uint8Array): boolean { for (let i = 0; i < constants.BLOCK_SIZE; i++) { - if (view.getUint8(i) !== 0) return false; + if (array[i] !== 0) return false; } return true; } @@ -110,12 +136,13 @@ function writeBytesToArray( export { never, pad, + calculateChecksum, splitFileName, dateToUnixTime, extractBytes, extractString, extractOctal, parseFilePath, - checkNullView, + isNullBlock, writeBytesToArray, }; diff --git a/tests/Generator.test.ts b/tests/Generator.test.ts index e14e754..9a90e90 100644 --- a/tests/Generator.test.ts +++ b/tests/Generator.test.ts @@ -1,5 +1,5 @@ import { test } from '@fast-check/jest'; -import { generateHeader } from '@/Generator'; +import { generateHeader, generateNullChunk } from '@/Generator'; import { EntryType } from '@/types'; import * as tarUtils from '@/utils'; import { dirArb, fileArb, splitHeaderData } from './utils'; @@ -40,4 +40,8 @@ describe('archive generation', () => { expect(format).toEqual('ustar'); expect(version).toEqual('00'); }); + + test('should generate a valid null chunk', () => { + expect(generateNullChunk().reduce((sum, byte) => (sum += byte))).toBe(0); + }); }); diff --git a/tests/Parser.test.ts b/tests/Parser.test.ts index c4ca1bc..dd83f35 100644 --- a/tests/Parser.test.ts +++ b/tests/Parser.test.ts @@ -1,10 +1,12 @@ import { test } from '@fast-check/jest'; import fc from 'fast-check'; -import { ParserState } from '@/types'; import Parser from '@/Parser'; +import { generateNullChunk } from '@/Generator'; +import { HeaderOffset, ParserState } from '@/types'; +import * as tarErrors from '@/errors'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; -import { tarHeaderArb, tarDataArb } from './utils'; +import { tarHeaderArb } from './utils'; describe('archive parsing', () => { test.prop([tarHeaderArb])( @@ -39,40 +41,119 @@ describe('archive parsing', () => { }, ); - test.prop([tarDataArb])( - 'should parse file with data', - ({ header, type, data, encodedData }) => { - // Make sure we are only testing against files - fc.pre(type === '0'); - + test.prop([tarHeaderArb])( + 'should parse headers with correct state', + ({ header, stat }) => { + const { type, path, uid, gid } = stat; const parser = new Parser(); - const headerToken = parser.write(header); + const token = parser.write(header); - expect(headerToken?.type).toEqual('header'); - if (headerToken?.type !== 'header') tarUtils.never('Token type'); + expect(token?.type).toEqual('header'); + if (token?.type !== 'header') tarUtils.never('Token type'); // @ts-ignore: accessing protected member for state analysis const state = parser.state; - expect(state).toEqual(ParserState.DATA); - let accumulator = ''; - const decoder = new TextDecoder(); - const totalBlocks = Math.ceil( - encodedData.length / tarConstants.BLOCK_SIZE, + switch (type) { + case '0': + expect(state).toEqual(ParserState.DATA); + expect(token.fileType).toEqual('file'); + break; + case '5': + expect(state).toEqual(ParserState.READY); + expect(token.fileType).toEqual('directory'); + break; + default: + tarUtils.never('Invalid state'); + } + + expect(token.filePath).toEqual(path); + expect(token.ownerUid).toEqual(uid); + expect(token.ownerGid).toEqual(gid); + }, + ); + + test.prop([fc.uint8Array({ minLength: 512, maxLength: 512 })])( + 'should fail to parse gibberish data', + (data) => { + // Make sure a null block doesn't get tested. It is reserved for ending a + // tar archive. + fc.pre(!tarUtils.isNullBlock(data)); + + const parser = new Parser(); + expect(() => parser.write(data)).toThrowError( + tarErrors.ErrorTarParserInvalidHeader, ); - for (let i = 0; i < totalBlocks; i++) { - const offset = i * tarConstants.BLOCK_SIZE; + }, + ); - const dataToken = parser.write( - encodedData.slice(offset, offset + tarConstants.BLOCK_SIZE), - ); - expect(dataToken?.type).toEqual('data'); - if (dataToken?.type !== 'data') tarUtils.never('Token type'); + test.prop([fc.uint8Array()])( + 'should fail to parse blocks with arbitrary size', + (data) => { + // Make sure a null block doesn't get tested. It is reserved for ending a + // tar archive. + fc.pre(data.length !== tarConstants.BLOCK_SIZE); - const content = decoder.decode(dataToken.data); - accumulator += content; - } - expect(data).toEqual(accumulator); + const parser = new Parser(); + expect(() => parser.write(data)).toThrowError( + tarErrors.ErrorTarParserBlockSize, + ); + }, + ); + + test.prop([tarHeaderArb, fc.uint8Array({ minLength: 8, maxLength: 8 })], { + numRuns: 1, + })( + 'should fail to parse header with an invalid checksum', + ({ header }, checksum) => { + header.set(checksum, HeaderOffset.CHECKSUM); + const parser = new Parser(); + expect(() => parser.write(header)).toThrowError( + tarErrors.ErrorTarParserInvalidHeader, + ); }, ); + + describe('parsing end of archive', () => { + test('should parse end of archive', () => { + const parser = new Parser(); + + const token1 = parser.write(generateNullChunk()); + expect(token1).toBeUndefined(); + // @ts-ignore: accessing protected member for state analysis + expect(parser.state).toEqual(ParserState.NULL); + + const token2 = parser.write(generateNullChunk()); + expect(token2?.type).toEqual('end'); + // @ts-ignore: accessing protected member for state analysis + expect(parser.state).toEqual(ParserState.ENDED); + }); + + test.prop([tarHeaderArb], { numRuns: 1 })( + 'should fail if end of archive is malformed', + ({ header }) => { + const parser = new Parser(); + + const token1 = parser.write(generateNullChunk()); + expect(token1).toBeUndefined(); + + expect(() => parser.write(header)).toThrowError( + tarErrors.ErrorTarParserEndOfArchive, + ); + }, + ); + + test.prop([tarHeaderArb], { numRuns: 1 })( + 'should fail if data is written after parser ending', + ({ header }) => { + const parser = new Parser(); + // @ts-ignore: updating parser state for testing + parser.state = ParserState.ENDED; + + expect(() => parser.write(header)).toThrowError( + tarErrors.ErrorTarParserEndOfArchive, + ); + }, + ); + }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 10cd71f..7338175 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,5 +1,6 @@ -import { FileStat, HeaderOffset } from '@/types'; +import type { FileStat } from '@/types'; import fc from 'fast-check'; +import { HeaderOffset } from '@/types'; import { EntryType } from '@/types'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; @@ -19,17 +20,16 @@ type DirectoryType = { }; function splitHeaderData(data: Uint8Array) { - const view = new DataView(data.buffer); return { - name: tarUtils.parseFilePath(view), - type: tarUtils.extractString(view, 156, 1), - mode: tarUtils.extractOctal(view, 100, 8), - uid: tarUtils.extractOctal(view, 108, 8), - gid: tarUtils.extractOctal(view, 116, 8), - size: tarUtils.extractOctal(view, 124, 12), - mtime: tarUtils.extractOctal(view, 136, 12), - format: tarUtils.extractString(view, 257, 6), - version: tarUtils.extractString(view, 263, 2), + name: tarUtils.parseFilePath(data), + type: tarUtils.extractString(data, 156, 1), + mode: tarUtils.extractOctal(data, 100, 8), + uid: tarUtils.extractOctal(data, 108, 8), + gid: tarUtils.extractOctal(data, 116, 8), + size: tarUtils.extractOctal(data, 124, 12), + mtime: tarUtils.extractOctal(data, 136, 12), + format: tarUtils.extractString(data, 257, 6), + version: tarUtils.extractString(data, 263, 2), }; } @@ -147,7 +147,14 @@ const tarHeaderArb = fc ); header.set(encoder.encode(' '), HeaderOffset.CHECKSUM); header.set(encoder.encode(type), HeaderOffset.TYPE_FLAG); - header.set(encoder.encode('ustar '), HeaderOffset.USTAR_NAME); + header.set( + encoder.encode(tarConstants.USTAR_NAME), + HeaderOffset.USTAR_NAME, + ); + header.set( + encoder.encode(tarConstants.USTAR_VERSION), + HeaderOffset.USTAR_VERSION, + ); // Compute and set checksum const checksum = header.reduce((sum, byte) => sum + byte, 0); From 6317bfe53556219855cc02c2f75d59a6857501b4 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Thu, 6 Mar 2025 17:34:21 +1100 Subject: [PATCH 11/19] feat: added support for extended metadata --- src/Generator.ts | 240 ++++++++++++++++++++++++++------------ src/Parser.ts | 64 +++++++---- src/constants.ts | 1 + src/errors.ts | 57 ++++++--- src/index.ts | 7 +- src/types.ts | 50 +++++--- src/utils.ts | 143 ++++++++++++++++++++--- tests/Generator.test.ts | 192 ++++++++++++++++++++++++------- tests/Parser.test.ts | 134 +++++++++++++--------- tests/index.test.ts | 147 +++++++++++++++++------- tests/utils.ts | 248 +++++++++++++++++++++++++++++----------- 11 files changed, 935 insertions(+), 348 deletions(-) diff --git a/src/Generator.ts b/src/Generator.ts index 4d530e5..e8ce581 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -1,64 +1,49 @@ import type { FileStat } from './types'; -import { EntryType, HeaderSize, HeaderOffset } from './types'; +import { GeneratorState, EntryType, HeaderSize, HeaderOffset } from './types'; import * as errors from './errors'; import * as utils from './utils'; import * as constants from './constants'; -function generateHeader( - filePath: string, - type: EntryType, - stat: FileStat, -): Uint8Array { - // TODO: implement long-file-name headers - if (filePath.length < 1 || filePath.length > 255) { - throw new errors.ErrorTarGeneratorInvalidFileName( - 'The file name must be longer than 1 character and shorter than 255 characters', +function generateHeader(filePath: string, type: EntryType, stat: FileStat) { + if (filePath.length > 255) { + throw new errors.ErrorVirtualTarGeneratorInvalidFileName( + 'The file name must shorter than 255 characters', ); } - // As the size does not matter for directories, it can be undefined. However, - // if the header is being generated for a file, then it needs to have a valid - // size. - if (stat.size == null && type === EntryType.FILE) { - throw new errors.ErrorTarGeneratorInvalidStat('Size must be set for files'); - } - const size = type === EntryType.FILE ? stat.size : 0; - // The time can be undefined, which would be referring to epoch 0. const time = utils.dateToUnixTime(stat.mtime ?? new Date()); const header = new Uint8Array(constants.BLOCK_SIZE); - // The TAR headers follow this structure - // Start Size Description - // ------------------------------ - // 0 100 File name (first 100 bytes) - // 100 8 File permissions (null-padded octal) - // 108 8 Owner UID (null-padded octal) - // 116 8 Owner GID (null-padded octal) - // 124 12 File size (null-padded octal, 0 for directories) - // 136 12 Mtime (null-padded octal) - // 148 8 Checksum (fill with ASCII spaces for computation) - // 156 1 Type flag ('0' for file, '5' for directory) - // 157 100 File owner name (null-terminated ASCII/UTF-8) - // 257 6 'ustar\0' (magic string) - // 263 2 '00' (ustar version) - // 265 32 Owner user name (null-terminated ASCII/UTF-8) - // 297 32 Owner group name (null-terminated ASCII/UTF-8) - // 329 8 Device major (unset in this implementation) - // 337 8 Device minor (unset in this implementation) - // 345 155 File name (last 155 bytes, total 255 bytes, null-padded) - // 500 12 '\0' (unused) - // - // Note that all numbers are in stringified octal format. - - // The first half of the file name (upto 100 bytes) is stored here. - utils.writeBytesToArray( - header, - utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME), - HeaderOffset.FILE_NAME, - HeaderSize.FILE_NAME, - ); + // If the length of the file path is less than 100 bytes, then we write it to + // the file name. Otherwise, we write it into the file name prefix and append + // file name to it. + if (filePath.length < HeaderSize.FILE_NAME) { + utils.writeBytesToArray( + header, + utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME), + HeaderOffset.FILE_NAME, + HeaderSize.FILE_NAME, + ); + } else { + utils.writeBytesToArray( + header, + utils.splitFileName( + filePath, + HeaderSize.FILE_NAME, + HeaderSize.FILE_NAME_PREFIX, + ), + HeaderOffset.FILE_NAME, + HeaderSize.FILE_NAME, + ); + utils.writeBytesToArray( + header, + utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME), + HeaderOffset.FILE_NAME_PREFIX, + HeaderSize.FILE_NAME_PREFIX, + ); + } // The file permissions, or the mode, is stored in the next chunk. This is // stored in an octal number format. @@ -89,7 +74,7 @@ function generateHeader( // directories, and it must be set for files. utils.writeBytesToArray( header, - utils.pad(size ?? '', HeaderSize.FILE_SIZE, '0', '\0'), + utils.pad(stat.size ?? '', HeaderSize.FILE_SIZE, '0', '\0'), HeaderOffset.FILE_SIZE, HeaderSize.FILE_SIZE, ); @@ -115,7 +100,7 @@ function generateHeader( HeaderSize.TYPE_FLAG, ); - // File owner name will be null, as regular stat-ing cannot extract that + // Link name will be null, as regular stat-ing cannot extract that // information. // This value is the USTAR magic string which makes this file appear as @@ -147,19 +132,6 @@ function generateHeader( // Device minor will be null, as this specific to linux kernel knowing what // drivers to use for executing certain files, and is irrelevant here. - // The second half of the file name is entered here. This chunk handles file - // names ranging 100 to 255 characters. - utils.writeBytesToArray( - header, - utils.splitFileName( - filePath, - HeaderSize.FILE_NAME, - HeaderSize.FILE_NAME_EXTRA, - ), - HeaderOffset.FILE_NAME_EXTRA, - HeaderSize.FILE_NAME_EXTRA, - ); - // Updating with the new checksum const checksum = utils.calculateChecksum(header); @@ -168,7 +140,7 @@ function generateHeader( // instead of null, which is why it is used like this here. utils.writeBytesToArray( header, - utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0 '), + utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0'), HeaderOffset.CHECKSUM, HeaderSize.CHECKSUM, ); @@ -176,11 +148,139 @@ function generateHeader( return header; } -// Creates a single null block. A null block is a block filled with all zeros. -// This is needed to end the archive, as two of these blocks mark the end of -// archive. -function generateNullChunk() { - return new Uint8Array(constants.BLOCK_SIZE); +/** + * The TAR headers follow this structure + * Start Size Description + * ------------------------------ + * 0 100 File name (first 100 bytes) + * 100 8 File mode (null-padded octal) + * 108 8 Owner user id (null-padded octal) + * 116 8 Owner group id (null-padded octal) + * 124 12 File size in bytes (null-padded octal, 0 for directories) + * 136 12 Mtime (null-padded octal) + * 148 8 Checksum (fill with ASCII spaces for computation) + * 156 1 Type flag ('0' for file, '5' for directory) + * 157 100 Link name (null-terminated ASCII/UTF-8) + * 257 6 'ustar\0' (magic string) + * 263 2 '00' (ustar version) + * 265 32 Owner user name (null-terminated ASCII/UTF-8) + * 297 32 Owner group name (null-terminated ASCII/UTF-8) + * 329 8 Device major (unset in this implementation) + * 337 8 Device minor (unset in this implementation) + * 345 155 File name (last 155 bytes, total 255 bytes, null-padded) + * 500 12 '\0' (unused) + * + * Note that all numbers are in stringified octal format. + */ +class Generator { + protected state: GeneratorState = GeneratorState.READY; + protected remainingBytes = 0; + + generateFile(filePath: string, stat: FileStat): Uint8Array { + if (this.state === GeneratorState.READY) { + // Make sure the size is valid + if (stat.size == null) { + throw new errors.ErrorVirtualTarGeneratorInvalidStat( + 'Files should have valid file sizes', + ); + } + + const generatedBlock = generateHeader(filePath, EntryType.FILE, stat); + + // If no data is in the file, then there is no need of a data block. It + // will remain as READY. + if (stat.size !== 0) this.state = GeneratorState.DATA; + this.remainingBytes = stat.size; + + return generatedBlock; + } + throw new errors.ErrorVirtualTarGeneratorInvalidState( + `Expected state ${GeneratorState[GeneratorState.READY]} but got ${ + GeneratorState[this.state] + }`, + ); + } + + generateDirectory(filePath: string, stat: FileStat): Uint8Array { + if (this.state === GeneratorState.READY) { + const directoryStat: FileStat = { + size: 0, + mode: stat.mode, + mtime: stat.mtime, + uid: stat.uid, + gid: stat.gid, + }; + return generateHeader(filePath, EntryType.DIRECTORY, directoryStat); + } + throw new errors.ErrorVirtualTarGeneratorInvalidState( + `Expected state ${GeneratorState[GeneratorState.READY]} but got ${ + GeneratorState[this.state] + }`, + ); + } + + generateExtended(size: number): Uint8Array { + if (this.state === GeneratorState.READY) { + this.state = GeneratorState.DATA; + this.remainingBytes = size; + return generateHeader('', EntryType.EXTENDED, { size }); + } + throw new errors.ErrorVirtualTarGeneratorInvalidState( + `Expected state ${GeneratorState[GeneratorState.READY]} but got ${ + GeneratorState[this.state] + }`, + ); + } + + generateData(data: Uint8Array): Uint8Array { + if (data.byteLength > constants.BLOCK_SIZE) { + throw new errors.ErrorVirtualTarGeneratorBlockSize( + `Expected data to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`, + ); + } + + if (this.state === GeneratorState.DATA) { + if (this.remainingBytes >= constants.BLOCK_SIZE) { + this.remainingBytes -= constants.BLOCK_SIZE; + if (this.remainingBytes === 0) this.state = GeneratorState.READY; + return data; + } else { + // Update state + this.remainingBytes = 0; + this.state = GeneratorState.READY; + + // Pad the remaining data with nulls + const paddedData = new Uint8Array(constants.BLOCK_SIZE); + paddedData.set(data, 0); + return paddedData; + } + } + + throw new errors.ErrorVirtualTarGeneratorInvalidState( + `Expected state ${GeneratorState[GeneratorState.DATA]} but got ${ + GeneratorState[this.state] + }`, + ); + } + + // Creates a single null block. A null block is a block filled with all zeros. + // This is needed to end the archive, as two of these blocks mark the end of + // archive. + generateEnd() { + switch (this.state) { + case GeneratorState.READY: + this.state = GeneratorState.NULL; + break; + case GeneratorState.NULL: + this.state = GeneratorState.ENDED; + break; + default: + throw new errors.ErrorVirtualTarGeneratorEndOfArchive( + 'Exactly two null chunks should be generated consecutively to end archive', + ); + } + return new Uint8Array(constants.BLOCK_SIZE); + } } -export { generateHeader, generateNullChunk }; +export default Generator; diff --git a/src/Parser.ts b/src/Parser.ts index 0f6f7db..27ad969 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,11 +1,11 @@ -import type { HeaderToken, DataToken, EndToken } from './types'; +import type { TokenHeader, TokenData, TokenEnd } from './types'; import { ParserState } from './types'; import { HeaderOffset, HeaderSize, EntryType } from './types'; import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; -function parseHeader(array: Uint8Array): HeaderToken { +function parseHeader(array: Uint8Array): TokenHeader { // Validate header by checking checksum and magic string const headerChecksum = utils.extractOctal( array, @@ -15,7 +15,7 @@ function parseHeader(array: Uint8Array): HeaderToken { const calculatedChecksum = utils.calculateChecksum(array); if (headerChecksum !== calculatedChecksum) { - throw new errors.ErrorTarParserInvalidHeader( + throw new errors.ErrorVirtualTarParserInvalidHeader( `Expected checksum to be ${calculatedChecksum} but received ${headerChecksum}`, ); } @@ -26,7 +26,7 @@ function parseHeader(array: Uint8Array): HeaderToken { HeaderSize.USTAR_NAME, ); if (ustarMagic !== constants.USTAR_NAME) { - throw new errors.ErrorTarParserInvalidHeader( + throw new errors.ErrorVirtualTarParserInvalidHeader( `Expected ustar magic to be '${constants.USTAR_NAME}', got '${ustarMagic}'`, ); } @@ -37,7 +37,7 @@ function parseHeader(array: Uint8Array): HeaderToken { HeaderSize.USTAR_VERSION, ); if (ustarVersion !== constants.USTAR_VERSION) { - throw new errors.ErrorTarParserInvalidHeader( + throw new errors.ErrorVirtualTarParserInvalidHeader( `Expected ustar version to be '${constants.USTAR_VERSION}', got '${ustarVersion}'`, ); } @@ -70,8 +70,8 @@ function parseHeader(array: Uint8Array): HeaderToken { ); const ownerName = utils.extractString( array, - HeaderOffset.OWNER_NAME, - HeaderSize.OWNER_NAME, + HeaderOffset.LINK_NAME, + HeaderSize.LINK_NAME, ); const ownerGroupName = utils.extractString( array, @@ -83,11 +83,27 @@ function parseHeader(array: Uint8Array): HeaderToken { HeaderOffset.OWNER_USERNAME, HeaderSize.OWNER_USERNAME, ); - const fileType = - utils.extractString(array, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG) === - EntryType.FILE - ? 'file' - : 'directory'; + let fileType: 'file' | 'directory' | 'metadata'; + const type = utils.extractString( + array, + HeaderOffset.TYPE_FLAG, + HeaderSize.TYPE_FLAG, + ); + switch (type) { + case EntryType.FILE: + fileType = 'file'; + break; + case EntryType.DIRECTORY: + fileType = 'directory'; + break; + case EntryType.EXTENDED: + fileType = 'metadata'; + break; + default: + throw new errors.ErrorVirtualTarParserInvalidHeader( + `Got invalid file type ${type}`, + ); + } return { type: 'header', @@ -104,7 +120,7 @@ function parseHeader(array: Uint8Array): HeaderToken { }; } -function parseData(array: Uint8Array, remainingBytes: number): DataToken { +function parseData(array: Uint8Array, remainingBytes: number): TokenData { if (remainingBytes > 512) { return { type: 'data', data: utils.extractBytes(array) }; } else { @@ -119,14 +135,14 @@ class Parser { write(data: Uint8Array) { if (data.byteLength !== constants.BLOCK_SIZE) { - throw new errors.ErrorTarParserBlockSize( + throw new errors.ErrorVirtualTarParserBlockSize( `Expected block size to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`, ); } switch (this.state) { case ParserState.ENDED: { - throw new errors.ErrorTarParserEndOfArchive( + throw new errors.ErrorVirtualTarParserEndOfArchive( 'Archive has already ended', ); } @@ -138,9 +154,17 @@ class Parser { return; } - // Set relevant state if the header corresponds to a file + // Set relevant state if the header corresponds to a file. If the file + // size 0, then no data blocks will follow the header. const headerToken = parseHeader(data); if (headerToken.fileType === 'file') { + if (headerToken.fileSize !== 0) { + this.state = ParserState.DATA; + this.remainingBytes = headerToken.fileSize; + } + } else if (headerToken.fileType === 'metadata') { + // A header might not have any data but a metadata header will always + // be followed by data. this.state = ParserState.DATA; this.remainingBytes = headerToken.fileSize; } @@ -149,17 +173,17 @@ class Parser { case ParserState.DATA: { const parsedData = parseData(data, this.remainingBytes); - this.remainingBytes -= 512; - if (this.remainingBytes < 0) this.state = ParserState.READY; + this.remainingBytes -= constants.BLOCK_SIZE; + if (this.remainingBytes <= 0) this.state = ParserState.READY; return parsedData; } case ParserState.NULL: { if (utils.isNullBlock(data)) { this.state = ParserState.ENDED; - return { type: 'end' } as EndToken; + return { type: 'end' } as TokenEnd; } else { - throw new errors.ErrorTarParserEndOfArchive( + throw new errors.ErrorVirtualTarParserEndOfArchive( 'Received garbage data after first end marker', ); } diff --git a/src/constants.ts b/src/constants.ts index dd7ae5c..6ab9b20 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,4 @@ export const BLOCK_SIZE = 512; +export const STANDARD_PATH_SIZE = 255; export const USTAR_NAME = 'ustar'; export const USTAR_VERSION = '00'; diff --git a/src/errors.ts b/src/errors.ts index 0eecccd..6df6f0c 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,49 +1,72 @@ import { AbstractError } from '@matrixai/errors'; -class ErrorTar extends AbstractError { +class ErrorVirtualTar extends AbstractError { static description = 'VirtualTar errors'; } -class ErrorVirtualTarUndefinedBehaviour extends ErrorTar { +class ErrorVirtualTarUndefinedBehaviour extends ErrorVirtualTar { static description = 'You should never see this error'; } -class ErrorTarGenerator extends ErrorTar { +class ErrorVirtualTarGenerator extends ErrorVirtualTar { static description = 'VirtualTar genereator errors'; } -class ErrorTarGeneratorInvalidFileName extends ErrorTarGenerator { +class ErrorVirtualTarGeneratorInvalidFileName< + T, +> extends ErrorVirtualTarGenerator { static description = 'The provided file name is invalid'; } -class ErrorTarGeneratorInvalidStat extends ErrorTarGenerator { +class ErrorVirtualTarGeneratorInvalidStat< + T, +> extends ErrorVirtualTarGenerator { static description = 'The stat contains invalid data'; } -class ErrorTarParser extends ErrorTar { +class ErrorVirtualTarGeneratorBlockSize extends ErrorVirtualTarGenerator { + static description = 'The block size is incorrect'; +} + +class ErrorVirtualTarGeneratorEndOfArchive< + T, +> extends ErrorVirtualTarGenerator { + static description = 'No data can come after an end-of-archive marker'; +} + +class ErrorVirtualTarGeneratorInvalidState< + T, +> extends ErrorVirtualTarGenerator { + static description = 'The state is incorrect for the desired operation'; +} + +class ErrorVirtualTarParser extends ErrorVirtualTar { static description = 'VirtualTar parsing errors'; } -class ErrorTarParserInvalidHeader extends ErrorTarParser { +class ErrorVirtualTarParserInvalidHeader extends ErrorVirtualTarParser { static description = 'The checksum did not match the header'; } -class ErrorTarParserBlockSize extends ErrorTarParser { +class ErrorVirtualTarParserBlockSize extends ErrorVirtualTarParser { static description = 'The block size is incorrect'; } -class ErrorTarParserEndOfArchive extends ErrorTarParser { +class ErrorVirtualTarParserEndOfArchive extends ErrorVirtualTarParser { static description = 'No data can come after an end-of-archive marker'; } export { - ErrorTar, - ErrorTarGenerator, + ErrorVirtualTar, ErrorVirtualTarUndefinedBehaviour, - ErrorTarGeneratorInvalidFileName, - ErrorTarGeneratorInvalidStat, - ErrorTarParser, - ErrorTarParserInvalidHeader, - ErrorTarParserBlockSize, - ErrorTarParserEndOfArchive, + ErrorVirtualTarGenerator, + ErrorVirtualTarGeneratorInvalidFileName, + ErrorVirtualTarGeneratorInvalidStat, + ErrorVirtualTarGeneratorBlockSize, + ErrorVirtualTarGeneratorEndOfArchive, + ErrorVirtualTarGeneratorInvalidState, + ErrorVirtualTarParser, + ErrorVirtualTarParserInvalidHeader, + ErrorVirtualTarParserBlockSize, + ErrorVirtualTarParserEndOfArchive, }; diff --git a/src/index.ts b/src/index.ts index 6fced85..5638928 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,6 @@ -// Starting project soon â„¢ +export { default as Generator } from './Generator'; +export { default as Parser } from './Parser'; +export * as contants from './constants'; +export * as errors from './errors'; +export * as utils from './utils'; +export * from './types'; diff --git a/src/types.ts b/src/types.ts index 52f51dd..124129a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,14 @@ -const enum EntryType { +enum EntryType { FILE = '0', DIRECTORY = '5', + EXTENDED = 'x', } -const enum HeaderOffset { +enum ExtendedHeaderKeywords { + FILE_PATH = 'path', +} + +enum HeaderOffset { FILE_NAME = 0, FILE_MODE = 100, OWNER_UID = 108, @@ -12,17 +17,17 @@ const enum HeaderOffset { FILE_MTIME = 136, CHECKSUM = 148, TYPE_FLAG = 156, - OWNER_NAME = 157, + LINK_NAME = 157, USTAR_NAME = 257, USTAR_VERSION = 263, OWNER_USERNAME = 265, OWNER_GROUPNAME = 297, DEVICE_MAJOR = 329, DEVICE_MINOR = 337, - FILE_NAME_EXTRA = 345, + FILE_NAME_PREFIX = 345, } -const enum HeaderSize { +enum HeaderSize { FILE_NAME = 100, FILE_MODE = 8, OWNER_UID = 8, @@ -31,14 +36,14 @@ const enum HeaderSize { FILE_MTIME = 12, CHECKSUM = 8, TYPE_FLAG = 1, - OWNER_NAME = 100, + LINK_NAME = 100, USTAR_NAME = 6, USTAR_VERSION = 2, OWNER_USERNAME = 32, OWNER_GROUPNAME = 32, DEVICE_MAJOR = 8, DEVICE_MINOR = 8, - FILE_NAME_EXTRA = 155, + FILE_NAME_PREFIX = 155, } type FileStat = { @@ -49,9 +54,9 @@ type FileStat = { mtime?: Date; }; -type HeaderToken = { +type TokenHeader = { type: 'header'; - fileType: 'file' | 'directory'; + fileType: 'file' | 'directory' | 'metadata'; filePath: string; fileMode: number; ownerUid: number; @@ -63,27 +68,42 @@ type HeaderToken = { ownerGroupName: string; }; -type DataToken = { +type TokenData = { type: 'data'; data: Uint8Array; }; -type EndToken = { +type TokenEnd = { type: 'end'; }; -const enum FileType { +enum FileType { FILE, DIRECTORY, } -const enum ParserState { +enum ParserState { READY, DATA, NULL, ENDED, } -export type { FileStat, HeaderToken, DataToken, EndToken }; +enum GeneratorState { + READY, + DATA, + NULL, + ENDED, +} -export { EntryType, HeaderOffset, HeaderSize, FileType, ParserState }; +export type { FileStat, TokenHeader, TokenData, TokenEnd }; + +export { + EntryType, + ExtendedHeaderKeywords, + HeaderOffset, + HeaderSize, + FileType, + ParserState, + GeneratorState, +}; diff --git a/src/utils.ts b/src/utils.ts index e0ecce9..ffb5689 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,10 @@ -import { HeaderOffset, HeaderSize } from './types'; +import { ExtendedHeaderKeywords, HeaderOffset, HeaderSize } from './types'; import * as errors from './errors'; import * as constants from './constants'; +// Text decoder for text parsing utilities +const decoder = new TextDecoder('ascii'); + function never(message: string): never { throw new errors.ErrorVirtualTarUndefinedBehaviour(message); } @@ -46,10 +49,6 @@ function dateToUnixTime(date: Date): number { return Math.round(date.getTime() / 1000); } -// PARSER - -const decoder = new TextDecoder('ascii'); - // Returns a view of the array with the given offset and length. Note that the // returned value is a view and not a copy, so any modifications to the data // will affect the original data. @@ -57,14 +56,14 @@ function extractBytes( array: Uint8Array, offset?: number, length?: number, - stopOnNull: boolean = false, + stoppingCharacter?: string, ): Uint8Array { const start = offset ?? 0; let end = length != null ? start + length : array.length; - if (stopOnNull) { + if (stoppingCharacter != null) { for (let i = start; i < end; i++) { - if (array[i] === 0) { + if (array[i] === stoppingCharacter.charCodeAt(0)) { end = i; break; } @@ -78,31 +77,49 @@ function extractString( array: Uint8Array, offset?: number, length?: number, + stoppingCharacter: string = '\0', ): string { - return decoder.decode(extractBytes(array, offset, length, true)); + return decoder.decode(extractBytes(array, offset, length, stoppingCharacter)); } function extractOctal( array: Uint8Array, offset?: number, length?: number, + stoppingCharacter?: string, ): number { - const value = extractString(array, offset, length); + const value = extractString(array, offset, length, stoppingCharacter); return value.length > 0 ? parseInt(value, 8) : 0; } +function extractDecimal( + array: Uint8Array, + offset?: number, + length?: number, + stoppingCharacter?: string, +): number { + const value = extractString(array, offset, length, stoppingCharacter); + return value.length > 0 ? parseInt(value, 10) : 0; +} + function parseFilePath(array: Uint8Array) { - const fileNameLower = extractString( + const fileNamePrefix = extractString( array, - HeaderOffset.FILE_NAME, - HeaderSize.FILE_NAME, + HeaderOffset.FILE_NAME_PREFIX, + HeaderSize.FILE_NAME_PREFIX, ); - const fileNameUpper = extractString( + + const fileNameSuffix = extractString( array, - HeaderOffset.FILE_NAME_EXTRA, - HeaderSize.FILE_NAME_EXTRA, + HeaderOffset.FILE_NAME, + HeaderSize.FILE_NAME, ); - return fileNameLower + fileNameUpper; + + if (fileNamePrefix !== '') { + return fileNamePrefix + fileNameSuffix; + } else { + return fileNameSuffix; + } } function isNullBlock(array: Uint8Array): boolean { @@ -133,6 +150,95 @@ function writeBytesToArray( return i; } +function encodeExtendedHeader( + data: Partial>, +): Uint8Array { + const encoder = new TextEncoder(); + let totalByteSize = 0; + const entries: Array = []; + + // For extended PAX headers, the format of metadata is as follows: + // =\n + // Where is the total length of the line including the key-value + // pair, the separator \n character, the space between the size and + // the line, and the size characters itself. Note \n is written using two + // characters but it is a single ASCII byte. + for (const [key, value] of Object.entries(data)) { + let size = key.length + value.length + 3; // Initial guess (' ', =, \n) + size += size.toString().length; // Adjust for size itself + + const entry = `${size} ${key}=${value}\n`; + entries.push(entry); + + // Update the total byte length of the header with the entry's size + totalByteSize += size; + } + + // The entries are encoded later to reduce memory allocation + const output = new Uint8Array(totalByteSize); + let offset = 0; + + for (const entry of entries) { + // Older browsers and runtimes might return written as undefined. That is + // not a concern for us. + const { written } = encoder.encodeInto(entry, output.subarray(offset)); + if (!written) throw new Error('TMP not written'); + offset += written; + } + + return output; +} + +function decodeExtendedHeader( + array: Uint8Array, +): Partial> { + const decoder = new TextDecoder(); + const data: Partial> = {}; + + // Track offset and remaining bytes in the array + let offset = 0; + let remainingBytes = array.byteLength; + + while (remainingBytes > 0) { + const size = extractDecimal(array, offset, undefined, ' '); + const fullLine = decoder.decode(array.subarray(offset, offset + size)); + + const sizeSeparatorIndex = fullLine.indexOf(' '); + if (sizeSeparatorIndex === -1) { + throw new Error('TMP invalid ennnntry'); + } + const line = fullLine.substring(sizeSeparatorIndex + 1); + + const entrySeparatorIndex = line.indexOf('='); + if (entrySeparatorIndex === -1) { + throw new Error('TMP invalid ennnntry'); + } + const key = line.substring(0, entrySeparatorIndex); + const _value = line.substring(entrySeparatorIndex + 1); + + if ( + !Object.values(ExtendedHeaderKeywords).includes( + key as ExtendedHeaderKeywords, + ) + ) { + throw new Error('TMP key doesnt exist'); + } + + // Remove the trailing newline + const value = _value.substring(0, _value.length - 1); + switch (key as ExtendedHeaderKeywords) { + case ExtendedHeaderKeywords.FILE_PATH: { + data[ExtendedHeaderKeywords.FILE_PATH] = value; + } + } + + offset += size; + remainingBytes -= size; + } + + return data; +} + export { never, pad, @@ -142,7 +248,10 @@ export { extractBytes, extractString, extractOctal, + extractDecimal, parseFilePath, isNullBlock, writeBytesToArray, + encodeExtendedHeader, + decodeExtendedHeader, }; diff --git a/tests/Generator.test.ts b/tests/Generator.test.ts index 9a90e90..883f9c9 100644 --- a/tests/Generator.test.ts +++ b/tests/Generator.test.ts @@ -1,47 +1,161 @@ +import fc from 'fast-check'; import { test } from '@fast-check/jest'; -import { generateHeader, generateNullChunk } from '@/Generator'; -import { EntryType } from '@/types'; +import Generator from '@/Generator'; +import { EntryType, GeneratorState } from '@/types'; import * as tarUtils from '@/utils'; -import { dirArb, fileArb, splitHeaderData } from './utils'; - -describe('archive generation', () => { - test.prop([fileArb()])('should generate a valid file header', (file) => { - // Generate and split the header - const header = generateHeader(file.path, EntryType.FILE, file.stat); - const { name, type, mode, uid, gid, size, mtime, format, version } = - splitHeaderData(header); - - // Compare the values to the expected ones - expect(name).toEqual(file.path); - expect(type).toEqual(EntryType.FILE); - expect(mode).toEqual(file.stat.mode); - expect(uid).toEqual(file.stat.uid); - expect(gid).toEqual(file.stat.gid); - expect(size).toEqual(file.stat.size); - expect(mtime).toEqual(tarUtils.dateToUnixTime(file.stat.mtime!)); - expect(format).toEqual('ustar'); - expect(version).toEqual('00'); +import * as tarErrors from '@/errors'; +import * as utils from './utils'; + +describe('generating archive', () => { + test.prop([utils.fileArb()])( + 'should generate a valid file header', + (file) => { + // Generate and split the header + const generator = new Generator(); + const header = generator.generateFile(file.path, file.stat); + const { name, type, mode, uid, gid, size, mtime, format, version } = + utils.splitHeaderData(header); + + // @ts-ignore: accessing protected member for state analysis + const state = generator.state; + if (file.stat.size === 0) expect(state).toEqual(GeneratorState.READY); + else expect(state).toEqual(GeneratorState.DATA); + + // Compare the values to the expected ones + expect(name).toEqual(file.path); + expect(type).toEqual(EntryType.FILE); + expect(mode).toEqual(file.stat.mode); + expect(uid).toEqual(file.stat.uid); + expect(gid).toEqual(file.stat.gid); + expect(size).toEqual(file.stat.size); + expect(mtime).toEqual(tarUtils.dateToUnixTime(file.stat.mtime!)); + expect(format).toEqual('ustar'); + expect(version).toEqual('00'); + }, + ); + + test.prop([utils.dirArb(0)])( + 'should generate a valid directory header', + (file) => { + // Generate and split the header + const generator = new Generator(); + const header = generator.generateDirectory(file.path, file.stat); + const { name, type, mode, uid, gid, size, mtime, format, version } = + utils.splitHeaderData(header); + + // @ts-ignore: accessing protected member for state analysis + const state = generator.state; + if (file.stat.size === 0) expect(state).toEqual(GeneratorState.READY); + + // Compare the values to the expected ones + expect(name).toEqual(file.path); + expect(type).toEqual(EntryType.DIRECTORY); + expect(mode).toEqual(file.stat.mode); + expect(uid).toEqual(file.stat.uid); + expect(gid).toEqual(file.stat.gid); + expect(size).toEqual(0); + expect(mtime).toEqual(tarUtils.dateToUnixTime(file.stat.mtime!)); + expect(format).toEqual('ustar'); + expect(version).toEqual('00'); + }, + ); + + test('should generate a valid null chunk', () => { + const generator = new Generator(); + const nullChunk = generator.generateEnd(); + expect(nullChunk.reduce((sum, byte) => (sum += byte))).toBe(0); + + // @ts-ignore: accessing protected member for state analysis + const state = generator.state; + expect(state).toEqual(GeneratorState.NULL); }); +}); - test.prop([dirArb(0)])('should generate a valid directory header', (file) => { - // Generate and split the header - const header = generateHeader(file.path, EntryType.DIRECTORY, file.stat); - const { name, type, mode, uid, gid, size, mtime, format, version } = - splitHeaderData(header); - - // Compare the values to the expected ones - expect(name).toEqual(file.path); - expect(type).toEqual(EntryType.DIRECTORY); - expect(mode).toEqual(file.stat.mode); - expect(uid).toEqual(file.stat.uid); - expect(gid).toEqual(file.stat.gid); - expect(size).toEqual(0); - expect(mtime).toEqual(tarUtils.dateToUnixTime(file.stat.mtime!)); - expect(format).toEqual('ustar'); - expect(version).toEqual('00'); +describe('generator state robustness', () => { + test.prop([utils.fileContentArb(0)], { numRuns: 1 })( + 'should fail writing data when header is expected', + (data) => { + const generator = new Generator(); + const encoder = new TextEncoder(); + expect(() => generator.generateData(encoder.encode(data))).toThrowError( + tarErrors.ErrorVirtualTarGeneratorInvalidState, + ); + }, + ); + + test.prop([utils.fileArb(), utils.fileArb()], { numRuns: 1 })( + 'should fail writing new header if previous file header has not sent any data', + (file1, file2) => { + fc.pre(file1.stat.size !== 0); + const generator = new Generator(); + + // Writing first file + generator.generateFile(file1.path, file1.stat); + // @ts-ignore: accessing protected member for state analysis + const state1 = generator.state; + expect(state1).toEqual(GeneratorState.DATA); + + // Writing second file + expect(() => generator.generateFile(file2.path, file2.stat)).toThrowError( + tarErrors.ErrorVirtualTarGeneratorInvalidState, + ); + }, + ); + + test.prop( + [fc.oneof(utils.fileContentArb(), utils.fileArb(), utils.dirArb(0))], + { numRuns: 10 }, + )('should fail writing data when attempting to end archive', (data) => { + const generator = new Generator(); + + // Parse the type of incoming data and smartly switch between different + // methods of writing to the generator. + const writeData = () => { + if (typeof data === 'string') { + // Data is file content + const encoder = new TextEncoder(); + generator.generateData(encoder.encode(data)); + } else if (data.type === EntryType.FILE) { + // Data is file + generator.generateFile(data.path, data.stat); + } else { + // Data is directory + generator.generateDirectory(data.path, data.stat); + } + }; + + generator.generateEnd(); + expect(writeData).toThrowError( + tarErrors.ErrorVirtualTarGeneratorInvalidState, + ); }); - test('should generate a valid null chunk', () => { - expect(generateNullChunk().reduce((sum, byte) => (sum += byte))).toBe(0); + test.prop( + [fc.oneof(utils.fileContentArb(), utils.fileArb(), utils.dirArb(0))], + { numRuns: 10 }, + )('should fail writing data after ending archive', (data) => { + const generator = new Generator(); + + // Parse the type of incoming data and smartly switch between different + // methods of writing to the generator. + const writeData = () => { + if (typeof data === 'string') { + // Data is file content + const encoder = new TextEncoder(); + generator.generateData(encoder.encode(data)); + } else if (data.type === EntryType.FILE) { + // Data is file + generator.generateFile(data.path, data.stat); + } else { + // Data is directory + generator.generateDirectory(data.path, data.stat); + } + }; + + generator.generateEnd(); + generator.generateEnd(); + expect(writeData).toThrowError( + tarErrors.ErrorVirtualTarGeneratorInvalidState, + ); }); }); diff --git a/tests/Parser.test.ts b/tests/Parser.test.ts index dd83f35..da62835 100644 --- a/tests/Parser.test.ts +++ b/tests/Parser.test.ts @@ -1,20 +1,19 @@ import { test } from '@fast-check/jest'; import fc from 'fast-check'; import Parser from '@/Parser'; -import { generateNullChunk } from '@/Generator'; import { HeaderOffset, ParserState } from '@/types'; import * as tarErrors from '@/errors'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; -import { tarHeaderArb } from './utils'; +import * as utils from './utils'; -describe('archive parsing', () => { - test.prop([tarHeaderArb])( +describe('parsing archive blocks', () => { + test.prop([utils.tarHeaderArb()])( 'should parse headers with correct state', - ({ header, stat }) => { + ({ headers, stat }) => { const { type, path, uid, gid } = stat; const parser = new Parser(); - const token = parser.write(header); + const token = parser.write(headers[0]); expect(token?.type).toEqual('header'); if (token?.type !== 'header') tarUtils.never('Token type'); @@ -24,44 +23,18 @@ describe('archive parsing', () => { switch (type) { case '0': - expect(state).toEqual(ParserState.DATA); + // If there is no data, then another header can be parsed immediately expect(token.fileType).toEqual('file'); + if (stat.size !== 0) expect(state).toEqual(ParserState.DATA); + else expect(state).toEqual(ParserState.READY); break; case '5': expect(state).toEqual(ParserState.READY); expect(token.fileType).toEqual('directory'); break; - default: - tarUtils.never('Invalid state'); - } - - expect(token.filePath).toEqual(path); - expect(token.ownerUid).toEqual(uid); - expect(token.ownerGid).toEqual(gid); - }, - ); - - test.prop([tarHeaderArb])( - 'should parse headers with correct state', - ({ header, stat }) => { - const { type, path, uid, gid } = stat; - const parser = new Parser(); - const token = parser.write(header); - - expect(token?.type).toEqual('header'); - if (token?.type !== 'header') tarUtils.never('Token type'); - - // @ts-ignore: accessing protected member for state analysis - const state = parser.state; - - switch (type) { - case '0': + case 'x': expect(state).toEqual(ParserState.DATA); - expect(token.fileType).toEqual('file'); - break; - case '5': - expect(state).toEqual(ParserState.READY); - expect(token.fileType).toEqual('directory'); + expect(token.fileType).toEqual('metadata'); break; default: tarUtils.never('Invalid state'); @@ -82,7 +55,7 @@ describe('archive parsing', () => { const parser = new Parser(); expect(() => parser.write(data)).toThrowError( - tarErrors.ErrorTarParserInvalidHeader, + tarErrors.ErrorVirtualTarParserInvalidHeader, ); }, ); @@ -96,20 +69,23 @@ describe('archive parsing', () => { const parser = new Parser(); expect(() => parser.write(data)).toThrowError( - tarErrors.ErrorTarParserBlockSize, + tarErrors.ErrorVirtualTarParserBlockSize, ); }, ); - test.prop([tarHeaderArb, fc.uint8Array({ minLength: 8, maxLength: 8 })], { - numRuns: 1, - })( + test.prop( + [utils.tarHeaderArb(), fc.uint8Array({ minLength: 8, maxLength: 8 })], + { + numRuns: 1, + }, + )( 'should fail to parse header with an invalid checksum', - ({ header }, checksum) => { - header.set(checksum, HeaderOffset.CHECKSUM); + ({ headers }, checksum) => { + headers[0].set(checksum, HeaderOffset.CHECKSUM); const parser = new Parser(); - expect(() => parser.write(header)).toThrowError( - tarErrors.ErrorTarParserInvalidHeader, + expect(() => parser.write(headers[0])).toThrowError( + tarErrors.ErrorVirtualTarParserInvalidHeader, ); }, ); @@ -118,42 +94,86 @@ describe('archive parsing', () => { test('should parse end of archive', () => { const parser = new Parser(); - const token1 = parser.write(generateNullChunk()); + const token1 = parser.write(new Uint8Array(tarConstants.BLOCK_SIZE)); expect(token1).toBeUndefined(); // @ts-ignore: accessing protected member for state analysis expect(parser.state).toEqual(ParserState.NULL); - const token2 = parser.write(generateNullChunk()); + const token2 = parser.write(new Uint8Array(tarConstants.BLOCK_SIZE)); expect(token2?.type).toEqual('end'); // @ts-ignore: accessing protected member for state analysis expect(parser.state).toEqual(ParserState.ENDED); }); - test.prop([tarHeaderArb], { numRuns: 1 })( + test.prop([utils.tarHeaderArb()], { numRuns: 1 })( 'should fail if end of archive is malformed', - ({ header }) => { + ({ headers }) => { const parser = new Parser(); - const token1 = parser.write(generateNullChunk()); + const token1 = parser.write(new Uint8Array(tarConstants.BLOCK_SIZE)); expect(token1).toBeUndefined(); - expect(() => parser.write(header)).toThrowError( - tarErrors.ErrorTarParserEndOfArchive, + expect(() => parser.write(headers[0])).toThrowError( + tarErrors.ErrorVirtualTarParserEndOfArchive, ); }, ); - test.prop([tarHeaderArb], { numRuns: 1 })( + test.prop([utils.tarHeaderArb()], { numRuns: 1 })( 'should fail if data is written after parser ending', - ({ header }) => { + ({ headers }) => { const parser = new Parser(); // @ts-ignore: updating parser state for testing parser.state = ParserState.ENDED; - expect(() => parser.write(header)).toThrowError( - tarErrors.ErrorTarParserEndOfArchive, + expect(() => parser.write(headers[0])).toThrowError( + tarErrors.ErrorVirtualTarParserEndOfArchive, ); }, ); }); }); + +describe('parsing extended metadata', () => { + test.prop([utils.tarHeaderArb({ minLength: 256, maxLength: 512 })], { + numRuns: 1, + })('should create pax header with long paths', ({ headers }) => { + const parser = new Parser(); + const token = parser.write(headers[0]); + expect(token?.type).toEqual('header'); + // @ts-ignore: accessing protected member for state analysis + expect(parser.state).toEqual(ParserState.DATA); + }); + + test.prop([utils.tarHeaderArb({ minLength: 256, maxLength: 512 })], { + numRuns: 1, + })('should retrieve full file path from pax header', ({ headers, stat }) => { + // Get the header size + const parser = new Parser(); + const paxHeader = parser.write(headers[0]); + if (paxHeader == null || paxHeader.type !== 'header') { + throw new Error('Invalid state'); + } + const size = paxHeader.fileSize; + + // Concatenate all the data into a single array + const data = new Uint8Array(size); + let offset = 0; + for (const header of headers.slice(1, -1)) { + const paxData = parser.write(header); + if (paxData == null || paxData.type !== 'data') { + throw new Error('Invalid state'); + } + data.set(paxData.data, offset); + offset += tarConstants.BLOCK_SIZE; + } + + // Parse the data into a record + const parsedHeader = tarUtils.decodeExtendedHeader(data); + expect(parsedHeader.path).toEqual(stat.path); + + // The actual path in the header is ignored if the PAX header contains + // metadata for the file path. Ignoring this is dependant on the user + // instead of on the parser. + }); +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index bd332d7..5cc38af 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,9 +1,9 @@ import type { FileType, DirectoryType } from './utils'; import path from 'path'; import { test } from '@fast-check/jest'; -import { generateHeader, generateNullChunk } from '@/Generator'; -import { EntryType } from '@/types'; +import Generator from '@/Generator'; import Parser from '@/Parser'; +import { EntryType, ExtendedHeaderKeywords } from '@/types'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; import * as utils from './utils'; @@ -12,32 +12,54 @@ describe('integration testing', () => { test.prop([utils.virtualFsArb])( 'should archive and unarchive a virtual file system', (vfs) => { + const generator = new Generator(); const blocks: Array = []; const generateArchive = (entry: FileType | DirectoryType) => { + if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { + // Push the extended metadata header + const data = tarUtils.encodeExtendedHeader({ + [ExtendedHeaderKeywords.FILE_PATH]: entry.path, + }); + blocks.push(generator.generateExtended(data.byteLength)); + + // Push the content + for ( + let offset = 0; + offset < data.byteLength; + offset += tarConstants.BLOCK_SIZE + ) { + blocks.push( + generator.generateData( + data.subarray(offset, offset + tarConstants.BLOCK_SIZE), + ), + ); + } + } + + const filePath = entry.path.length <= 255 ? entry.path : ''; + switch (entry.type) { case EntryType.FILE: { // Generate the header entry = entry as FileType; - blocks.push(generateHeader(entry.path, entry.type, entry.stat)); + blocks.push(generator.generateFile(filePath, entry.stat)); // Generate the data const encoder = new TextEncoder(); let content = entry.content; - do { + while (content.length > 0) { const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); - blocks.push( - encoder.encode(dataChunk.padEnd(tarConstants.BLOCK_SIZE, '\0')), - ); + blocks.push(generator.generateData(encoder.encode(dataChunk))); content = content.slice(tarConstants.BLOCK_SIZE); - } while (content.length > 0); + } break; } case EntryType.DIRECTORY: { // Generate the header entry = entry as DirectoryType; - blocks.push(generateHeader(entry.path, entry.type, entry.stat)); + blocks.push(generator.generateDirectory(filePath, entry.stat)); // Perform the same operation on all children for (const file of entry.children) { @@ -54,8 +76,8 @@ describe('integration testing', () => { for (const entry of vfs) { generateArchive(entry); } - blocks.push(generateNullChunk()); - blocks.push(generateNullChunk()); + blocks.push(generator.generateEnd()); + blocks.push(generator.generateEnd()); // The tar archive should be inside the blocks array now. Each block is // a single chunk aligned to 512-byte. Now we can parse it and check if @@ -66,6 +88,8 @@ describe('integration testing', () => { const reconstructedVfs: Array = []; const pathStack: Map = new Map(); let currentEntry: FileType; + let extendedData: Uint8Array | undefined; + let dataOffset = 0; for (const chunk of blocks) { const token = parser.write(chunk); @@ -73,37 +97,62 @@ describe('integration testing', () => { switch (token.type) { case 'header': { - let parsedEntry: FileType | DirectoryType; - - if (token.fileType === 'file') { - parsedEntry = { - type: EntryType.FILE, - path: token.filePath, - content: '', - stat: { - mode: token.fileMode, - uid: token.ownerUid, - gid: token.ownerGid, - size: token.fileSize, - mtime: token.fileMtime, - }, - }; - } else { - parsedEntry = { - type: EntryType.DIRECTORY, - path: token.filePath, - children: [], - stat: { - mode: token.fileMode, - uid: token.ownerUid, - gid: token.ownerGid, - size: token.fileSize, - mtime: token.fileMtime, - }, - }; + let parsedEntry: FileType | DirectoryType | undefined; + let extendedMetadata: + | Partial> + | undefined; + if (extendedData != null) { + extendedMetadata = tarUtils.decodeExtendedHeader(extendedData); } - const parentPath = path.dirname(token.filePath); + const fullPath = extendedMetadata?.path?.trim() + ? extendedMetadata.path + : token.filePath; + + switch (token.fileType) { + case 'file': { + parsedEntry = { + type: EntryType.FILE, + path: fullPath, + content: '', + stat: { + mode: token.fileMode, + uid: token.ownerUid, + gid: token.ownerGid, + size: token.fileSize, + mtime: token.fileMtime, + }, + }; + break; + } + case 'directory': { + parsedEntry = { + type: EntryType.DIRECTORY, + path: fullPath, + children: [], + stat: { + mode: token.fileMode, + uid: token.ownerUid, + gid: token.ownerGid, + size: token.fileSize, + mtime: token.fileMtime, + }, + }; + break; + } + case 'metadata': { + extendedData = new Uint8Array(token.fileSize); + extendedMetadata = {}; + break; + } + default: + throw new Error('Invalid state'); + } + // If parsed entry has not been reassigned, then it was a metadata + // header. Continue to fetch extended metadata. + if (parsedEntry == null) continue; + + const parentPath = path.dirname(fullPath); // If this entry is a directory, then it is pushed to the root of // the reconstructed virtual file system and into a map at the same @@ -121,19 +170,29 @@ describe('integration testing', () => { } if (parsedEntry.type === EntryType.DIRECTORY) { - pathStack.set(token.filePath, parsedEntry); + pathStack.set(fullPath, parsedEntry); } else { // Type narrowing doesn't work well with manually specified types currentEntry = parsedEntry as FileType; } + // If we were using the extended metadata for this header, reset it + // for the next header. + extendedData = undefined; + dataOffset = 0; + break; } case 'data': { - // It is guaranteed that in a valid tar file, a data block will - // always come after a header block for a file. - currentEntry!['content'] += decoder.decode(token.data); + if (extendedData == null) { + // It is guaranteed that in a valid tar file, a data block will + // always come after a header block for a file. + currentEntry!['content'] += decoder.decode(token.data); + } else { + extendedData.set(token.data, dataOffset); + dataOffset += token.data.byteLength; + } break; } } diff --git a/tests/utils.ts b/tests/utils.ts index 7338175..1f8702e 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,24 +1,30 @@ import type { FileStat } from '@/types'; import fc from 'fast-check'; +import { ExtendedHeaderKeywords, HeaderSize } from '@/types'; import { HeaderOffset } from '@/types'; import { EntryType } from '@/types'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; type FileType = { - type: EntryType; + type: EntryType.FILE; path: string; stat: FileStat; content: string; }; type DirectoryType = { - type: EntryType; + type: EntryType.DIRECTORY; path: string; stat: FileStat; children: Array; }; +type MetadataType = { + type: EntryType.EXTENDED; + size: number; +}; + function splitHeaderData(data: Uint8Array) { return { name: tarUtils.parseFilePath(data), @@ -33,12 +39,16 @@ function splitHeaderData(data: Uint8Array) { }; } -const filenameArb = fc - .string({ minLength: 1, maxLength: 32 }) - .filter((name) => !name.includes('/') && name !== '.' && name !== '..') - .noShrink(); +const filenameArb = ( + { minLength, maxLength } = { minLength: 1, maxLength: 512 }, +) => + fc + .string({ minLength, maxLength }) + .filter((name) => !name.includes('/') && name !== '.' && name !== '..') + .noShrink(); -const fileContentArb = fc.string({ minLength: 0, maxLength: 4096 }).noShrink(); +const fileContentArb = (maxLength: number = 4096) => + fc.string({ minLength: 0, maxLength }).noShrink(); // Dates are stored in 11 digits of octal number. This can store from 0 to // 0o77777777777 or 8589934591 seconds. This comes up to 2242-03-16T12:56:31. @@ -62,12 +72,15 @@ const statDataArb = ( }) .noShrink(); -const fileArb = (parentPath: string = ''): fc.Arbitrary => +const fileArb = ( + parentPath: string = '', + dataLength: number = 4096, +): fc.Arbitrary => fc .record({ - type: fc.constant(EntryType.FILE), - path: filenameArb.map((name) => `${parentPath}/${name}`), - content: fileContentArb, + type: fc.constant(EntryType.FILE), + path: filenameArb().map((name) => `${parentPath}/${name}`), + content: fileContentArb(dataLength), }) .chain((file) => statDataArb(EntryType.FILE, file.content).map((stat) => ({ @@ -83,8 +96,8 @@ const dirArb = ( ): fc.Arbitrary => fc .record({ - type: fc.constant(EntryType.DIRECTORY), - path: filenameArb.map((name) => `${parentPath}/${name}`), + type: fc.constant(EntryType.DIRECTORY), + path: filenameArb().map((name) => `${parentPath}/${name}`), }) .chain((dir) => fc @@ -115,59 +128,158 @@ const virtualFsArb = fc }) .noShrink(); -const tarHeaderArb = fc - .record({ - path: filenameArb, - uid: fc.nat(65535), - gid: fc.nat(65535), - size: fc.nat(65536), - typeflag: fc.constantFrom('0', '5'), - }) - .map(({ path, uid, gid, size, typeflag }) => { - const header = new Uint8Array(tarConstants.BLOCK_SIZE); - const type = typeflag as '0' | '5'; - const encoder = new TextEncoder(); - - if (type === '5') size = 0; - - // Fill header fields - header.set(encoder.encode(path), HeaderOffset.FILE_NAME); - header.set(encoder.encode('0000777'), HeaderOffset.FILE_MODE); - header.set( - encoder.encode(uid.toString(8).padStart(7, '0')), - HeaderOffset.OWNER_UID, - ); - header.set( - encoder.encode(gid.toString(8).padStart(7, '0')), - HeaderOffset.OWNER_GID, - ); - header.set( - encoder.encode(size.toString(8).padStart(11, '0') + '\0'), - HeaderOffset.FILE_SIZE, - ); - header.set(encoder.encode(' '), HeaderOffset.CHECKSUM); - header.set(encoder.encode(type), HeaderOffset.TYPE_FLAG); - header.set( - encoder.encode(tarConstants.USTAR_NAME), - HeaderOffset.USTAR_NAME, - ); - header.set( - encoder.encode(tarConstants.USTAR_VERSION), - HeaderOffset.USTAR_VERSION, - ); - - // Compute and set checksum - const checksum = header.reduce((sum, byte) => sum + byte, 0); - header.set( - encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '), - HeaderOffset.CHECKSUM, - ); - - return { header, stat: { type, size, path, uid, gid } }; - }) - .noShrink(); +const tarHeaderArb = ( + { minLength, maxLength } = { + minLength: 1, + maxLength: 512, + }, +) => + fc + .record({ + path: filenameArb({ minLength, maxLength }), + uid: fc.nat(65535), + gid: fc.nat(65535), + size: fc.nat(65536), + typeflag: fc.constantFrom('0', '5'), + }) + .map(({ path, uid, gid, size, typeflag }) => { + let headers: Array = []; + headers.push(new Uint8Array(tarConstants.BLOCK_SIZE)); + const type = typeflag as '0' | '5' | 'x'; + const encoder = new TextEncoder(); + + if (type === '5') size = 0; + + // If the + if (path.length > tarConstants.STANDARD_PATH_SIZE) { + // Set the metadata for the header + const extendedHeader = new Uint8Array(tarConstants.BLOCK_SIZE); + const extendedData = tarUtils.encodeExtendedHeader({ + [ExtendedHeaderKeywords.FILE_PATH]: path, + }); + + // Set the size of the content, the type flag, the ustar values, and the + // checksum. + extendedHeader.set( + encoder.encode( + tarUtils.pad( + extendedData.byteLength, + HeaderSize.FILE_SIZE, + '0', + '\0', + ), + ), + HeaderOffset.FILE_SIZE, + ); + + extendedHeader.set( + encoder.encode(tarConstants.USTAR_NAME), + HeaderOffset.USTAR_NAME, + ); + extendedHeader.set( + encoder.encode(tarConstants.USTAR_VERSION), + HeaderOffset.USTAR_VERSION, + ); + extendedHeader.set( + encoder.encode(EntryType.EXTENDED), + HeaderOffset.TYPE_FLAG, + ); + + const checksum = tarUtils.calculateChecksum(extendedHeader); + extendedHeader.set( + encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '), + HeaderOffset.CHECKSUM, + ); + + // Split out the data to 512-byte chunks + const data: Array = []; + let offset = 0; + while (offset < extendedData.length) { + const block = new Uint8Array(tarConstants.BLOCK_SIZE); + block.set( + extendedData.slice(offset, offset + tarConstants.BLOCK_SIZE), + ); + data.push(block); + offset += tarConstants.BLOCK_SIZE; + } + + headers = [extendedHeader, ...data, ...headers]; + } else { + if (path.length < HeaderSize.FILE_NAME) { + headers + .at(-1)! + .set( + encoder.encode( + tarUtils.splitFileName(path, 0, HeaderSize.FILE_NAME), + ), + HeaderOffset.FILE_NAME, + ); + } else { + const fileSuffix = tarUtils.splitFileName( + path, + 0, + HeaderSize.FILE_NAME, + ); + const filePrefix = tarUtils.splitFileName( + path, + HeaderSize.FILE_NAME, + HeaderSize.FILE_NAME_PREFIX, + ); + headers + .at(-1)! + .set(encoder.encode(fileSuffix), HeaderOffset.FILE_NAME); + headers + .at(-1)! + .set(encoder.encode(filePrefix), HeaderOffset.FILE_NAME_PREFIX); + } + } + + // Fill normal header fields + headers.at(-1)!.set(encoder.encode('0000777'), HeaderOffset.FILE_MODE); + headers + .at(-1)! + .set( + encoder.encode(uid.toString(8).padStart(7, '0')), + HeaderOffset.OWNER_UID, + ); + headers + .at(-1)! + .set( + encoder.encode(gid.toString(8).padStart(7, '0')), + HeaderOffset.OWNER_GID, + ); + headers + .at(-1)! + .set( + encoder.encode(size.toString(8).padStart(11, '0') + '\0'), + HeaderOffset.FILE_SIZE, + ); + headers.at(-1)!.set(encoder.encode(' '), HeaderOffset.CHECKSUM); + headers.at(-1)!.set(encoder.encode(type), HeaderOffset.TYPE_FLAG); + headers + .at(-1)! + .set(encoder.encode(tarConstants.USTAR_NAME), HeaderOffset.USTAR_NAME); + headers + .at(-1)! + .set( + encoder.encode(tarConstants.USTAR_VERSION), + HeaderOffset.USTAR_VERSION, + ); + + // Compute and set checksum + const checksum = headers.at(-1)!.reduce((sum, byte) => sum + byte, 0); + headers + .at(-1)! + .set( + encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '), + HeaderOffset.CHECKSUM, + ); + + return { headers, stat: { type, size, path, uid, gid } }; + }) + .noShrink(); -const tarDataArb = tarHeaderArb +const tarDataArb = tarHeaderArb() .chain((header) => fc .record({ @@ -178,7 +290,7 @@ const tarDataArb = tarHeaderArb }), }) .map(({ header, data }) => { - const { header: headerBlock, stat } = header; + const { headers, stat } = header; const encoder = new TextEncoder(); const encodedData = encoder.encode(data); @@ -196,7 +308,7 @@ const tarDataArb = tarHeaderArb } return { - header: headerBlock, + headers: headers, data: data, encodedData: dataBlock, type: stat.type, @@ -205,7 +317,7 @@ const tarDataArb = tarHeaderArb ) .noShrink(); -export type { FileType, DirectoryType }; +export type { FileType, DirectoryType, MetadataType }; export { splitHeaderData, filenameArb, From 26043c32d3787db61ab019b45da81e93dea76025 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Fri, 7 Mar 2025 18:04:26 +1100 Subject: [PATCH 12/19] test: added testing against node-tar for correctness --- package-lock.json | 474 +++++++++++++++++++++++++++++++++++++- package.json | 2 + src/Generator.ts | 277 ++++++++-------------- src/Parser.ts | 243 +++++++++---------- src/VirtualTar.ts | 0 src/index.ts | 4 +- src/types.ts | 14 +- src/utils.ts | 198 ++++++++++++++-- tests/Generator.test.ts | 181 ++++++++++++++- tests/Parser.test.ts | 235 ++++++++++++++++--- tests/index.test.ts | 225 ++---------------- tests/integration.test.ts | 204 ++++++++++++++++ tests/types.ts | 22 ++ tests/utils.ts | 331 -------------------------- tests/utils/fastcheck.ts | 306 ++++++++++++++++++++++++ tests/utils/index.ts | 2 + tests/utils/utils.ts | 30 +++ 17 files changed, 1847 insertions(+), 901 deletions(-) create mode 100644 src/VirtualTar.ts create mode 100644 tests/integration.test.ts create mode 100644 tests/types.ts delete mode 100644 tests/utils.ts create mode 100644 tests/utils/fastcheck.ts create mode 100644 tests/utils/index.ts create mode 100644 tests/utils/utils.ts diff --git a/package-lock.json b/package-lock.json index f92439e..cf4c6bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@swc/jest": "^0.2.26", "@types/jest": "^28.1.3", "@types/node": "^18.15.0", + "@types/tar": "^6.1.13", "@typescript-eslint/eslint-plugin": "^5.45.1", "@typescript-eslint/parser": "^5.45.1", "eslint": "^8.15.0", @@ -35,6 +36,7 @@ "pkg": "^5.8.1", "prettier": "^2.6.2", "shx": "^0.3.4", + "tar": "^7.4.3", "ts-jest": "^28.0.5", "ts-node": "^10.9.1", "tsconfig-paths": "^3.9.0", @@ -722,6 +724,132 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1289,6 +1417,17 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1740,6 +1879,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "minipass": "^4.0.0" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -3028,6 +3178,13 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.97", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.97.tgz", @@ -3854,6 +4011,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/from2": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", @@ -4990,6 +5177,22 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jest": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", @@ -6056,6 +6259,103 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minizlib/node_modules/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, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minizlib/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -6443,6 +6743,13 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6512,6 +6819,40 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7687,6 +8028,22 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -7759,6 +8116,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -7832,6 +8203,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-fs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", @@ -7845,7 +8234,22 @@ "tar-stream": "^2.1.4" } }, - "node_modules/tar-stream": { + "node_modules/tar-fs/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tar-fs/node_modules/tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", @@ -7862,19 +8266,50 @@ "node": ">=6" } }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "bin": { + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { - "node": ">= 6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" } }, "node_modules/terminal-link": { @@ -8633,6 +9068,25 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 417c3d2..a7ba8a3 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "@swc/jest": "^0.2.26", "@types/jest": "^28.1.3", "@types/node": "^18.15.0", + "@types/tar": "^6.1.13", "@typescript-eslint/eslint-plugin": "^5.45.1", "@typescript-eslint/parser": "^5.45.1", "eslint": "^8.15.0", @@ -61,6 +62,7 @@ "pkg": "^5.8.1", "prettier": "^2.6.2", "shx": "^0.3.4", + "tar": "^7.4.3", "ts-jest": "^28.0.5", "ts-node": "^10.9.1", "tsconfig-paths": "^3.9.0", diff --git a/src/Generator.ts b/src/Generator.ts index e8ce581..597ffac 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -1,155 +1,11 @@ -import type { FileStat } from './types'; -import { GeneratorState, EntryType, HeaderSize, HeaderOffset } from './types'; +import type { FileType, FileStat } from './types'; +import { GeneratorState, EntryType, HeaderSize } from './types'; import * as errors from './errors'; import * as utils from './utils'; import * as constants from './constants'; -function generateHeader(filePath: string, type: EntryType, stat: FileStat) { - if (filePath.length > 255) { - throw new errors.ErrorVirtualTarGeneratorInvalidFileName( - 'The file name must shorter than 255 characters', - ); - } - - // The time can be undefined, which would be referring to epoch 0. - const time = utils.dateToUnixTime(stat.mtime ?? new Date()); - - const header = new Uint8Array(constants.BLOCK_SIZE); - - // If the length of the file path is less than 100 bytes, then we write it to - // the file name. Otherwise, we write it into the file name prefix and append - // file name to it. - if (filePath.length < HeaderSize.FILE_NAME) { - utils.writeBytesToArray( - header, - utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME), - HeaderOffset.FILE_NAME, - HeaderSize.FILE_NAME, - ); - } else { - utils.writeBytesToArray( - header, - utils.splitFileName( - filePath, - HeaderSize.FILE_NAME, - HeaderSize.FILE_NAME_PREFIX, - ), - HeaderOffset.FILE_NAME, - HeaderSize.FILE_NAME, - ); - utils.writeBytesToArray( - header, - utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME), - HeaderOffset.FILE_NAME_PREFIX, - HeaderSize.FILE_NAME_PREFIX, - ); - } - - // The file permissions, or the mode, is stored in the next chunk. This is - // stored in an octal number format. - utils.writeBytesToArray( - header, - utils.pad(stat.mode ?? '', HeaderSize.FILE_MODE, '0', '\0'), - HeaderOffset.FILE_MODE, - HeaderSize.FILE_MODE, - ); - - // The owner UID is stored in this chunk - utils.writeBytesToArray( - header, - utils.pad(stat.uid ?? '', HeaderSize.OWNER_UID, '0', '\0'), - HeaderOffset.OWNER_UID, - HeaderSize.OWNER_UID, - ); - - // The owner GID is stored in this chunk - utils.writeBytesToArray( - header, - utils.pad(stat.gid ?? '', HeaderSize.OWNER_GID, '0', '\0'), - HeaderOffset.OWNER_GID, - HeaderSize.OWNER_GID, - ); - - // The file size is stored in this chunk. The file size must be zero for - // directories, and it must be set for files. - utils.writeBytesToArray( - header, - utils.pad(stat.size ?? '', HeaderSize.FILE_SIZE, '0', '\0'), - HeaderOffset.FILE_SIZE, - HeaderSize.FILE_SIZE, - ); - - // The file mtime is stored in this chunk. As the mtime is not modified when - // extracting a TAR file, the mtime can be preserved while still getting - // deterministic archives. - utils.writeBytesToArray( - header, - utils.pad(time, HeaderSize.FILE_MTIME, '0', '\0'), - HeaderOffset.FILE_MTIME, - HeaderSize.FILE_MTIME, - ); - - // The checksum is calculated as the sum of all bytes in the header. It is - // left blank for later calculation. - - // The type of file is written as a single byte in the header. - utils.writeBytesToArray( - header, - type, - HeaderOffset.TYPE_FLAG, - HeaderSize.TYPE_FLAG, - ); - - // Link name will be null, as regular stat-ing cannot extract that - // information. - - // This value is the USTAR magic string which makes this file appear as - // a tar file. Without this, the file cannot be parsed and extracted. - utils.writeBytesToArray( - header, - constants.USTAR_NAME, - HeaderOffset.USTAR_NAME, - HeaderSize.USTAR_NAME, - ); - - // This chunk stores the version of USTAR, which is '00' in this case. - utils.writeBytesToArray( - header, - constants.USTAR_VERSION, - HeaderOffset.USTAR_VERSION, - HeaderSize.USTAR_VERSION, - ); - - // Owner user name will be null, as regular stat-ing cannot extract this - // information. - - // Owner group name will be null, as regular stat-ing cannot extract this - // information. - - // Device major will be null, as this specific to linux kernel knowing what - // drivers to use for executing certain files, and is irrelevant here. - - // Device minor will be null, as this specific to linux kernel knowing what - // drivers to use for executing certain files, and is irrelevant here. - - // Updating with the new checksum - const checksum = utils.calculateChecksum(header); - - // Note the extra space in the padding for the checksum value. It is - // intentionally placed there. The padding for checksum is ASCII spaces - // instead of null, which is why it is used like this here. - utils.writeBytesToArray( - header, - utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0'), - HeaderOffset.CHECKSUM, - HeaderSize.CHECKSUM, - ); - - return header; -} - /** - * The TAR headers follow this structure + * The TAR headers follow this structure: * Start Size Description * ------------------------------ * 0 100 File name (first 100 bytes) @@ -171,83 +27,154 @@ function generateHeader(filePath: string, type: EntryType, stat: FileStat) { * 500 12 '\0' (unused) * * Note that all numbers are in stringified octal format. + * + * The following data will be left blank (null): + * - Link name + * - Owner user name + * - Owner group name + * - Device major + * - Device minor + * + * This is because this implementation does not interact with linked files. + * Owner user name and group name cannot be extracted via regular stat-ing, + * so it is left blank. In virtual situations, this field won't be useful + * anyways. The device major and minor are specific to linux kernel, which + * is not relevant to this virtual tar implementation. This is the reason + * these fields have been left blank. */ class Generator { - protected state: GeneratorState = GeneratorState.READY; + protected state: GeneratorState = GeneratorState.HEADER; protected remainingBytes = 0; + protected generateHeader(filePath: string, type: FileType, stat: FileStat): Uint8Array { + if (filePath.length > 255) { + throw new errors.ErrorVirtualTarGeneratorInvalidFileName( + 'The file name must shorter than 255 characters', + ); + } + + if (stat?.size != null && stat?.size > 0o7777777) { + throw new errors.ErrorVirtualTarGeneratorInvalidStat( + 'The file size must be smaller than 7.99 GiB (8,589,934,591 bytes)', + ); + } + + if ( + stat?.username != null && + stat?.username.length > HeaderSize.OWNER_USERNAME + ) { + throw new errors.ErrorVirtualTarGeneratorInvalidStat( + `The username must not exceed ${HeaderSize.OWNER_USERNAME} bytes`, + ); + } + + if ( + stat?.groupname != null && + stat?.groupname.length > HeaderSize.OWNER_GROUPNAME + ) { + throw new errors.ErrorVirtualTarGeneratorInvalidStat( + `The groupname must not exceed ${HeaderSize.OWNER_GROUPNAME} bytes`, + ); + } + + const header = new Uint8Array(constants.BLOCK_SIZE); + + // Every directory in tar must have a trailing slash + if (type === 'directory') { + filePath = filePath.endsWith('/') ? filePath : filePath + '/'; + } + + utils.writeUstarMagic(header); + utils.writeFileType(header, type); + utils.writeFilePath(header, filePath); + utils.writeFileMode(header, stat.mode); + utils.writeOwnerUid(header, stat.uid); + utils.writeOwnerGid(header, stat.gid); + utils.writeOwnerUserName(header, stat.username); + utils.writeOwnerGroupName(header, stat.groupname); + utils.writeFileSize(header, stat.size); + utils.writeFileMtime(header, stat.mtime); + + // The checksum can only be calculated once the entire header has been + // written. This is why the checksum is calculated and written at the end. + utils.writeChecksum(header, utils.calculateChecksum(header)); + + return header; + } + generateFile(filePath: string, stat: FileStat): Uint8Array { - if (this.state === GeneratorState.READY) { + if (this.state === GeneratorState.HEADER) { // Make sure the size is valid if (stat.size == null) { throw new errors.ErrorVirtualTarGeneratorInvalidStat( - 'Files should have valid file sizes', + 'Files must have valid file sizes', ); } - const generatedBlock = generateHeader(filePath, EntryType.FILE, stat); + const generatedBlock = this.generateHeader(filePath, 'file', stat); // If no data is in the file, then there is no need of a data block. It // will remain as READY. - if (stat.size !== 0) this.state = GeneratorState.DATA; - this.remainingBytes = stat.size; + if (stat.size !== 0) { + this.state = GeneratorState.DATA; + this.remainingBytes = stat.size; + } return generatedBlock; } throw new errors.ErrorVirtualTarGeneratorInvalidState( - `Expected state ${GeneratorState[GeneratorState.READY]} but got ${ + `Expected state ${GeneratorState[GeneratorState.HEADER]} but got ${ GeneratorState[this.state] }`, ); } - generateDirectory(filePath: string, stat: FileStat): Uint8Array { - if (this.state === GeneratorState.READY) { + generateDirectory(filePath: string, stat?: FileStat): Uint8Array { + if (this.state === GeneratorState.HEADER) { + // The size is zero for directories. Override this value in the stat if + // set. const directoryStat: FileStat = { + ...stat, size: 0, - mode: stat.mode, - mtime: stat.mtime, - uid: stat.uid, - gid: stat.gid, }; - return generateHeader(filePath, EntryType.DIRECTORY, directoryStat); + return this.generateHeader(filePath, 'directory', directoryStat); } throw new errors.ErrorVirtualTarGeneratorInvalidState( - `Expected state ${GeneratorState[GeneratorState.READY]} but got ${ + `Expected state ${GeneratorState[GeneratorState.HEADER]} but got ${ GeneratorState[this.state] }`, ); } generateExtended(size: number): Uint8Array { - if (this.state === GeneratorState.READY) { + if (this.state === GeneratorState.HEADER) { this.state = GeneratorState.DATA; this.remainingBytes = size; - return generateHeader('', EntryType.EXTENDED, { size }); + return this.generateHeader('./PaxHeader', 'extended', { size }); } throw new errors.ErrorVirtualTarGeneratorInvalidState( - `Expected state ${GeneratorState[GeneratorState.READY]} but got ${ + `Expected state ${GeneratorState[GeneratorState.HEADER]} but got ${ GeneratorState[this.state] }`, ); } generateData(data: Uint8Array): Uint8Array { - if (data.byteLength > constants.BLOCK_SIZE) { - throw new errors.ErrorVirtualTarGeneratorBlockSize( - `Expected data to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`, - ); - } - if (this.state === GeneratorState.DATA) { + if (data.byteLength > constants.BLOCK_SIZE) { + throw new errors.ErrorVirtualTarGeneratorBlockSize( + `Expected data to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`, + ); + } + if (this.remainingBytes >= constants.BLOCK_SIZE) { this.remainingBytes -= constants.BLOCK_SIZE; - if (this.remainingBytes === 0) this.state = GeneratorState.READY; + if (this.remainingBytes === 0) this.state = GeneratorState.HEADER; return data; } else { // Update state this.remainingBytes = 0; - this.state = GeneratorState.READY; + this.state = GeneratorState.HEADER; // Pad the remaining data with nulls const paddedData = new Uint8Array(constants.BLOCK_SIZE); @@ -266,9 +193,9 @@ class Generator { // Creates a single null block. A null block is a block filled with all zeros. // This is needed to end the archive, as two of these blocks mark the end of // archive. - generateEnd() { + generateEnd(): Uint8Array { switch (this.state) { - case GeneratorState.READY: + case GeneratorState.HEADER: this.state = GeneratorState.NULL; break; case GeneratorState.NULL: diff --git a/src/Parser.ts b/src/Parser.ts index 27ad969..f182093 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -5,133 +5,136 @@ import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; -function parseHeader(array: Uint8Array): TokenHeader { - // Validate header by checking checksum and magic string - const headerChecksum = utils.extractOctal( - array, - HeaderOffset.CHECKSUM, - HeaderSize.CHECKSUM, - ); - const calculatedChecksum = utils.calculateChecksum(array); - - if (headerChecksum !== calculatedChecksum) { - throw new errors.ErrorVirtualTarParserInvalidHeader( - `Expected checksum to be ${calculatedChecksum} but received ${headerChecksum}`, - ); - } +class Parser { + protected state: ParserState = ParserState.HEADER; + protected remainingBytes = 0; - const ustarMagic = utils.extractString( - array, - HeaderOffset.USTAR_NAME, - HeaderSize.USTAR_NAME, - ); - if (ustarMagic !== constants.USTAR_NAME) { - throw new errors.ErrorVirtualTarParserInvalidHeader( - `Expected ustar magic to be '${constants.USTAR_NAME}', got '${ustarMagic}'`, + protected parseHeader(array: Uint8Array): TokenHeader { + // Validate header by checking checksum and magic string + const headerChecksum = utils.extractOctal( + array, + HeaderOffset.CHECKSUM, + HeaderSize.CHECKSUM, ); - } + const calculatedChecksum = utils.calculateChecksum(array); + + if (headerChecksum !== calculatedChecksum) { + throw new errors.ErrorVirtualTarParserInvalidHeader( + `Expected checksum to be ${calculatedChecksum} but received ${headerChecksum}`, + ); + } - const ustarVersion = utils.extractString( - array, - HeaderOffset.USTAR_VERSION, - HeaderSize.USTAR_VERSION, - ); - if (ustarVersion !== constants.USTAR_VERSION) { - throw new errors.ErrorVirtualTarParserInvalidHeader( - `Expected ustar version to be '${constants.USTAR_VERSION}', got '${ustarVersion}'`, + const ustarMagic = utils.extractString( + array, + HeaderOffset.USTAR_NAME, + HeaderSize.USTAR_NAME, ); - } + if (ustarMagic !== constants.USTAR_NAME) { + throw new errors.ErrorVirtualTarParserInvalidHeader( + `Expected ustar magic to be '${constants.USTAR_NAME}', got '${ustarMagic}'`, + ); + } - // Extract the relevant metadata from the header - const filePath = utils.parseFilePath(array); - const fileSize = utils.extractOctal( - array, - HeaderOffset.FILE_SIZE, - HeaderSize.FILE_SIZE, - ); - const fileMtime = new Date( - utils.extractOctal(array, HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME) * - 1000, - ); - const fileMode = utils.extractOctal( - array, - HeaderOffset.FILE_MODE, - HeaderSize.FILE_MODE, - ); - const ownerGid = utils.extractOctal( - array, - HeaderOffset.OWNER_GID, - HeaderSize.OWNER_GID, - ); - const ownerUid = utils.extractOctal( - array, - HeaderOffset.OWNER_UID, - HeaderSize.OWNER_UID, - ); - const ownerName = utils.extractString( - array, - HeaderOffset.LINK_NAME, - HeaderSize.LINK_NAME, - ); - const ownerGroupName = utils.extractString( - array, - HeaderOffset.OWNER_GROUPNAME, - HeaderSize.OWNER_GROUPNAME, - ); - const ownerUserName = utils.extractString( - array, - HeaderOffset.OWNER_USERNAME, - HeaderSize.OWNER_USERNAME, - ); - let fileType: 'file' | 'directory' | 'metadata'; - const type = utils.extractString( - array, - HeaderOffset.TYPE_FLAG, - HeaderSize.TYPE_FLAG, - ); - switch (type) { - case EntryType.FILE: - fileType = 'file'; - break; - case EntryType.DIRECTORY: - fileType = 'directory'; - break; - case EntryType.EXTENDED: - fileType = 'metadata'; - break; - default: + const ustarVersion = utils.extractString( + array, + HeaderOffset.USTAR_VERSION, + HeaderSize.USTAR_VERSION, + ); + if (ustarVersion !== constants.USTAR_VERSION) { throw new errors.ErrorVirtualTarParserInvalidHeader( - `Got invalid file type ${type}`, + `Expected ustar version to be '${constants.USTAR_VERSION}', got '${ustarVersion}'`, ); - } + } - return { - type: 'header', - filePath, - fileType, - fileMode, - fileMtime, - fileSize, - ownerGid, - ownerUid, - ownerName, - ownerUserName, - ownerGroupName, - }; -} + // Extract the relevant metadata from the header + const filePath = utils.decodeFilePath(array); + const fileSize = utils.extractOctal( + array, + HeaderOffset.FILE_SIZE, + HeaderSize.FILE_SIZE, + ); + const fileMtime = new Date( + utils.extractOctal( + array, + HeaderOffset.FILE_MTIME, + HeaderSize.FILE_MTIME, + ) * 1000, + ); + const fileMode = utils.extractOctal( + array, + HeaderOffset.FILE_MODE, + HeaderSize.FILE_MODE, + ); + const ownerGid = utils.extractOctal( + array, + HeaderOffset.OWNER_GID, + HeaderSize.OWNER_GID, + ); + const ownerUid = utils.extractOctal( + array, + HeaderOffset.OWNER_UID, + HeaderSize.OWNER_UID, + ); + const ownerName = utils.extractString( + array, + HeaderOffset.LINK_NAME, + HeaderSize.LINK_NAME, + ); + const ownerGroupName = utils.extractString( + array, + HeaderOffset.OWNER_GROUPNAME, + HeaderSize.OWNER_GROUPNAME, + ); + const ownerUserName = utils.extractString( + array, + HeaderOffset.OWNER_USERNAME, + HeaderSize.OWNER_USERNAME, + ); + let fileType: 'file' | 'directory' | 'metadata'; + const type = utils.extractString( + array, + HeaderOffset.TYPE_FLAG, + HeaderSize.TYPE_FLAG, + ); + switch (type) { + case EntryType.FILE: + fileType = 'file'; + break; + case EntryType.DIRECTORY: + fileType = 'directory'; + break; + case EntryType.EXTENDED: + fileType = 'metadata'; + break; + default: + throw new errors.ErrorVirtualTarParserInvalidHeader( + `Got invalid file type ${type}`, + ); + } -function parseData(array: Uint8Array, remainingBytes: number): TokenData { - if (remainingBytes > 512) { - return { type: 'data', data: utils.extractBytes(array) }; - } else { - const data = utils.extractBytes(array, 0, remainingBytes); - return { type: 'data', data: data }; + return { + type: 'header', + filePath, + fileType, + fileMode, + fileMtime, + fileSize, + ownerGid, + ownerUid, + ownerName, + ownerUserName, + ownerGroupName, + }; } -} -class Parser { - protected state: ParserState = ParserState.READY; - protected remainingBytes = 0; + protected parseData(array: Uint8Array, remainingBytes: number): TokenData { + if (remainingBytes > 512) { + return { type: 'data', data: utils.extractBytes(array) }; + } else { + const data = utils.extractBytes(array, 0, remainingBytes); + return { type: 'data', data: data }; + } + } write(data: Uint8Array) { if (data.byteLength !== constants.BLOCK_SIZE) { @@ -147,7 +150,7 @@ class Parser { ); } - case ParserState.READY: { + case ParserState.HEADER: { // Check if we need to parse the end-of-archive marker if (utils.isNullBlock(data)) { this.state = ParserState.NULL; @@ -156,7 +159,7 @@ class Parser { // Set relevant state if the header corresponds to a file. If the file // size 0, then no data blocks will follow the header. - const headerToken = parseHeader(data); + const headerToken = this.parseHeader(data); if (headerToken.fileType === 'file') { if (headerToken.fileSize !== 0) { this.state = ParserState.DATA; @@ -172,9 +175,9 @@ class Parser { } case ParserState.DATA: { - const parsedData = parseData(data, this.remainingBytes); + const parsedData = this.parseData(data, this.remainingBytes); this.remainingBytes -= constants.BLOCK_SIZE; - if (this.remainingBytes <= 0) this.state = ParserState.READY; + if (this.remainingBytes <= 0) this.state = ParserState.HEADER; return parsedData; } diff --git a/src/VirtualTar.ts b/src/VirtualTar.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/index.ts b/src/index.ts index 5638928..aea9a17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ export { default as Generator } from './Generator'; export { default as Parser } from './Parser'; -export * as contants from './constants'; +export * as constants from './constants'; export * as errors from './errors'; export * as utils from './utils'; -export * from './types'; +export * as types from './types'; diff --git a/src/types.ts b/src/types.ts index 124129a..f5d63f3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +type FileType = 'file' | 'directory' | 'extended'; + enum EntryType { FILE = '0', DIRECTORY = '5', @@ -52,6 +54,8 @@ type FileStat = { gid?: number; size?: number; mtime?: Date; + username?: string; + groupname?: string; }; type TokenHeader = { @@ -77,33 +81,33 @@ type TokenEnd = { type: 'end'; }; -enum FileType { +enum _FileType { FILE, DIRECTORY, } enum ParserState { - READY, + HEADER, DATA, NULL, ENDED, } enum GeneratorState { - READY, + HEADER, DATA, NULL, ENDED, } -export type { FileStat, TokenHeader, TokenData, TokenEnd }; +export type { FileType, FileStat, TokenHeader, TokenData, TokenEnd }; export { EntryType, ExtendedHeaderKeywords, HeaderOffset, HeaderSize, - FileType, + _FileType, ParserState, GeneratorState, }; diff --git a/src/utils.ts b/src/utils.ts index ffb5689..5e4a9a0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,9 @@ -import { ExtendedHeaderKeywords, HeaderOffset, HeaderSize } from './types'; +import { + EntryType, + ExtendedHeaderKeywords, + HeaderOffset, + HeaderSize, +} from './types'; import * as errors from './errors'; import * as constants from './constants'; @@ -36,15 +41,6 @@ function calculateChecksum(array: Uint8Array): number { }); } -function splitFileName( - fileName: string, - offset: number, - size: number, - padding: string = '\0', -) { - return fileName.slice(offset, offset + size).padEnd(size, padding); -} - function dateToUnixTime(date: Date): number { return Math.round(date.getTime() / 1000); } @@ -102,7 +98,7 @@ function extractDecimal( return value.length > 0 ? parseInt(value, 10) : 0; } -function parseFilePath(array: Uint8Array) { +function decodeFilePath(array: Uint8Array): string { const fileNamePrefix = extractString( array, HeaderOffset.FILE_NAME_PREFIX, @@ -150,6 +146,172 @@ function writeBytesToArray( return i; } +function writeFilePath(header: Uint8Array, filePath: string): void { + // return fileName.slice(offset, offset + size).padEnd(size, padding); + // If the length of the file path is less than 100 bytes, then we write it to + // the file name. Otherwise, we write it into the file name prefix and append + // file name to it. + + const filePathSuffix = filePath + .slice(0, HeaderSize.FILE_NAME) + .padEnd(HeaderSize.FILE_NAME, '\0'); + + if (filePath.length < HeaderSize.FILE_NAME) { + writeBytesToArray( + header, + filePathSuffix, + HeaderOffset.FILE_NAME, + HeaderSize.FILE_NAME, + ); + } else { + const filePathPrefix = filePath + .slice( + HeaderSize.FILE_NAME, + HeaderSize.FILE_NAME + HeaderSize.FILE_NAME_PREFIX, + ) + .padEnd(HeaderSize.FILE_NAME_PREFIX, '\0'); + + writeBytesToArray( + header, + filePathPrefix, + HeaderOffset.FILE_NAME, + HeaderSize.FILE_NAME, + ); + writeBytesToArray( + header, + filePathSuffix, + HeaderOffset.FILE_NAME_PREFIX, + HeaderSize.FILE_NAME_PREFIX, + ); + } +} + +function writeFileMode(header: Uint8Array, mode?: number): void { + // The file permissions, or the mode, is stored in the next chunk. This is + // stored in an octal number format. + writeBytesToArray( + header, + pad(mode ?? '', HeaderSize.FILE_MODE, '0', '\0'), + HeaderOffset.FILE_MODE, + HeaderSize.FILE_MODE, + ); +} + +function writeOwnerUid(header: Uint8Array, uid?: number): void { + writeBytesToArray( + header, + pad(uid ?? '', HeaderSize.OWNER_UID, '0', '\0'), + HeaderOffset.OWNER_UID, + HeaderSize.OWNER_UID, + ); +} + +function writeOwnerGid(header: Uint8Array, gid?: number): void { + writeBytesToArray( + header, + pad(gid ?? '', HeaderSize.OWNER_GID, '0', '\0'), + HeaderOffset.OWNER_GID, + HeaderSize.OWNER_GID, + ); +} + +function writeFileSize(header: Uint8Array, size?: number): void { + // The file size is stored in this chunk. The file size must be zero for + // directories, and it must be set for files. + writeBytesToArray( + header, + pad(size ?? '', HeaderSize.FILE_SIZE, '0', '\0'), + HeaderOffset.FILE_SIZE, + HeaderSize.FILE_SIZE, + ); +} + +function writeFileMtime(header: Uint8Array, mtime?: Date): void { + // The file mtime is stored in this chunk. As the mtime is not modified when + // extracting a TAR file, the mtime can be preserved while still getting + // deterministic archives. + const date = mtime != null ? dateToUnixTime(mtime) : ''; + writeBytesToArray( + header, + pad(date, HeaderSize.FILE_MTIME, '0', '\0'), + HeaderOffset.FILE_MTIME, + HeaderSize.FILE_MTIME, + ); +} + +function writeFileType( + header: Uint8Array, + type: 'file' | 'directory' | 'extended', +): void { + // The file mtime is stored in this chunk. As the mtime is not modified when + // extracting a TAR file, the mtime can be preserved while still getting + // deterministic archives. + let entryType: EntryType; + switch (type) { + case 'file': + entryType = EntryType.FILE; + break; + case 'directory': + entryType = EntryType.DIRECTORY; + break; + case 'extended': + entryType = EntryType.EXTENDED; + break; + } + writeBytesToArray( + header, + pad(entryType, HeaderSize.TYPE_FLAG, '0', '\0'), + HeaderOffset.TYPE_FLAG, + HeaderSize.TYPE_FLAG, + ); +} + +function writeUstarMagic(header: Uint8Array): void { + // This value is the USTAR magic string which makes this file appear as + // a tar file. Without this, the file cannot be parsed and extracted. + writeBytesToArray( + header, + constants.USTAR_NAME, + HeaderOffset.USTAR_NAME, + HeaderSize.USTAR_NAME, + ); + + // This chunk stores the version of USTAR, which is '00' in this case. + writeBytesToArray( + header, + constants.USTAR_VERSION, + HeaderOffset.USTAR_VERSION, + HeaderSize.USTAR_VERSION, + ); +} + +function writeChecksum(header: Uint8Array, checksum: number): void { + writeBytesToArray( + header, + pad(checksum, HeaderSize.CHECKSUM, '0', '\0'), + HeaderOffset.CHECKSUM, + HeaderSize.CHECKSUM, + ); +} + +function writeOwnerUserName(header: Uint8Array, username?: string): void { + writeBytesToArray( + header, + pad(username ?? '', HeaderSize.OWNER_USERNAME, '0', '\0'), + HeaderOffset.OWNER_USERNAME, + HeaderSize.OWNER_USERNAME, + ); +} + +function writeOwnerGroupName(header: Uint8Array, groupname?: string): void { + writeBytesToArray( + header, + pad(groupname ?? '', HeaderSize.OWNER_GROUPNAME, '0', '\0'), + HeaderOffset.OWNER_GROUPNAME, + HeaderSize.OWNER_GROUPNAME, + ); +} + function encodeExtendedHeader( data: Partial>, ): Uint8Array { @@ -243,15 +405,25 @@ export { never, pad, calculateChecksum, - splitFileName, dateToUnixTime, extractBytes, extractString, extractOctal, extractDecimal, - parseFilePath, + decodeFilePath, isNullBlock, writeBytesToArray, + writeFilePath, + writeFileMode, + writeOwnerUid, + writeOwnerGid, + writeFileSize, + writeFileMtime, + writeFileType, + writeUstarMagic, + writeChecksum, + writeOwnerUserName, + writeOwnerGroupName, encodeExtendedHeader, decodeExtendedHeader, }; diff --git a/tests/Generator.test.ts b/tests/Generator.test.ts index 883f9c9..913215d 100644 --- a/tests/Generator.test.ts +++ b/tests/Generator.test.ts @@ -1,9 +1,15 @@ +import type { VirtualFile, VirtualDirectory } from './types'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; import fc from 'fast-check'; import { test } from '@fast-check/jest'; -import Generator from '@/Generator'; import { EntryType, GeneratorState } from '@/types'; -import * as tarUtils from '@/utils'; +import Generator from '@/Generator'; +import * as tar from 'tar'; +import * as tarConstants from '@/constants'; import * as tarErrors from '@/errors'; +import * as tarUtils from '@/utils'; import * as utils from './utils'; describe('generating archive', () => { @@ -18,7 +24,7 @@ describe('generating archive', () => { // @ts-ignore: accessing protected member for state analysis const state = generator.state; - if (file.stat.size === 0) expect(state).toEqual(GeneratorState.READY); + if (file.stat.size === 0) expect(state).toEqual(GeneratorState.HEADER); else expect(state).toEqual(GeneratorState.DATA); // Compare the values to the expected ones @@ -45,7 +51,7 @@ describe('generating archive', () => { // @ts-ignore: accessing protected member for state analysis const state = generator.state; - if (file.stat.size === 0) expect(state).toEqual(GeneratorState.READY); + if (file.stat.size === 0) expect(state).toEqual(GeneratorState.HEADER); // Compare the values to the expected ones expect(name).toEqual(file.path); @@ -112,14 +118,11 @@ describe('generator state robustness', () => { // methods of writing to the generator. const writeData = () => { if (typeof data === 'string') { - // Data is file content const encoder = new TextEncoder(); generator.generateData(encoder.encode(data)); - } else if (data.type === EntryType.FILE) { - // Data is file + } else if (data.type === 'file') { generator.generateFile(data.path, data.stat); } else { - // Data is directory generator.generateDirectory(data.path, data.stat); } }; @@ -140,14 +143,11 @@ describe('generator state robustness', () => { // methods of writing to the generator. const writeData = () => { if (typeof data === 'string') { - // Data is file content const encoder = new TextEncoder(); generator.generateData(encoder.encode(data)); - } else if (data.type === EntryType.FILE) { - // Data is file + } else if (data.type === 'file') { generator.generateFile(data.path, data.stat); } else { - // Data is directory generator.generateDirectory(data.path, data.stat); } }; @@ -159,3 +159,160 @@ describe('generator state robustness', () => { ); }); }); + +describe('testing against tar', () => { + test.skip.prop([utils.virtualFsArb])('should match output of tar', async (vfs) => { + // Create a temp directory to use for node-tar + const tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'js-virtualtar-test-'), + ); + + try { + // Create the archive using the Generator + const generator = new Generator(); + const blocks: Array = []; + + const trimmedVfs = structuredClone(vfs); + const trimStat = (entry: VirtualFile | VirtualDirectory) => { + entry.stat = { size: entry.stat.size, mode: entry.stat.mode }; + if (entry.type === 'directory') { + for (const child of entry.children) { + trimStat(child); + } + } + }; + for (const entry of trimmedVfs) trimStat(entry); + + const generateEntry = (entry: VirtualFile | VirtualDirectory) => { + // Due to operating system restrictions, node-tar cannot properly + // reproduce all the metadata at the time of extracting files. The + // mtime defaults to extraction time, the uid and gid is fixed to the + // user who the program is running under. As fast-check is used to + // generate this data, this will always differ than the observed stat, + // so these fields will be ignored for this test. + entry.stat = { + mode: entry.stat.mode, + size: entry.stat.size, + }; + + if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { + // Push the extended metadata header + const data = tarUtils.encodeExtendedHeader({ path: entry.path }); + blocks.push(generator.generateExtended(data.byteLength)); + + // Push the content block + for ( + let offset = 0; + offset < data.byteLength; + offset += tarConstants.BLOCK_SIZE + ) { + blocks.push( + generator.generateData( + data.subarray(offset, offset + tarConstants.BLOCK_SIZE), + ), + ); + } + } + + const filePath = + entry.path.length <= tarConstants.STANDARD_PATH_SIZE + ? entry.path + : ''; + + switch (entry.type) { + case 'file': { + // Generate the header + entry = entry as VirtualFile; + blocks.push(generator.generateFile(filePath, entry.stat)); + + // Generate the data + const encoder = new TextEncoder(); + let content = entry.content; + while (content.length > 0) { + const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); + blocks.push(generator.generateData(encoder.encode(dataChunk))); + content = content.slice(tarConstants.BLOCK_SIZE); + } + break; + } + + case 'directory': { + // Generate the header + entry = entry as VirtualDirectory; + blocks.push(generator.generateDirectory(filePath, entry.stat)); + + // Perform the same operation on all children + for (const file of entry.children) { + generateEntry(file); + } + break; + } + + default: + throw new Error('Invalid type'); + } + }; + + for (const entry of vfs) generateEntry(entry); + blocks.push(generator.generateEnd()); + blocks.push(generator.generateEnd()); + + // Write the archive to fs + const archivePath = path.join(tempDir, 'archive.tar'); + const tarFile = await fs.promises.open(archivePath, 'w+'); + for (const block of blocks) await tarFile.write(block); + await tarFile.close(); + + const vfsPath = path.join(tempDir, 'vfs'); + await fs.promises.mkdir(vfsPath, { recursive: true }); + await tar.extract({ + file: archivePath, + cwd: vfsPath, + preservePaths: true, + }); + + // Reconstruct the vfs and compare the contents to actual vfs + const traverse = async (currentPath: string) => { + const entries = await fs.promises.readdir(currentPath, { + withFileTypes: true, + }); + const vfsEntries: Array = []; + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + const relativePath = path.relative(vfsPath, fullPath); + const stats = await fs.promises.stat(fullPath); + + if (entry.isDirectory()) { + // Sometimes, the size of a directory on disk might not be 0 bytes + // due to the storage of additional metadata. This is different from + // the way tar stores directories, so the size is being manually set. + const entry: VirtualDirectory = { + type: 'directory', + path: relativePath + '/', + children: await traverse(fullPath), + stat: { size: 0, mode: stats.mode }, + }; + vfsEntries.push(entry); + } else { + const content = await fs.promises.readFile(fullPath); + const entry: VirtualFile = { + type: 'file', + path: relativePath, + content: content.toString(), + stat: { size: stats.size, mode: stats.mode }, + }; + vfsEntries.push(entry); + } + } + + return vfsEntries; + }; + + const reconstructedVfs = await traverse(vfsPath); + expect(utils.deepSort(reconstructedVfs)).toEqual(utils.deepSort(vfs)); + } finally { + await fs.promises.rm(tempDir, { force: true, recursive: true }); + } + }); +}); diff --git a/tests/Parser.test.ts b/tests/Parser.test.ts index da62835..5aee96c 100644 --- a/tests/Parser.test.ts +++ b/tests/Parser.test.ts @@ -1,17 +1,23 @@ +import type { VirtualFile, VirtualDirectory } from './types'; +import type { ExtendedHeaderKeywords } from '@/types'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; import { test } from '@fast-check/jest'; import fc from 'fast-check'; +import * as tar from 'tar'; import Parser from '@/Parser'; -import { HeaderOffset, ParserState } from '@/types'; +import { EntryType, HeaderOffset, ParserState } from '@/types'; import * as tarErrors from '@/errors'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; import * as utils from './utils'; describe('parsing archive blocks', () => { - test.prop([utils.tarHeaderArb()])( + test.prop([utils.tarEntryArb()])( 'should parse headers with correct state', - ({ headers, stat }) => { - const { type, path, uid, gid } = stat; + ({ headers, data }) => { + const { type, path, stat } = data; const parser = new Parser(); const token = parser.write(headers[0]); @@ -22,27 +28,29 @@ describe('parsing archive blocks', () => { const state = parser.state; switch (type) { - case '0': - // If there is no data, then another header can be parsed immediately - expect(token.fileType).toEqual('file'); - if (stat.size !== 0) expect(state).toEqual(ParserState.DATA); - else expect(state).toEqual(ParserState.READY); + case 'file': + // The file can have an extended header or a regular header + if (data.path.length > tarConstants.STANDARD_PATH_SIZE) { + expect(token.fileType).toEqual('metadata'); + expect(state).toEqual(ParserState.DATA); + } else { + // If there is no data, then another header can be parsed immediately + expect(token.fileType).toEqual('file'); + if (stat.size !== 0) expect(state).toEqual(ParserState.DATA); + else expect(state).toEqual(ParserState.HEADER); + } break; - case '5': - expect(state).toEqual(ParserState.READY); + case 'directory': + expect(state).toEqual(ParserState.HEADER); expect(token.fileType).toEqual('directory'); break; - case 'x': - expect(state).toEqual(ParserState.DATA); - expect(token.fileType).toEqual('metadata'); - break; default: tarUtils.never('Invalid state'); } expect(token.filePath).toEqual(path); - expect(token.ownerUid).toEqual(uid); - expect(token.ownerGid).toEqual(gid); + expect(token.ownerUid).toEqual(stat.uid); + expect(token.ownerGid).toEqual(stat.gid); }, ); @@ -75,7 +83,7 @@ describe('parsing archive blocks', () => { ); test.prop( - [utils.tarHeaderArb(), fc.uint8Array({ minLength: 8, maxLength: 8 })], + [utils.tarEntryArb(), fc.uint8Array({ minLength: 8, maxLength: 8 })], { numRuns: 1, }, @@ -105,7 +113,7 @@ describe('parsing archive blocks', () => { expect(parser.state).toEqual(ParserState.ENDED); }); - test.prop([utils.tarHeaderArb()], { numRuns: 1 })( + test.prop([utils.tarEntryArb()], { numRuns: 1 })( 'should fail if end of archive is malformed', ({ headers }) => { const parser = new Parser(); @@ -119,7 +127,7 @@ describe('parsing archive blocks', () => { }, ); - test.prop([utils.tarHeaderArb()], { numRuns: 1 })( + test.prop([utils.tarEntryArb()], { numRuns: 1 })( 'should fail if data is written after parser ending', ({ headers }) => { const parser = new Parser(); @@ -135,7 +143,7 @@ describe('parsing archive blocks', () => { }); describe('parsing extended metadata', () => { - test.prop([utils.tarHeaderArb({ minLength: 256, maxLength: 512 })], { + test.prop([utils.tarEntryArb({ minFilePathSize: 256, maxFilePathSize: 512 })], { numRuns: 1, })('should create pax header with long paths', ({ headers }) => { const parser = new Parser(); @@ -145,9 +153,9 @@ describe('parsing extended metadata', () => { expect(parser.state).toEqual(ParserState.DATA); }); - test.prop([utils.tarHeaderArb({ minLength: 256, maxLength: 512 })], { + test.prop([utils.tarEntryArb({ minFilePathSize: 256, maxFilePathSize: 512 })], { numRuns: 1, - })('should retrieve full file path from pax header', ({ headers, stat }) => { + })('should retrieve full file path from pax header', ({ headers, data }) => { // Get the header size const parser = new Parser(); const paxHeader = parser.write(headers[0]); @@ -157,23 +165,190 @@ describe('parsing extended metadata', () => { const size = paxHeader.fileSize; // Concatenate all the data into a single array - const data = new Uint8Array(size); + const numDataBlocks = Math.ceil(size / tarConstants.BLOCK_SIZE); + const dataBlock = new Uint8Array(size); let offset = 0; - for (const header of headers.slice(1, -1)) { + for (const header of headers.slice(1, 1 + numDataBlocks)) { const paxData = parser.write(header); if (paxData == null || paxData.type !== 'data') { - throw new Error('Invalid state'); + throw new Error(`Invalid state: ${paxData?.type}`); } - data.set(paxData.data, offset); - offset += tarConstants.BLOCK_SIZE; + dataBlock.set(paxData.data, offset); + offset += paxData.data.byteLength; } // Parse the data into a record - const parsedHeader = tarUtils.decodeExtendedHeader(data); - expect(parsedHeader.path).toEqual(stat.path); + const parsedHeader = tarUtils.decodeExtendedHeader(dataBlock); + expect(parsedHeader.path).toEqual(data.path); // The actual path in the header is ignored if the PAX header contains // metadata for the file path. Ignoring this is dependant on the user // instead of on the parser. }); }); + +describe('testing against tar', () => { + test.skip.prop([utils.virtualFsArb], { numRuns: 1 })( + 'should match output of tar', + async (vfs) => { + // Create a temp directory to use for node-tar + const tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'js-virtualtar-test-'), + ); + + try { + const vfsPath = path.join(tempDir, 'vfs'); + await fs.promises.mkdir(vfsPath); + + // Write the vfs to disk for tar to archive + const writeVfs = async (entry: VirtualFile | VirtualDirectory) => { + // Due to operating system restrictions, all the generated metadata + // cannot be written to disk. The mode, mtime, uid, and gid is + // determined by external variables, and as such, will not be tested. + delete entry.stat.mode; + delete entry.stat.mtime; + delete entry.stat.uid; + delete entry.stat.gid; + + const entryPath = path.join(vfsPath, entry.path); + if (entry.type === 'directory') { + await fs.promises.mkdir(entryPath); + for (const file of entry.children) await writeVfs(file); + } else { + await fs.promises.writeFile(entryPath, entry.content); + } + }; + for (const entry of vfs) await writeVfs(entry); + + // Use tar to archive the file + const archivePath = path.join(tempDir, 'archive.tar'); + const entries = await fs.promises.readdir(vfsPath); + await new Promise((resolve) => { + tar + .create( + { + cwd: vfsPath, + preservePaths: true, + }, + entries, + ) + .pipe(fs.createWriteStream(archivePath)) + .on('close', resolve); + }); + + const chunks: Uint8Array[] = []; + const stream = fs.createReadStream(archivePath, { highWaterMark: 512 }); + for await (const chunk of stream) { + chunks.push(new Uint8Array(chunk.buffer)); + } + + const parser = new Parser(); + const decoder = new TextDecoder(); + const reconstructedVfs: Array = []; + const pathStack: Map = new Map(); + let currentEntry: VirtualFile; + let extendedData: Uint8Array | undefined; + let dataOffset = 0; + + for (const chunk of chunks) { + const token = parser.write(chunk); + if (token == null) continue; + + switch (token.type) { + case 'header': { + let parsedEntry: VirtualFile | VirtualDirectory | undefined; + let extendedMetadata: + | Partial> + | undefined; + if (extendedData != null) { + extendedMetadata = tarUtils.decodeExtendedHeader(extendedData); + } + + const fullPath = extendedMetadata?.path?.trim() + ? extendedMetadata.path + : token.filePath; + + switch (token.fileType) { + case 'file': { + parsedEntry = { + type: 'file', + path: fullPath, + content: '', + stat: { size: token.fileSize }, + }; + break; + } + case 'directory': { + parsedEntry = { + type: 'directory', + path: fullPath, + children: [], + stat: { size: token.fileSize }, + }; + break; + } + case 'metadata': { + extendedData = new Uint8Array(token.fileSize); + extendedMetadata = {}; + break; + } + default: + throw new Error('Invalid state'); + } + // If parsed entry has not been reassigned, then it was a metadata + // header. Continue to fetch extended metadata. + if (parsedEntry == null) continue; + + const parentPath = path.dirname(fullPath); + + // If this entry is a directory, then it is pushed to the root of + // the reconstructed virtual file system and into a map at the same + // time. This allows us to add new children to the directory by + // looking up the path in a map rather than modifying the value in + // the reconstructed file system. + + if (parentPath === '.') { + reconstructedVfs.push(parsedEntry); + } else { + // It is guaranteed that in a valid tar file, the parent will + // always exist. + const parent: VirtualDirectory = pathStack.get(parentPath + '/'); + parent.children.push(parsedEntry); + } + + if (parsedEntry.type === 'directory') { + pathStack.set(fullPath, parsedEntry); + } else { + // Type narrowing doesn't work well with manually specified types + currentEntry = parsedEntry as VirtualFile; + } + + // If we were using the extended metadata for this header, reset it + // for the next header. + extendedData = undefined; + dataOffset = 0; + + break; + } + + case 'data': { + if (extendedData == null) { + // It is guaranteed that in a valid tar file, a data block will + // always come after a header block for a file. + currentEntry!['content'] += decoder.decode(token.data); + } else { + extendedData.set(token.data, dataOffset); + dataOffset += token.data.byteLength; + } + break; + } + } + } + + expect(utils.deepSort(reconstructedVfs)).toEqual(utils.deepSort(vfs)); + } finally { + await fs.promises.rm(tempDir, { force: true, recursive: true }); + } + }, + ); +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index 5cc38af..09425b5 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,204 +1,23 @@ -import type { FileType, DirectoryType } from './utils'; -import path from 'path'; -import { test } from '@fast-check/jest'; -import Generator from '@/Generator'; -import Parser from '@/Parser'; -import { EntryType, ExtendedHeaderKeywords } from '@/types'; -import * as tarUtils from '@/utils'; -import * as tarConstants from '@/constants'; -import * as utils from './utils'; - -describe('integration testing', () => { - test.prop([utils.virtualFsArb])( - 'should archive and unarchive a virtual file system', - (vfs) => { - const generator = new Generator(); - const blocks: Array = []; - - const generateArchive = (entry: FileType | DirectoryType) => { - if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { - // Push the extended metadata header - const data = tarUtils.encodeExtendedHeader({ - [ExtendedHeaderKeywords.FILE_PATH]: entry.path, - }); - blocks.push(generator.generateExtended(data.byteLength)); - - // Push the content - for ( - let offset = 0; - offset < data.byteLength; - offset += tarConstants.BLOCK_SIZE - ) { - blocks.push( - generator.generateData( - data.subarray(offset, offset + tarConstants.BLOCK_SIZE), - ), - ); - } - } - - const filePath = entry.path.length <= 255 ? entry.path : ''; - - switch (entry.type) { - case EntryType.FILE: { - // Generate the header - entry = entry as FileType; - blocks.push(generator.generateFile(filePath, entry.stat)); - - // Generate the data - const encoder = new TextEncoder(); - let content = entry.content; - while (content.length > 0) { - const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); - blocks.push(generator.generateData(encoder.encode(dataChunk))); - content = content.slice(tarConstants.BLOCK_SIZE); - } - break; - } - - case EntryType.DIRECTORY: { - // Generate the header - entry = entry as DirectoryType; - blocks.push(generator.generateDirectory(filePath, entry.stat)); - - // Perform the same operation on all children - for (const file of entry.children) { - generateArchive(file); - } - break; - } - - default: - tarUtils.never('Invalid type'); - } - }; - - for (const entry of vfs) { - generateArchive(entry); - } - blocks.push(generator.generateEnd()); - blocks.push(generator.generateEnd()); - - // The tar archive should be inside the blocks array now. Each block is - // a single chunk aligned to 512-byte. Now we can parse it and check if - // the parsed virtual file system matches the input. - - const parser = new Parser(); - const decoder = new TextDecoder(); - const reconstructedVfs: Array = []; - const pathStack: Map = new Map(); - let currentEntry: FileType; - let extendedData: Uint8Array | undefined; - let dataOffset = 0; - - for (const chunk of blocks) { - const token = parser.write(chunk); - if (token == null) continue; - - switch (token.type) { - case 'header': { - let parsedEntry: FileType | DirectoryType | undefined; - let extendedMetadata: - | Partial> - | undefined; - if (extendedData != null) { - extendedMetadata = tarUtils.decodeExtendedHeader(extendedData); - } - - const fullPath = extendedMetadata?.path?.trim() - ? extendedMetadata.path - : token.filePath; - - switch (token.fileType) { - case 'file': { - parsedEntry = { - type: EntryType.FILE, - path: fullPath, - content: '', - stat: { - mode: token.fileMode, - uid: token.ownerUid, - gid: token.ownerGid, - size: token.fileSize, - mtime: token.fileMtime, - }, - }; - break; - } - case 'directory': { - parsedEntry = { - type: EntryType.DIRECTORY, - path: fullPath, - children: [], - stat: { - mode: token.fileMode, - uid: token.ownerUid, - gid: token.ownerGid, - size: token.fileSize, - mtime: token.fileMtime, - }, - }; - break; - } - case 'metadata': { - extendedData = new Uint8Array(token.fileSize); - extendedMetadata = {}; - break; - } - default: - throw new Error('Invalid state'); - } - // If parsed entry has not been reassigned, then it was a metadata - // header. Continue to fetch extended metadata. - if (parsedEntry == null) continue; - - const parentPath = path.dirname(fullPath); - - // If this entry is a directory, then it is pushed to the root of - // the reconstructed virtual file system and into a map at the same - // time. This allows us to add new children to the directory by - // looking up the path in a map rather than modifying the value in - // the reconstructed file system. - - if (parentPath === '/' || parentPath === '.') { - reconstructedVfs.push(parsedEntry); - } else { - // It is guaranteed that in a valid tar file, the parent will - // always exist. - const parent: DirectoryType = pathStack.get(parentPath); - parent.children.push(parsedEntry); - } - - if (parsedEntry.type === EntryType.DIRECTORY) { - pathStack.set(fullPath, parsedEntry); - } else { - // Type narrowing doesn't work well with manually specified types - currentEntry = parsedEntry as FileType; - } - - // If we were using the extended metadata for this header, reset it - // for the next header. - extendedData = undefined; - dataOffset = 0; - - break; - } - - case 'data': { - if (extendedData == null) { - // It is guaranteed that in a valid tar file, a data block will - // always come after a header block for a file. - currentEntry!['content'] += decoder.decode(token.data); - } else { - extendedData.set(token.data, dataOffset); - dataOffset += token.data.byteLength; - } - break; - } - } - } - - expect(reconstructedVfs).toContainAllValues(vfs); - }, - ); +import * as tar from '@'; + +describe('index', () => { + test('exports Generator, Parser, constants, errors, types, and utils', () => { + expect('Generator' in tar).toBeTrue(); + expect('Parser' in tar).toBeTrue(); + expect('constants' in tar).toBeTrue(); + expect('errors' in tar).toBeTrue(); + expect('utils' in tar).toBeTrue(); + expect('types' in tar).toBeTrue(); + }); }); + +test('test', async () => { + const fs = await import('fs'); + const generator = new tar.Generator(); + const fd = await fs.promises.open('./tmp/test.tar', 'w+'); + await fd.write(generator.generateFile('abc/def/file.txt', {size: 3})); + await fd.write(generator.generateData(Buffer.from('123'))); + await fd.write(generator.generateEnd()); + await fd.write(generator.generateEnd()); + await fd.close(); +}); \ No newline at end of file diff --git a/tests/integration.test.ts b/tests/integration.test.ts new file mode 100644 index 0000000..a31e589 --- /dev/null +++ b/tests/integration.test.ts @@ -0,0 +1,204 @@ +import type { VirtualFile, VirtualDirectory } from './types'; +import path from 'path'; +import { test } from '@fast-check/jest'; +import Generator from '@/Generator'; +import Parser from '@/Parser'; +import { EntryType, ExtendedHeaderKeywords } from '@/types'; +import * as tarUtils from '@/utils'; +import * as tarConstants from '@/constants'; +import * as utils from './utils'; + +describe('integration testing', () => { + test.skip.prop([utils.virtualFsArb])( + 'should archive and unarchive a virtual file system', + (vfs) => { + const generator = new Generator(); + const blocks: Array = []; + + const generateArchive = (entry: VirtualFile | VirtualDirectory) => { + if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { + // Push the extended metadata header + const data = tarUtils.encodeExtendedHeader({ + [ExtendedHeaderKeywords.FILE_PATH]: entry.path, + }); + blocks.push(generator.generateExtended(data.byteLength)); + + // Push the content + for ( + let offset = 0; + offset < data.byteLength; + offset += tarConstants.BLOCK_SIZE + ) { + blocks.push( + generator.generateData( + data.subarray(offset, offset + tarConstants.BLOCK_SIZE), + ), + ); + } + } + + const filePath = entry.path.length <= 255 ? entry.path : ''; + + switch (entry.type) { + case 'file': { + // Generate the header + entry = entry as VirtualFile; + blocks.push(generator.generateFile(filePath, entry.stat)); + + // Generate the data + const encoder = new TextEncoder(); + let content = entry.content; + while (content.length > 0) { + const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); + blocks.push(generator.generateData(encoder.encode(dataChunk))); + content = content.slice(tarConstants.BLOCK_SIZE); + } + break; + } + + case 'directory': { + // Generate the header + entry = entry as VirtualDirectory; + blocks.push(generator.generateDirectory(filePath, entry.stat)); + + // Perform the same operation on all children + for (const file of entry.children) { + generateArchive(file); + } + break; + } + + default: + tarUtils.never('Invalid type'); + } + }; + + for (const entry of vfs) { + generateArchive(entry); + } + blocks.push(generator.generateEnd()); + blocks.push(generator.generateEnd()); + + // The tar archive should be inside the blocks array now. Each block is + // a single chunk aligned to 512-byte. Now we can parse it and check if + // the parsed virtual file system matches the input. + + const parser = new Parser(); + const decoder = new TextDecoder(); + const reconstructedVfs: Array = []; + const pathStack: Map = new Map(); + let currentEntry: VirtualFile; + let extendedData: Uint8Array | undefined; + let dataOffset = 0; + + for (const chunk of blocks) { + const token = parser.write(chunk); + if (token == null) continue; + + switch (token.type) { + case 'header': { + let parsedEntry: VirtualFile | VirtualDirectory | undefined; + let extendedMetadata: + | Partial> + | undefined; + if (extendedData != null) { + extendedMetadata = tarUtils.decodeExtendedHeader(extendedData); + } + + const fullPath = extendedMetadata?.path?.trim() + ? extendedMetadata.path + : token.filePath; + + switch (token.fileType) { + case 'file': { + parsedEntry = { + type: 'file', + path: fullPath, + content: '', + stat: { + mode: token.fileMode, + uid: token.ownerUid, + gid: token.ownerGid, + size: token.fileSize, + mtime: token.fileMtime, + }, + }; + break; + } + case 'directory': { + parsedEntry = { + type: 'directory', + path: fullPath, + children: [], + stat: { + mode: token.fileMode, + uid: token.ownerUid, + gid: token.ownerGid, + size: token.fileSize, + mtime: token.fileMtime, + }, + }; + break; + } + case 'metadata': { + extendedData = new Uint8Array(token.fileSize); + extendedMetadata = {}; + break; + } + default: + throw new Error('Invalid state'); + } + // If parsed entry has not been reassigned, then it was a metadata + // header. Continue to fetch extended metadata. + if (parsedEntry == null) continue; + + const parentPath = path.dirname(fullPath); + + // If this entry is a directory, then it is pushed to the root of + // the reconstructed virtual file system and into a map at the same + // time. This allows us to add new children to the directory by + // looking up the path in a map rather than modifying the value in + // the reconstructed file system. + + if (parentPath === '.') { + reconstructedVfs.push(parsedEntry); + } else { + // It is guaranteed that in a valid tar file, the parent will + // always exist. + const parent: VirtualDirectory = pathStack.get(parentPath + '/'); + parent.children.push(parsedEntry); + } + + if (parsedEntry.type === 'directory') { + pathStack.set(fullPath, parsedEntry); + } else { + // Type narrowing doesn't work well with manually specified types + currentEntry = parsedEntry as VirtualFile; + } + + // If we were using the extended metadata for this header, reset it + // for the next header. + extendedData = undefined; + dataOffset = 0; + + break; + } + + case 'data': { + if (extendedData == null) { + // It is guaranteed that in a valid tar file, a data block will + // always come after a header block for a file. + currentEntry!['content'] += decoder.decode(token.data); + } else { + extendedData.set(token.data, dataOffset); + dataOffset += token.data.byteLength; + } + break; + } + } + } + + expect(reconstructedVfs).toContainAllValues(vfs); + }, + ); +}); diff --git a/tests/types.ts b/tests/types.ts new file mode 100644 index 0000000..c2f98f4 --- /dev/null +++ b/tests/types.ts @@ -0,0 +1,22 @@ +import type { FileStat } from '@/types'; + +type VirtualFile = { + type: 'file'; + path: string; + stat: FileStat; + content: string; +}; + +type VirtualDirectory = { + type: 'directory'; + path: string; + stat: FileStat; + children: Array; +}; + +type VirtualMetadata = { + type: 'extended'; + size: number; +}; + +export type { VirtualFile, VirtualDirectory, VirtualMetadata } diff --git a/tests/utils.ts b/tests/utils.ts deleted file mode 100644 index 1f8702e..0000000 --- a/tests/utils.ts +++ /dev/null @@ -1,331 +0,0 @@ -import type { FileStat } from '@/types'; -import fc from 'fast-check'; -import { ExtendedHeaderKeywords, HeaderSize } from '@/types'; -import { HeaderOffset } from '@/types'; -import { EntryType } from '@/types'; -import * as tarUtils from '@/utils'; -import * as tarConstants from '@/constants'; - -type FileType = { - type: EntryType.FILE; - path: string; - stat: FileStat; - content: string; -}; - -type DirectoryType = { - type: EntryType.DIRECTORY; - path: string; - stat: FileStat; - children: Array; -}; - -type MetadataType = { - type: EntryType.EXTENDED; - size: number; -}; - -function splitHeaderData(data: Uint8Array) { - return { - name: tarUtils.parseFilePath(data), - type: tarUtils.extractString(data, 156, 1), - mode: tarUtils.extractOctal(data, 100, 8), - uid: tarUtils.extractOctal(data, 108, 8), - gid: tarUtils.extractOctal(data, 116, 8), - size: tarUtils.extractOctal(data, 124, 12), - mtime: tarUtils.extractOctal(data, 136, 12), - format: tarUtils.extractString(data, 257, 6), - version: tarUtils.extractString(data, 263, 2), - }; -} - -const filenameArb = ( - { minLength, maxLength } = { minLength: 1, maxLength: 512 }, -) => - fc - .string({ minLength, maxLength }) - .filter((name) => !name.includes('/') && name !== '.' && name !== '..') - .noShrink(); - -const fileContentArb = (maxLength: number = 4096) => - fc.string({ minLength: 0, maxLength }).noShrink(); - -// Dates are stored in 11 digits of octal number. This can store from 0 to -// 0o77777777777 or 8589934591 seconds. This comes up to 2242-03-16T12:56:31. -const statDataArb = ( - type: EntryType, - content: string = '', -): fc.Arbitrary => - fc - .record({ - mode: fc.constant(0o777), - uid: fc.integer({ min: 0, max: 65535 }), - gid: fc.integer({ min: 0, max: 65535 }), - size: fc.constant(type === EntryType.FILE ? content.length : 0), - mtime: fc - .date({ - min: new Date(0), - max: new Date(0o77777777777 * 1000), - noInvalidDate: true, - }) - .map((date) => new Date(Math.floor(date.getTime() / 1000) * 1000)), // Snap to whole seconds - }) - .noShrink(); - -const fileArb = ( - parentPath: string = '', - dataLength: number = 4096, -): fc.Arbitrary => - fc - .record({ - type: fc.constant(EntryType.FILE), - path: filenameArb().map((name) => `${parentPath}/${name}`), - content: fileContentArb(dataLength), - }) - .chain((file) => - statDataArb(EntryType.FILE, file.content).map((stat) => ({ - ...file, - stat, - })), - ) - .noShrink(); - -const dirArb = ( - depth: number, - parentPath: string = '', -): fc.Arbitrary => - fc - .record({ - type: fc.constant(EntryType.DIRECTORY), - path: filenameArb().map((name) => `${parentPath}/${name}`), - }) - .chain((dir) => - fc - .array( - fc.oneof( - { weight: 3, arbitrary: fileArb(dir.path) }, - { - weight: depth > 0 ? 1 : 0, - arbitrary: dirArb(depth - 1, dir.path), - }, - ), - { - minLength: 0, - maxLength: 4, - }, - ) - .map((children) => ({ ...dir, children })), - ) - .chain((dir) => - statDataArb(EntryType.DIRECTORY).map((stat) => ({ ...dir, stat })), - ) - .noShrink(); - -const virtualFsArb = fc - .array(fc.oneof(fileArb(), dirArb(5)), { - minLength: 1, - maxLength: 10, - }) - .noShrink(); - -const tarHeaderArb = ( - { minLength, maxLength } = { - minLength: 1, - maxLength: 512, - }, -) => - fc - .record({ - path: filenameArb({ minLength, maxLength }), - uid: fc.nat(65535), - gid: fc.nat(65535), - size: fc.nat(65536), - typeflag: fc.constantFrom('0', '5'), - }) - .map(({ path, uid, gid, size, typeflag }) => { - let headers: Array = []; - headers.push(new Uint8Array(tarConstants.BLOCK_SIZE)); - const type = typeflag as '0' | '5' | 'x'; - const encoder = new TextEncoder(); - - if (type === '5') size = 0; - - // If the - if (path.length > tarConstants.STANDARD_PATH_SIZE) { - // Set the metadata for the header - const extendedHeader = new Uint8Array(tarConstants.BLOCK_SIZE); - const extendedData = tarUtils.encodeExtendedHeader({ - [ExtendedHeaderKeywords.FILE_PATH]: path, - }); - - // Set the size of the content, the type flag, the ustar values, and the - // checksum. - extendedHeader.set( - encoder.encode( - tarUtils.pad( - extendedData.byteLength, - HeaderSize.FILE_SIZE, - '0', - '\0', - ), - ), - HeaderOffset.FILE_SIZE, - ); - - extendedHeader.set( - encoder.encode(tarConstants.USTAR_NAME), - HeaderOffset.USTAR_NAME, - ); - extendedHeader.set( - encoder.encode(tarConstants.USTAR_VERSION), - HeaderOffset.USTAR_VERSION, - ); - extendedHeader.set( - encoder.encode(EntryType.EXTENDED), - HeaderOffset.TYPE_FLAG, - ); - - const checksum = tarUtils.calculateChecksum(extendedHeader); - extendedHeader.set( - encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '), - HeaderOffset.CHECKSUM, - ); - - // Split out the data to 512-byte chunks - const data: Array = []; - let offset = 0; - while (offset < extendedData.length) { - const block = new Uint8Array(tarConstants.BLOCK_SIZE); - block.set( - extendedData.slice(offset, offset + tarConstants.BLOCK_SIZE), - ); - data.push(block); - offset += tarConstants.BLOCK_SIZE; - } - - headers = [extendedHeader, ...data, ...headers]; - } else { - if (path.length < HeaderSize.FILE_NAME) { - headers - .at(-1)! - .set( - encoder.encode( - tarUtils.splitFileName(path, 0, HeaderSize.FILE_NAME), - ), - HeaderOffset.FILE_NAME, - ); - } else { - const fileSuffix = tarUtils.splitFileName( - path, - 0, - HeaderSize.FILE_NAME, - ); - const filePrefix = tarUtils.splitFileName( - path, - HeaderSize.FILE_NAME, - HeaderSize.FILE_NAME_PREFIX, - ); - headers - .at(-1)! - .set(encoder.encode(fileSuffix), HeaderOffset.FILE_NAME); - headers - .at(-1)! - .set(encoder.encode(filePrefix), HeaderOffset.FILE_NAME_PREFIX); - } - } - - // Fill normal header fields - headers.at(-1)!.set(encoder.encode('0000777'), HeaderOffset.FILE_MODE); - headers - .at(-1)! - .set( - encoder.encode(uid.toString(8).padStart(7, '0')), - HeaderOffset.OWNER_UID, - ); - headers - .at(-1)! - .set( - encoder.encode(gid.toString(8).padStart(7, '0')), - HeaderOffset.OWNER_GID, - ); - headers - .at(-1)! - .set( - encoder.encode(size.toString(8).padStart(11, '0') + '\0'), - HeaderOffset.FILE_SIZE, - ); - headers.at(-1)!.set(encoder.encode(' '), HeaderOffset.CHECKSUM); - headers.at(-1)!.set(encoder.encode(type), HeaderOffset.TYPE_FLAG); - headers - .at(-1)! - .set(encoder.encode(tarConstants.USTAR_NAME), HeaderOffset.USTAR_NAME); - headers - .at(-1)! - .set( - encoder.encode(tarConstants.USTAR_VERSION), - HeaderOffset.USTAR_VERSION, - ); - - // Compute and set checksum - const checksum = headers.at(-1)!.reduce((sum, byte) => sum + byte, 0); - headers - .at(-1)! - .set( - encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '), - HeaderOffset.CHECKSUM, - ); - - return { headers, stat: { type, size, path, uid, gid } }; - }) - .noShrink(); - -const tarDataArb = tarHeaderArb() - .chain((header) => - fc - .record({ - header: fc.constant(header), - data: fc.string({ - minLength: header.stat.size, - maxLength: header.stat.size, - }), - }) - .map(({ header, data }) => { - const { headers, stat } = header; - const encoder = new TextEncoder(); - const encodedData = encoder.encode(data); - - // Directories don't have any data, so set their size to zero. - let dataBlock: Uint8Array; - if (stat.type === '0') { - // Make sure the data is aligned to 512-byte chunks - dataBlock = new Uint8Array( - Math.ceil(stat.size / tarConstants.BLOCK_SIZE) * - tarConstants.BLOCK_SIZE, - ); - dataBlock.set(encodedData); - } else { - dataBlock = new Uint8Array(0); - } - - return { - headers: headers, - data: data, - encodedData: dataBlock, - type: stat.type, - }; - }), - ) - .noShrink(); - -export type { FileType, DirectoryType, MetadataType }; -export { - splitHeaderData, - filenameArb, - fileContentArb, - statDataArb, - fileArb, - dirArb, - virtualFsArb, - tarHeaderArb, - tarDataArb, -}; diff --git a/tests/utils/fastcheck.ts b/tests/utils/fastcheck.ts new file mode 100644 index 0000000..f3bd8f0 --- /dev/null +++ b/tests/utils/fastcheck.ts @@ -0,0 +1,306 @@ +import type { VirtualFile, VirtualDirectory } from '../types'; +import type { FileStat } from '@/types'; +import { EntryType, HeaderSize, HeaderOffset } from '@/types'; +import fc from 'fast-check'; +import * as tarConstants from '@/constants'; +import * as tarUtils from '@/utils'; + +/** + * Creates an arbitrary to produce valid (and common) unix file modes. Note that + * this is only used for testing virtual file system and not for any tests which + * interact with the physical file system. + */ +const modeArb = (type?: 'file' | 'directory') => { + switch (type) { + case 'file': + return fc.constant(0o100755); + case 'directory': + return fc.constant(0o40755); + case undefined: + return fc.constant(0o755); + } +}; + +const uidgidArb = fc.integer({ min: 1000, max: 4096 }); + +const sizeArb = (type?: 'file' | 'directory', content?: string) => { + if (type === 'file') { + if (content == null) throw new Error('Files must have content'); + return fc.constant(content.length); + } + return fc.constant(0); +}; + +// Produce valid dates which snap to whole seconds +const mtimeArb = fc + .date({ + min: new Date(0), + max: new Date(0o77777777777 * 1000), + noInvalidDate: true, + }) + .map((date) => new Date(Math.floor(date.getTime() / 1000) * 1000)); + +/** + * Due to the large amount of conditions, using a string primitive arbitrary + * takes unfeasibly long to generate values, especially for larger path lengths. + * To bypass this, an optimisation has been made in the generation process. An + * array of valid ASCII numbers are generated and any characters which might be + * invalid on an operating system are filtered out. Then, any complex operations + * like filtering out compound words are carried out. At this stage, it is much + * more efficient than applying all these filters on a primitive string arbitrary. + * + * This care is needed to make sure operations involving writing files to disk + * won't be platform-dependent. + */ +const filenameArb = ( + parent: string = '', + { + minLength, + maxLength, + }: { + minLength?: number; + maxLength?: number; + } = { + minLength: 1, + maxLength: 512, + }, +) => { + // Most of these characters are disallowed by windows + const restrictedCharacters = '/\\*?"<>|:'; + const filterRegex = /^(\.|\.\.|con|prn|aux|nul|tty|null|zero|full)$/i; + + const charCodes = fc.array( + fc + .integer({ min: 33, max: 126 }) + .filter( + (char) => !restrictedCharacters.includes(String.fromCharCode(char)), + ), + { minLength, maxLength }, + ); + + const fileName = charCodes.map((chars) => String.fromCharCode(...chars)); + const filteredFileName = fileName.filter((name) => !filterRegex.test(name)); + + // If there is a parent path, then properly nest the generated file name + // relative to the parent path. + if (parent !== '') { + return filteredFileName + .map( + (name) => + `${ + !parent.endsWith('/') && parent !== '' ? parent + '/' : parent + }${name}`, + ) + .noShrink(); + } + return filteredFileName.noShrink(); +}; + +const fileContentArb = (maxLength: number = 4096) => + fc.string({ minLength: 0, maxLength, unit: 'binary-ascii' }).noShrink(); + +const statDataArb = ( + type: 'file' | 'directory', + content: string = '', +): fc.Arbitrary => + fc + .record({ + mode: modeArb(type), + uid: uidgidArb, + gid: uidgidArb, + size: sizeArb(type, content), + mtime: mtimeArb, + }) + .noShrink(); + +const fileArb = ( + parentPath: string = '', + dataLength: number = 4096, + { + minFilePathSize, + maxFilePathSize, + }: { + minFilePathSize?: number; + maxFilePathSize?: number; + } = {}, +): fc.Arbitrary => { + // Generate file-specific records + const fileData = fc.record({ + type: fc.constant<'file'>('file'), + path: filenameArb(parentPath, { + minLength: minFilePathSize, + maxLength: maxFilePathSize, + }), + content: fileContentArb(dataLength), + }); + + // Generate the stat based on the file data + const fileWithStat = fileData.chain((file) => + statDataArb('file', file.content).map((stat) => ({ + ...file, + stat, + })), + ); + + return fileWithStat.noShrink(); +}; + +const dirArb = ( + depth: number, + parentPath: string = '', + { + minFilePathSize, + maxFilePathSize, + }: { + minFilePathSize?: number; + maxFilePathSize?: number; + } = {}, +): fc.Arbitrary => { + // Generate directory-specific records + const dirData = fc.record({ + type: fc.constant<'directory'>('directory'), + path: filenameArb(parentPath, { + minLength: minFilePathSize, + maxLength: maxFilePathSize, + }).map( + (name) => + `${ + !parentPath.endsWith('/') && parentPath !== '' + ? parentPath + '/' + : parentPath + }${name}/`, + ), + }); + + // Add either subdirectories or files as children of the directory + const populatedDir = dirData.chain((dir) => { + // By default, there is a 1 in 4 chance of a subdirectory being created under + // this directory. However, if we have reached the maximum depth of recursion, + // then the directory weight drops to zero, ensuring all entries will be a + // file. + const dirWeight = depth > 0 ? 1 : 0; + + const fileOrDir = fc.oneof( + { + weight: 3, + arbitrary: fileArb(dir.path, undefined, { + minFilePathSize, + maxFilePathSize, + }), + }, + { + weight: dirWeight, + arbitrary: dirArb(depth - 1, dir.path, { + minFilePathSize, + maxFilePathSize, + }), + }, + ); + + const children = fc.array(fileOrDir, { minLength: 0, maxLength: 4 }); + return children.map((children) => ({ ...dir, children })); + }); + + const dirWithStat = populatedDir.chain((dir) => + statDataArb('directory').map((stat) => ({ ...dir, stat })), + ); + + return dirWithStat.noShrink(); +}; + +/** + * Uses arbitraries generating files and directories to create a virtual file + * system as a JSON object. + */ +const virtualFsArb: fc.Arbitrary> = fc + .array(fc.oneof(fileArb(), dirArb(5)), { + minLength: 1, + maxLength: 10, + }) + .noShrink(); + +const tarEntryArb = ({ + minFilePathSize, + maxFilePathSize, +}: { + minFilePathSize?: number; + maxFilePathSize?: number; +} = {}) => { + const data = fc.oneof( + fileArb(undefined, undefined, { minFilePathSize, maxFilePathSize }), + dirArb(0, undefined, { minFilePathSize, maxFilePathSize }), + ); + + const headers = data.map((data) => { + const extendedHeaders: Array = []; + const header = new Uint8Array(tarConstants.BLOCK_SIZE); + const dataHeaders: Array = []; + + // Write the file path + if (data.path.length > tarConstants.STANDARD_PATH_SIZE) { + const extendedData = tarUtils.encodeExtendedHeader({ path: data.path }); + + // Create the extended header array + const extendedHeader = new Uint8Array(tarConstants.BLOCK_SIZE); + tarUtils.writeFileType(extendedHeader, 'extended'); + tarUtils.writeFileSize(extendedHeader, extendedData.byteLength); + tarUtils.writeUstarMagic(extendedHeader); + tarUtils.writeChecksum( + extendedHeader, + tarUtils.calculateChecksum(extendedHeader), + ); + extendedHeaders.push(extendedHeader); + + // Push the data array in 512-byte chunks + let offset = 0; + while (offset < extendedData.length) { + const block = new Uint8Array(tarConstants.BLOCK_SIZE); + block.set(extendedData.slice(offset, offset + tarConstants.BLOCK_SIZE)); + extendedHeaders.push(block); + offset += tarConstants.BLOCK_SIZE; + } + } else { + tarUtils.writeFilePath(header, data.path); + } + + // Write the regular header info + tarUtils.writeFileType(header, data.type); + tarUtils.writeFileSize(header, data.stat.size); + tarUtils.writeFileMode(header, data.stat.mode); + tarUtils.writeFileMtime(header, data.stat.mtime); + tarUtils.writeOwnerUid(header, data.stat.uid); + tarUtils.writeOwnerGid(header, data.stat.gid); + tarUtils.writeUstarMagic(header); + tarUtils.writeChecksum(header, tarUtils.calculateChecksum(header)); + + // If it is a file, then append the data to the data array + if (data.type === 'file') { + let content = data.content; + const encoder = new TextEncoder(); + while (content.length > 0) { + const block = new Uint8Array(tarConstants.BLOCK_SIZE); + const chunk = content.substring(0, tarConstants.BLOCK_SIZE); + block.set(encoder.encode(chunk)); + dataHeaders.push(block); + content = content.substring(tarConstants.BLOCK_SIZE); + } + } + + return { headers: [...extendedHeaders, header, ...dataHeaders], data }; + }); + return headers.noShrink(); +}; + +export { + modeArb, + uidgidArb, + sizeArb, + mtimeArb, + filenameArb, + fileContentArb, + statDataArb, + fileArb, + dirArb, + virtualFsArb, + tarEntryArb, +}; diff --git a/tests/utils/index.ts b/tests/utils/index.ts new file mode 100644 index 0000000..28700db --- /dev/null +++ b/tests/utils/index.ts @@ -0,0 +1,2 @@ +export * from './fastcheck'; +export * from './utils'; \ No newline at end of file diff --git a/tests/utils/utils.ts b/tests/utils/utils.ts new file mode 100644 index 0000000..3b1cd70 --- /dev/null +++ b/tests/utils/utils.ts @@ -0,0 +1,30 @@ +import * as tarUtils from '@/utils'; + +const deepSort = (obj: unknown) => { + if (Array.isArray(obj)) { + return obj + .map(deepSort) + .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + } else if (typeof obj === 'object' && obj !== null) { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, deepSort(value)]), + ); + } + return obj; +}; + +function splitHeaderData(data: Uint8Array) { + return { + name: tarUtils.decodeFilePath(data), + type: tarUtils.extractString(data, 156, 1), + mode: tarUtils.extractOctal(data, 100, 8), + uid: tarUtils.extractOctal(data, 108, 8), + gid: tarUtils.extractOctal(data, 116, 8), + size: tarUtils.extractOctal(data, 124, 12), + mtime: tarUtils.extractOctal(data, 136, 12), + format: tarUtils.extractString(data, 257, 6), + version: tarUtils.extractString(data, 263, 2), + }; +} + +export { deepSort, splitHeaderData }; From 92de4eb5a244585e253783489ee0a372e636e895 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 12 Mar 2025 20:24:21 +1100 Subject: [PATCH 13/19] feat: added higher level virtualtar api --- src/Generator.ts | 28 ++-- src/Parser.ts | 6 +- src/VirtualTar.ts | 291 ++++++++++++++++++++++++++++++++++++++ src/errors.ts | 5 + src/types.ts | 59 ++++++-- src/utils.ts | 39 ++--- tests/Generator.test.ts | 285 +++++++++++++++++++------------------ tests/Parser.test.ts | 30 ++-- tests/index.test.ts | 4 +- tests/integration.test.ts | 10 +- tests/types.ts | 2 +- tests/utils/fastcheck.ts | 25 ++-- tests/utils/index.ts | 2 +- 13 files changed, 570 insertions(+), 216 deletions(-) diff --git a/src/Generator.ts b/src/Generator.ts index 597ffac..f64fa70 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -1,5 +1,5 @@ import type { FileType, FileStat } from './types'; -import { GeneratorState, EntryType, HeaderSize } from './types'; +import { GeneratorState, HeaderSize } from './types'; import * as errors from './errors'; import * as utils from './utils'; import * as constants from './constants'; @@ -46,7 +46,11 @@ class Generator { protected state: GeneratorState = GeneratorState.HEADER; protected remainingBytes = 0; - protected generateHeader(filePath: string, type: FileType, stat: FileStat): Uint8Array { + protected generateHeader( + filePath: string, + type: FileType, + stat: FileStat, + ): Uint8Array { if (filePath.length > 255) { throw new errors.ErrorVirtualTarGeneratorInvalidFileName( 'The file name must shorter than 255 characters', @@ -59,18 +63,15 @@ class Generator { ); } - if ( - stat?.username != null && - stat?.username.length > HeaderSize.OWNER_USERNAME - ) { + if (stat?.uname != null && stat?.uname.length > HeaderSize.OWNER_USERNAME) { throw new errors.ErrorVirtualTarGeneratorInvalidStat( `The username must not exceed ${HeaderSize.OWNER_USERNAME} bytes`, ); } if ( - stat?.groupname != null && - stat?.groupname.length > HeaderSize.OWNER_GROUPNAME + stat?.gname != null && + stat?.gname.length > HeaderSize.OWNER_GROUPNAME ) { throw new errors.ErrorVirtualTarGeneratorInvalidStat( `The groupname must not exceed ${HeaderSize.OWNER_GROUPNAME} bytes`, @@ -90,8 +91,8 @@ class Generator { utils.writeFileMode(header, stat.mode); utils.writeOwnerUid(header, stat.uid); utils.writeOwnerGid(header, stat.gid); - utils.writeOwnerUserName(header, stat.username); - utils.writeOwnerGroupName(header, stat.groupname); + utils.writeOwnerUserName(header, stat.uname); + utils.writeOwnerGroupName(header, stat.gname); utils.writeFileSize(header, stat.size); utils.writeFileMtime(header, stat.mtime); @@ -172,6 +173,13 @@ class Generator { if (this.remainingBytes === 0) this.state = GeneratorState.HEADER; return data; } else { + // Make sure we don't attempt to write extra data + if (data.byteLength !== this.remainingBytes) { + throw new errors.ErrorVirtualTarGeneratorBlockSize( + `Expected data to be ${this.remainingBytes} bytes but received ${data.byteLength} bytes`, + ); + } + // Update state this.remainingBytes = 0; this.state = GeneratorState.HEADER; diff --git a/src/Parser.ts b/src/Parser.ts index f182093..10313d8 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -129,14 +129,14 @@ class Parser { protected parseData(array: Uint8Array, remainingBytes: number): TokenData { if (remainingBytes > 512) { - return { type: 'data', data: utils.extractBytes(array) }; + return { type: 'data', data: utils.extractBytes(array), end: false }; } else { const data = utils.extractBytes(array, 0, remainingBytes); - return { type: 'data', data: data }; + return { type: 'data', data: data, end: true }; } } - write(data: Uint8Array) { + write(data: Uint8Array): TokenHeader | TokenData | TokenEnd | undefined { if (data.byteLength !== constants.BLOCK_SIZE) { throw new errors.ErrorVirtualTarParserBlockSize( `Expected block size to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`, diff --git a/src/VirtualTar.ts b/src/VirtualTar.ts index e69de29..5938e9d 100644 --- a/src/VirtualTar.ts +++ b/src/VirtualTar.ts @@ -0,0 +1,291 @@ +import type { + FileStat, + ParsedFile, + ParsedDirectory, + ParsedMetadata, + ParsedEmpty, + MetadataKeywords, +} from './types'; +import { VirtualTarState } from './types'; +import Generator from './Generator'; +import Parser from './Parser'; +import * as constants from './constants'; +import * as errors from './errors'; +import * as utils from './utils'; + +class VirtualTar { + protected state: VirtualTarState; + protected generator: Generator; + protected parser: Parser; + protected chunks: Array; + protected encoder = new TextEncoder(); + protected accumulator: Uint8Array; + protected workingToken: ParsedFile | ParsedMetadata | undefined; + protected workingData: Array; + protected workingMetadata: + | Partial> + | undefined; + + protected addEntry( + filePath: string, + type: 'file' | 'directory', + stat: FileStat = {}, + dataOrCallback?: + | Uint8Array + | string + | ((write: (chunk: string | Uint8Array) => void) => void), + ): void { + if (filePath.length > constants.STANDARD_PATH_SIZE) { + // Push the extended metadata header + const data = utils.encodeExtendedHeader({ path: filePath }); + this.chunks.push(this.generator.generateExtended(data.byteLength)); + + // Push the content + for ( + let offset = 0; + offset < data.byteLength; + offset += constants.BLOCK_SIZE + ) { + this.chunks.push( + this.generator.generateData( + data.subarray(offset, offset + constants.BLOCK_SIZE), + ), + ); + } + } + + filePath = filePath.length <= 255 ? filePath : ''; + + // Generate the header + if (type === 'file') { + this.chunks.push(this.generator.generateFile(filePath, stat)); + } else { + this.chunks.push(this.generator.generateDirectory(filePath, stat)); + } + + // Generate the data + if (dataOrCallback == null) return; + + const writeData = (data: string | Uint8Array) => { + if (data instanceof Uint8Array) { + for ( + let offset = 0; + offset < data.byteLength; + offset += constants.BLOCK_SIZE + ) { + const chunk = data.slice(offset, offset + constants.BLOCK_SIZE); + this.chunks.push(this.generator.generateData(chunk)); + } + } else { + while (data.length > 0) { + const chunk = this.encoder.encode( + data.slice(0, constants.BLOCK_SIZE), + ); + this.chunks.push(this.generator.generateData(chunk)); + data = data.slice(constants.BLOCK_SIZE); + } + } + }; + + if (typeof dataOrCallback === 'function') { + const data: Array = []; + const writer = (chunk: string | Uint8Array) => { + if (chunk instanceof Uint8Array) data.push(chunk); + else data.push(this.encoder.encode(chunk)); + }; + dataOrCallback(writer); + writeData(utils.concatUint8Arrays(...data)); + } else { + writeData(dataOrCallback); + } + } + + constructor({ mode }: { mode: 'generate' | 'parse' } = { mode: 'parse' }) { + if (mode === 'generate') { + this.state = VirtualTarState.GENERATOR; + this.generator = new Generator(); + this.chunks = []; + } else { + this.state = VirtualTarState.PARSER; + this.parser = new Parser(); + this.workingData = []; + } + } + + public addFile( + filePath: string, + stat: FileStat, + data?: Uint8Array | string, + ): void; + + public addFile( + filePath: string, + stat: FileStat, + data?: + | Uint8Array + | string + | ((writer: (chunk: string | Uint8Array) => void) => void), + ): void; + + public addFile( + filePath: string, + stat: FileStat, + data?: + | Uint8Array + | string + | ((writer: (chunk: string | Uint8Array) => void) => void), + ): void { + if (this.state !== VirtualTarState.GENERATOR) { + throw new errors.ErrorVirtualTarInvalidState( + 'VirtualTar is not in generator mode', + ); + } + this.addEntry(filePath, 'file', stat, data); + } + + public addDirectory(filePath: string, stat?: FileStat): void { + if (this.state !== VirtualTarState.GENERATOR) { + throw new errors.ErrorVirtualTarInvalidState( + 'VirtualTar is not in generator mode', + ); + } + this.addEntry(filePath, 'directory', stat); + } + + public finalize(): Uint8Array { + if (this.state !== VirtualTarState.GENERATOR) { + throw new errors.ErrorVirtualTarInvalidState( + 'VirtualTar is not in generator mode', + ); + } + this.chunks.push(this.generator.generateEnd()); + this.chunks.push(this.generator.generateEnd()); + return utils.concatUint8Arrays(...this.chunks); + } + + public push(chunk: Uint8Array): void { + if (this.state !== VirtualTarState.PARSER) { + throw new errors.ErrorVirtualTarInvalidState( + 'VirtualTar is not in parser mode', + ); + } + this.accumulator = utils.concatUint8Arrays(this.accumulator, chunk); + } + + public next(): ParsedFile | ParsedDirectory | ParsedEmpty { + if (this.state !== VirtualTarState.PARSER) { + throw new errors.ErrorVirtualTarInvalidState( + 'VirtualTar is not in parser mode', + ); + } + if (this.accumulator.byteLength < constants.BLOCK_SIZE) { + return { type: 'empty', awaitingData: true }; + } + + const chunk = this.accumulator.slice(0, constants.BLOCK_SIZE); + this.accumulator = this.accumulator.slice(constants.BLOCK_SIZE); + const token = this.parser.write(chunk); + + if (token == null) { + return { type: 'empty', awaitingData: false }; + } + + if (token.type === 'header') { + if (token.fileType === 'metadata') { + this.workingToken = { type: 'metadata' }; + return { type: 'empty', awaitingData: false }; + } + + // If we have additional metadata, then use it to override token data + let filePath = token.filePath; + if (this.workingMetadata != null) { + filePath = this.workingMetadata.path ?? filePath; + this.workingMetadata = undefined; + } + + if (token.fileType === 'directory') { + return { + type: 'directory', + path: filePath, + stat: { + size: token.fileSize, + mode: token.fileMode, + mtime: token.fileMtime, + uid: token.ownerUid, + gid: token.ownerGid, + uname: token.ownerUserName, + gname: token.ownerGroupName, + }, + }; + } else if (token.fileType === 'file') { + this.workingToken = { + type: 'file', + path: filePath, + stat: { + size: token.fileSize, + mode: token.fileMode, + mtime: token.fileMtime, + uid: token.ownerUid, + gid: token.ownerGid, + uname: token.ownerUserName, + gname: token.ownerGroupName, + }, + content: new Uint8Array(token.fileSize), + }; + } + } else { + if (this.workingToken == null) { + throw new errors.ErrorVirtualTarInvalidState( + 'Received data token before header token', + ); + } + if (token.type === 'end') { + throw new errors.ErrorVirtualTarInvalidState( + 'Received end token before header token', + ); + } + + // Token is of type 'data' after this + const { data, end } = token; + this.workingData.push(data); + + if (end) { + // Concat the working data into a single Uint8Array + const data = utils.concatUint8Arrays(...this.workingData); + this.workingData = []; + + // If the current working token is a metadata token, then decode the + // accumulated header. Otherwise, we have obtained all the data for + // a file. Set the content of the file then return it. + if (this.workingToken.type === 'metadata') { + this.workingMetadata = utils.decodeExtendedHeader(data); + return { type: 'empty', awaitingData: false }; + } else if (this.workingToken.type === 'file') { + this.workingToken.content.set(data); + const fileToken = this.workingToken; + this.workingToken = undefined; + return fileToken; + } + } + } + return { type: 'empty', awaitingData: false }; + } + + public parseAvailable(): Array { + if (this.state !== VirtualTarState.PARSER) { + throw new errors.ErrorVirtualTarInvalidState( + 'VirtualTar is not in parser mode', + ); + } + + const parsedTokens: Array = []; + let token; + while (token.type !== 'empty' && !token.awaitingData) { + token = this.next(); + parsedTokens.push(token); + } + return parsedTokens; + } +} + +export default VirtualTar; diff --git a/src/errors.ts b/src/errors.ts index 6df6f0c..ab3f179 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -8,6 +8,10 @@ class ErrorVirtualTarUndefinedBehaviour extends ErrorVirtualTar { static description = 'You should never see this error'; } +class ErrorVirtualTarInvalidState extends ErrorVirtualTar { + static description = 'The state is incorrect for the desired operation'; +} + class ErrorVirtualTarGenerator extends ErrorVirtualTar { static description = 'VirtualTar genereator errors'; } @@ -59,6 +63,7 @@ class ErrorVirtualTarParserEndOfArchive extends ErrorVirtualTarParser { export { ErrorVirtualTar, ErrorVirtualTarUndefinedBehaviour, + ErrorVirtualTarInvalidState, ErrorVirtualTarGenerator, ErrorVirtualTarGeneratorInvalidFileName, ErrorVirtualTarGeneratorInvalidStat, diff --git a/src/types.ts b/src/types.ts index f5d63f3..f9325c4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ enum EntryType { EXTENDED = 'x', } -enum ExtendedHeaderKeywords { +enum MetadataKeywords { FILE_PATH = 'path', } @@ -49,13 +49,13 @@ enum HeaderSize { } type FileStat = { + size?: number; mode?: number; + mtime?: Date; uid?: number; gid?: number; - size?: number; - mtime?: Date; - username?: string; - groupname?: string; + uname?: string; + gname?: string; }; type TokenHeader = { @@ -75,17 +75,13 @@ type TokenHeader = { type TokenData = { type: 'data'; data: Uint8Array; + end: boolean; }; type TokenEnd = { type: 'end'; }; -enum _FileType { - FILE, - DIRECTORY, -} - enum ParserState { HEADER, DATA, @@ -100,14 +96,51 @@ enum GeneratorState { ENDED, } -export type { FileType, FileStat, TokenHeader, TokenData, TokenEnd }; +enum VirtualTarState { + GENERATOR, + PARSER, +} + +type ParsedFile = { + type: 'file'; + path: string; + stat: FileStat; + content: Uint8Array; +}; + +type ParsedDirectory = { + type: 'directory'; + path: string; + stat: FileStat; +}; + +type ParsedMetadata = { + type: 'metadata'; +}; + +type ParsedEmpty = { + type: 'empty'; + awaitingData: boolean; +}; + +export type { + FileType, + FileStat, + TokenHeader, + TokenData, + TokenEnd, + ParsedFile, + ParsedDirectory, + ParsedMetadata, + ParsedEmpty, +}; export { EntryType, - ExtendedHeaderKeywords, + MetadataKeywords, HeaderOffset, HeaderSize, - _FileType, ParserState, GeneratorState, + VirtualTarState, }; diff --git a/src/utils.ts b/src/utils.ts index 5e4a9a0..e663943 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,9 +1,4 @@ -import { - EntryType, - ExtendedHeaderKeywords, - HeaderOffset, - HeaderSize, -} from './types'; +import { EntryType, MetadataKeywords, HeaderOffset, HeaderSize } from './types'; import * as errors from './errors'; import * as constants from './constants'; @@ -147,7 +142,7 @@ function writeBytesToArray( } function writeFilePath(header: Uint8Array, filePath: string): void { - // return fileName.slice(offset, offset + size).padEnd(size, padding); + // Return fileName.slice(offset, offset + size).padEnd(size, padding); // If the length of the file path is less than 100 bytes, then we write it to // the file name. Otherwise, we write it into the file name prefix and append // file name to it. @@ -313,7 +308,7 @@ function writeOwnerGroupName(header: Uint8Array, groupname?: string): void { } function encodeExtendedHeader( - data: Partial>, + data: Partial>, ): Uint8Array { const encoder = new TextEncoder(); let totalByteSize = 0; @@ -353,9 +348,9 @@ function encodeExtendedHeader( function decodeExtendedHeader( array: Uint8Array, -): Partial> { +): Partial> { const decoder = new TextDecoder(); - const data: Partial> = {}; + const data: Partial> = {}; // Track offset and remaining bytes in the array let offset = 0; @@ -378,19 +373,15 @@ function decodeExtendedHeader( const key = line.substring(0, entrySeparatorIndex); const _value = line.substring(entrySeparatorIndex + 1); - if ( - !Object.values(ExtendedHeaderKeywords).includes( - key as ExtendedHeaderKeywords, - ) - ) { + if (!Object.values(MetadataKeywords).includes(key as MetadataKeywords)) { throw new Error('TMP key doesnt exist'); } // Remove the trailing newline const value = _value.substring(0, _value.length - 1); - switch (key as ExtendedHeaderKeywords) { - case ExtendedHeaderKeywords.FILE_PATH: { - data[ExtendedHeaderKeywords.FILE_PATH] = value; + switch (key as MetadataKeywords) { + case MetadataKeywords.FILE_PATH: { + data[MetadataKeywords.FILE_PATH] = value; } } @@ -401,6 +392,17 @@ function decodeExtendedHeader( return data; } +function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array { + const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + return result; +} + export { never, pad, @@ -426,4 +428,5 @@ export { writeOwnerGroupName, encodeExtendedHeader, decodeExtendedHeader, + concatUint8Arrays, }; diff --git a/tests/Generator.test.ts b/tests/Generator.test.ts index 913215d..f955974 100644 --- a/tests/Generator.test.ts +++ b/tests/Generator.test.ts @@ -4,9 +4,9 @@ import os from 'os'; import path from 'path'; import fc from 'fast-check'; import { test } from '@fast-check/jest'; +import * as tar from 'tar'; import { EntryType, GeneratorState } from '@/types'; import Generator from '@/Generator'; -import * as tar from 'tar'; import * as tarConstants from '@/constants'; import * as tarErrors from '@/errors'; import * as tarUtils from '@/utils'; @@ -161,158 +161,161 @@ describe('generator state robustness', () => { }); describe('testing against tar', () => { - test.skip.prop([utils.virtualFsArb])('should match output of tar', async (vfs) => { - // Create a temp directory to use for node-tar - const tempDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'js-virtualtar-test-'), - ); + test.skip.prop([utils.fileTreeArb])( + 'should match output of tar', + async (vfs) => { + // Create a temp directory to use for node-tar + const tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'js-virtualtar-test-'), + ); - try { - // Create the archive using the Generator - const generator = new Generator(); - const blocks: Array = []; - - const trimmedVfs = structuredClone(vfs); - const trimStat = (entry: VirtualFile | VirtualDirectory) => { - entry.stat = { size: entry.stat.size, mode: entry.stat.mode }; - if (entry.type === 'directory') { - for (const child of entry.children) { - trimStat(child); + try { + // Create the archive using the Generator + const generator = new Generator(); + const blocks: Array = []; + + const trimmedVfs = structuredClone(vfs); + const trimStat = (entry: VirtualFile | VirtualDirectory) => { + entry.stat = { size: entry.stat.size, mode: entry.stat.mode }; + if (entry.type === 'directory') { + for (const child of entry.children) { + trimStat(child); + } } - } - }; - for (const entry of trimmedVfs) trimStat(entry); - - const generateEntry = (entry: VirtualFile | VirtualDirectory) => { - // Due to operating system restrictions, node-tar cannot properly - // reproduce all the metadata at the time of extracting files. The - // mtime defaults to extraction time, the uid and gid is fixed to the - // user who the program is running under. As fast-check is used to - // generate this data, this will always differ than the observed stat, - // so these fields will be ignored for this test. - entry.stat = { - mode: entry.stat.mode, - size: entry.stat.size, }; - - if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { - // Push the extended metadata header - const data = tarUtils.encodeExtendedHeader({ path: entry.path }); - blocks.push(generator.generateExtended(data.byteLength)); - - // Push the content block - for ( - let offset = 0; - offset < data.byteLength; - offset += tarConstants.BLOCK_SIZE - ) { - blocks.push( - generator.generateData( - data.subarray(offset, offset + tarConstants.BLOCK_SIZE), - ), - ); - } - } - - const filePath = - entry.path.length <= tarConstants.STANDARD_PATH_SIZE - ? entry.path - : ''; - - switch (entry.type) { - case 'file': { - // Generate the header - entry = entry as VirtualFile; - blocks.push(generator.generateFile(filePath, entry.stat)); - - // Generate the data - const encoder = new TextEncoder(); - let content = entry.content; - while (content.length > 0) { - const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); - blocks.push(generator.generateData(encoder.encode(dataChunk))); - content = content.slice(tarConstants.BLOCK_SIZE); + for (const entry of trimmedVfs) trimStat(entry); + + const generateEntry = (entry: VirtualFile | VirtualDirectory) => { + // Due to operating system restrictions, node-tar cannot properly + // reproduce all the metadata at the time of extracting files. The + // mtime defaults to extraction time, the uid and gid is fixed to the + // user who the program is running under. As fast-check is used to + // generate this data, this will always differ than the observed stat, + // so these fields will be ignored for this test. + entry.stat = { + mode: entry.stat.mode, + size: entry.stat.size, + }; + + if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { + // Push the extended metadata header + const data = tarUtils.encodeExtendedHeader({ path: entry.path }); + blocks.push(generator.generateExtended(data.byteLength)); + + // Push the content block + for ( + let offset = 0; + offset < data.byteLength; + offset += tarConstants.BLOCK_SIZE + ) { + blocks.push( + generator.generateData( + data.subarray(offset, offset + tarConstants.BLOCK_SIZE), + ), + ); } - break; } - case 'directory': { - // Generate the header - entry = entry as VirtualDirectory; - blocks.push(generator.generateDirectory(filePath, entry.stat)); + const filePath = + entry.path.length <= tarConstants.STANDARD_PATH_SIZE + ? entry.path + : ''; + + switch (entry.type) { + case 'file': { + // Generate the header + entry = entry as VirtualFile; + blocks.push(generator.generateFile(filePath, entry.stat)); + + // Generate the data + const encoder = new TextEncoder(); + let content = entry.content; + while (content.length > 0) { + const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); + blocks.push(generator.generateData(encoder.encode(dataChunk))); + content = content.slice(tarConstants.BLOCK_SIZE); + } + break; + } + + case 'directory': { + // Generate the header + entry = entry as VirtualDirectory; + blocks.push(generator.generateDirectory(filePath, entry.stat)); - // Perform the same operation on all children - for (const file of entry.children) { - generateEntry(file); + // Perform the same operation on all children + for (const file of entry.children) { + generateEntry(file); + } + break; } - break; + + default: + throw new Error('Invalid type'); } + }; - default: - throw new Error('Invalid type'); - } - }; - - for (const entry of vfs) generateEntry(entry); - blocks.push(generator.generateEnd()); - blocks.push(generator.generateEnd()); - - // Write the archive to fs - const archivePath = path.join(tempDir, 'archive.tar'); - const tarFile = await fs.promises.open(archivePath, 'w+'); - for (const block of blocks) await tarFile.write(block); - await tarFile.close(); - - const vfsPath = path.join(tempDir, 'vfs'); - await fs.promises.mkdir(vfsPath, { recursive: true }); - await tar.extract({ - file: archivePath, - cwd: vfsPath, - preservePaths: true, - }); - - // Reconstruct the vfs and compare the contents to actual vfs - const traverse = async (currentPath: string) => { - const entries = await fs.promises.readdir(currentPath, { - withFileTypes: true, + for (const entry of vfs) generateEntry(entry); + blocks.push(generator.generateEnd()); + blocks.push(generator.generateEnd()); + + // Write the archive to fs + const archivePath = path.join(tempDir, 'archive.tar'); + const tarFile = await fs.promises.open(archivePath, 'w+'); + for (const block of blocks) await tarFile.write(block); + await tarFile.close(); + + const vfsPath = path.join(tempDir, 'vfs'); + await fs.promises.mkdir(vfsPath, { recursive: true }); + await tar.extract({ + file: archivePath, + cwd: vfsPath, + preservePaths: true, }); - const vfsEntries: Array = []; - - for (const entry of entries) { - const fullPath = path.join(currentPath, entry.name); - const relativePath = path.relative(vfsPath, fullPath); - const stats = await fs.promises.stat(fullPath); - - if (entry.isDirectory()) { - // Sometimes, the size of a directory on disk might not be 0 bytes - // due to the storage of additional metadata. This is different from - // the way tar stores directories, so the size is being manually set. - const entry: VirtualDirectory = { - type: 'directory', - path: relativePath + '/', - children: await traverse(fullPath), - stat: { size: 0, mode: stats.mode }, - }; - vfsEntries.push(entry); - } else { - const content = await fs.promises.readFile(fullPath); - const entry: VirtualFile = { - type: 'file', - path: relativePath, - content: content.toString(), - stat: { size: stats.size, mode: stats.mode }, - }; - vfsEntries.push(entry); + + // Reconstruct the vfs and compare the contents to actual vfs + const traverse = async (currentPath: string) => { + const entries = await fs.promises.readdir(currentPath, { + withFileTypes: true, + }); + const vfsEntries: Array = []; + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + const relativePath = path.relative(vfsPath, fullPath); + const stats = await fs.promises.stat(fullPath); + + if (entry.isDirectory()) { + // Sometimes, the size of a directory on disk might not be 0 bytes + // due to the storage of additional metadata. This is different from + // the way tar stores directories, so the size is being manually set. + const entry: VirtualDirectory = { + type: 'directory', + path: relativePath + '/', + children: await traverse(fullPath), + stat: { size: 0, mode: stats.mode }, + }; + vfsEntries.push(entry); + } else { + const content = await fs.promises.readFile(fullPath); + const entry: VirtualFile = { + type: 'file', + path: relativePath, + content: content.toString(), + stat: { size: stats.size, mode: stats.mode }, + }; + vfsEntries.push(entry); + } } - } - return vfsEntries; - }; + return vfsEntries; + }; - const reconstructedVfs = await traverse(vfsPath); - expect(utils.deepSort(reconstructedVfs)).toEqual(utils.deepSort(vfs)); - } finally { - await fs.promises.rm(tempDir, { force: true, recursive: true }); - } - }); + const reconstructedVfs = await traverse(vfsPath); + expect(utils.deepSort(reconstructedVfs)).toEqual(utils.deepSort(vfs)); + } finally { + await fs.promises.rm(tempDir, { force: true, recursive: true }); + } + }, + ); }); diff --git a/tests/Parser.test.ts b/tests/Parser.test.ts index 5aee96c..229fcb9 100644 --- a/tests/Parser.test.ts +++ b/tests/Parser.test.ts @@ -1,5 +1,5 @@ import type { VirtualFile, VirtualDirectory } from './types'; -import type { ExtendedHeaderKeywords } from '@/types'; +import type { MetadataKeywords } from '@/types'; import fs from 'fs'; import path from 'path'; import os from 'os'; @@ -7,7 +7,7 @@ import { test } from '@fast-check/jest'; import fc from 'fast-check'; import * as tar from 'tar'; import Parser from '@/Parser'; -import { EntryType, HeaderOffset, ParserState } from '@/types'; +import { HeaderOffset, ParserState } from '@/types'; import * as tarErrors from '@/errors'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; @@ -143,9 +143,12 @@ describe('parsing archive blocks', () => { }); describe('parsing extended metadata', () => { - test.prop([utils.tarEntryArb({ minFilePathSize: 256, maxFilePathSize: 512 })], { - numRuns: 1, - })('should create pax header with long paths', ({ headers }) => { + test.prop( + [utils.tarEntryArb({ minFilePathSize: 256, maxFilePathSize: 512 })], + { + numRuns: 1, + }, + )('should create pax header with long paths', ({ headers }) => { const parser = new Parser(); const token = parser.write(headers[0]); expect(token?.type).toEqual('header'); @@ -153,9 +156,12 @@ describe('parsing extended metadata', () => { expect(parser.state).toEqual(ParserState.DATA); }); - test.prop([utils.tarEntryArb({ minFilePathSize: 256, maxFilePathSize: 512 })], { - numRuns: 1, - })('should retrieve full file path from pax header', ({ headers, data }) => { + test.prop( + [utils.tarEntryArb({ minFilePathSize: 256, maxFilePathSize: 512 })], + { + numRuns: 1, + }, + )('should retrieve full file path from pax header', ({ headers, data }) => { // Get the header size const parser = new Parser(); const paxHeader = parser.write(headers[0]); @@ -188,7 +194,7 @@ describe('parsing extended metadata', () => { }); describe('testing against tar', () => { - test.skip.prop([utils.virtualFsArb], { numRuns: 1 })( + test.skip.prop([utils.fileTreeArb], { numRuns: 1 })( 'should match output of tar', async (vfs) => { // Create a temp directory to use for node-tar @@ -258,7 +264,7 @@ describe('testing against tar', () => { case 'header': { let parsedEntry: VirtualFile | VirtualDirectory | undefined; let extendedMetadata: - | Partial> + | Partial> | undefined; if (extendedData != null) { extendedMetadata = tarUtils.decodeExtendedHeader(extendedData); @@ -312,7 +318,9 @@ describe('testing against tar', () => { } else { // It is guaranteed that in a valid tar file, the parent will // always exist. - const parent: VirtualDirectory = pathStack.get(parentPath + '/'); + const parent: VirtualDirectory = pathStack.get( + parentPath + '/', + ); parent.children.push(parsedEntry); } diff --git a/tests/index.test.ts b/tests/index.test.ts index 09425b5..86fdb18 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -15,9 +15,9 @@ test('test', async () => { const fs = await import('fs'); const generator = new tar.Generator(); const fd = await fs.promises.open('./tmp/test.tar', 'w+'); - await fd.write(generator.generateFile('abc/def/file.txt', {size: 3})); + await fd.write(generator.generateFile('abc/def/file.txt', { size: 3 })); await fd.write(generator.generateData(Buffer.from('123'))); await fd.write(generator.generateEnd()); await fd.write(generator.generateEnd()); await fd.close(); -}); \ No newline at end of file +}); diff --git a/tests/integration.test.ts b/tests/integration.test.ts index a31e589..3b64a23 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -1,15 +1,15 @@ import type { VirtualFile, VirtualDirectory } from './types'; +import type { MetadataKeywords } from '@/types'; import path from 'path'; import { test } from '@fast-check/jest'; import Generator from '@/Generator'; import Parser from '@/Parser'; -import { EntryType, ExtendedHeaderKeywords } from '@/types'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; import * as utils from './utils'; describe('integration testing', () => { - test.skip.prop([utils.virtualFsArb])( + test.skip.prop([utils.fileTreeArb])( 'should archive and unarchive a virtual file system', (vfs) => { const generator = new Generator(); @@ -18,9 +18,7 @@ describe('integration testing', () => { const generateArchive = (entry: VirtualFile | VirtualDirectory) => { if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { // Push the extended metadata header - const data = tarUtils.encodeExtendedHeader({ - [ExtendedHeaderKeywords.FILE_PATH]: entry.path, - }); + const data = tarUtils.encodeExtendedHeader({ path: entry.path }); blocks.push(generator.generateExtended(data.byteLength)); // Push the content @@ -99,7 +97,7 @@ describe('integration testing', () => { case 'header': { let parsedEntry: VirtualFile | VirtualDirectory | undefined; let extendedMetadata: - | Partial> + | Partial> | undefined; if (extendedData != null) { extendedMetadata = tarUtils.decodeExtendedHeader(extendedData); diff --git a/tests/types.ts b/tests/types.ts index c2f98f4..ec8d2a7 100644 --- a/tests/types.ts +++ b/tests/types.ts @@ -19,4 +19,4 @@ type VirtualMetadata = { size: number; }; -export type { VirtualFile, VirtualDirectory, VirtualMetadata } +export type { VirtualFile, VirtualDirectory, VirtualMetadata }; diff --git a/tests/utils/fastcheck.ts b/tests/utils/fastcheck.ts index f3bd8f0..5b7061e 100644 --- a/tests/utils/fastcheck.ts +++ b/tests/utils/fastcheck.ts @@ -1,6 +1,5 @@ import type { VirtualFile, VirtualDirectory } from '../types'; import type { FileStat } from '@/types'; -import { EntryType, HeaderSize, HeaderOffset } from '@/types'; import fc from 'fast-check'; import * as tarConstants from '@/constants'; import * as tarUtils from '@/utils'; @@ -10,7 +9,7 @@ import * as tarUtils from '@/utils'; * this is only used for testing virtual file system and not for any tests which * interact with the physical file system. */ -const modeArb = (type?: 'file' | 'directory') => { +const modeArb = (type?: 'file' | 'directory'): fc.Arbitrary => { switch (type) { case 'file': return fc.constant(0o100755); @@ -21,9 +20,12 @@ const modeArb = (type?: 'file' | 'directory') => { } }; -const uidgidArb = fc.integer({ min: 1000, max: 4096 }); +const uidgidArb: fc.Arbitrary = fc.integer({ min: 1000, max: 4096 }); -const sizeArb = (type?: 'file' | 'directory', content?: string) => { +const sizeArb = ( + type?: 'file' | 'directory', + content?: string, +): fc.Arbitrary => { if (type === 'file') { if (content == null) throw new Error('Files must have content'); return fc.constant(content.length); @@ -32,7 +34,7 @@ const sizeArb = (type?: 'file' | 'directory', content?: string) => { }; // Produce valid dates which snap to whole seconds -const mtimeArb = fc +const mtimeArb: fc.Arbitrary = fc .date({ min: new Date(0), max: new Date(0o77777777777 * 1000), @@ -64,7 +66,7 @@ const filenameArb = ( minLength: 1, maxLength: 512, }, -) => { +): fc.Arbitrary => { // Most of these characters are disallowed by windows const restrictedCharacters = '/\\*?"<>|:'; const filterRegex = /^(\.|\.\.|con|prn|aux|nul|tty|null|zero|full)$/i; @@ -96,7 +98,7 @@ const filenameArb = ( return filteredFileName.noShrink(); }; -const fileContentArb = (maxLength: number = 4096) => +const fileContentArb = (maxLength: number = 4096): fc.Arbitrary => fc.string({ minLength: 0, maxLength, unit: 'binary-ascii' }).noShrink(); const statDataArb = ( @@ -212,7 +214,7 @@ const dirArb = ( * Uses arbitraries generating files and directories to create a virtual file * system as a JSON object. */ -const virtualFsArb: fc.Arbitrary> = fc +const fileTreeArb: fc.Arbitrary> = fc .array(fc.oneof(fileArb(), dirArb(5)), { minLength: 1, maxLength: 10, @@ -225,7 +227,10 @@ const tarEntryArb = ({ }: { minFilePathSize?: number; maxFilePathSize?: number; -} = {}) => { +} = {}): fc.Arbitrary<{ + headers: Array; + data: VirtualFile | VirtualDirectory; +}> => { const data = fc.oneof( fileArb(undefined, undefined, { minFilePathSize, maxFilePathSize }), dirArb(0, undefined, { minFilePathSize, maxFilePathSize }), @@ -301,6 +306,6 @@ export { statDataArb, fileArb, dirArb, - virtualFsArb, + fileTreeArb, tarEntryArb, }; diff --git a/tests/utils/index.ts b/tests/utils/index.ts index 28700db..e54c7b6 100644 --- a/tests/utils/index.ts +++ b/tests/utils/index.ts @@ -1,2 +1,2 @@ export * from './fastcheck'; -export * from './utils'; \ No newline at end of file +export * from './utils'; From cf3dd6af569466c0d8f45941d264e4ac4b04fa6d Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Thu, 13 Mar 2025 17:11:45 +1100 Subject: [PATCH 14/19] chore: addressed feedback on VirtualTar api --- src/VirtualTar.ts | 446 +++++++++++++++++++++++++++------------------- src/types.ts | 1 - 2 files changed, 260 insertions(+), 187 deletions(-) diff --git a/src/VirtualTar.ts b/src/VirtualTar.ts index 5938e9d..f43ed39 100644 --- a/src/VirtualTar.ts +++ b/src/VirtualTar.ts @@ -5,6 +5,7 @@ import type { ParsedMetadata, ParsedEmpty, MetadataKeywords, + TokenData, } from './types'; import { VirtualTarState } from './types'; import Generator from './Generator'; @@ -14,31 +15,73 @@ import * as errors from './errors'; import * as utils from './utils'; class VirtualTar { + protected ended: boolean; protected state: VirtualTarState; protected generator: Generator; protected parser: Parser; - protected chunks: Array; + protected queue: Array<() => AsyncGenerator>; protected encoder = new TextEncoder(); - protected accumulator: Uint8Array; + protected workingAccumulator: Uint8Array; protected workingToken: ParsedFile | ParsedMetadata | undefined; - protected workingData: Array; + protected workingData: Array; + protected workingDataQueue: Array; protected workingMetadata: | Partial> | undefined; + protected resolveWaitP: (() => void) | undefined; + protected resolveWaitDataP: (() => void) | undefined; + protected settledP: Promise | undefined; + protected resolveSettledP: (() => void) | undefined; + protected fileCallback: ( + header: ParsedFile, + data: () => AsyncGenerator, + ) => Promise; + protected directoryCallback: (header: ParsedDirectory) => Promise; + protected endCallback: () => void; - protected addEntry( + constructor({ + mode, + onFile, + onDirectory, + onEnd, + }: { + mode: 'generate' | 'parse'; + onFile?: ( + header: ParsedFile, + data: () => AsyncGenerator, + ) => Promise; + onDirectory?: (header: ParsedDirectory) => Promise; + onEnd?: () => void; + }) { + if (mode === 'generate') { + if (onFile != null || onDirectory != null || onEnd != null) { + throw new errors.ErrorVirtualTar( + 'VirtualTar in generate mode does not support callbacks', + ); + } + this.state = VirtualTarState.GENERATOR; + this.generator = new Generator(); + this.queue = []; + } else { + this.state = VirtualTarState.PARSER; + this.parser = new Parser(); + this.workingData = []; + this.workingDataQueue = []; + this.directoryCallback = onDirectory ?? (() => Promise.resolve()); + this.fileCallback = onFile ?? (() => Promise.resolve()); + this.endCallback = onEnd ?? (() => {}); + } + } + + protected async *generateHeader( filePath: string, - type: 'file' | 'directory', stat: FileStat = {}, - dataOrCallback?: - | Uint8Array - | string - | ((write: (chunk: string | Uint8Array) => void) => void), - ): void { + type: 'file' | 'directory', + ): AsyncGenerator { if (filePath.length > constants.STANDARD_PATH_SIZE) { // Push the extended metadata header const data = utils.encodeExtendedHeader({ path: filePath }); - this.chunks.push(this.generator.generateExtended(data.byteLength)); + yield this.generator.generateExtended(data.byteLength); // Push the content for ( @@ -46,10 +89,8 @@ class VirtualTar { offset < data.byteLength; offset += constants.BLOCK_SIZE ) { - this.chunks.push( - this.generator.generateData( - data.subarray(offset, offset + constants.BLOCK_SIZE), - ), + yield this.generator.generateData( + data.subarray(offset, offset + constants.BLOCK_SIZE), ); } } @@ -58,89 +99,93 @@ class VirtualTar { // Generate the header if (type === 'file') { - this.chunks.push(this.generator.generateFile(filePath, stat)); - } else { - this.chunks.push(this.generator.generateDirectory(filePath, stat)); - } - - // Generate the data - if (dataOrCallback == null) return; - - const writeData = (data: string | Uint8Array) => { - if (data instanceof Uint8Array) { - for ( - let offset = 0; - offset < data.byteLength; - offset += constants.BLOCK_SIZE - ) { - const chunk = data.slice(offset, offset + constants.BLOCK_SIZE); - this.chunks.push(this.generator.generateData(chunk)); - } - } else { - while (data.length > 0) { - const chunk = this.encoder.encode( - data.slice(0, constants.BLOCK_SIZE), - ); - this.chunks.push(this.generator.generateData(chunk)); - data = data.slice(constants.BLOCK_SIZE); - } - } - }; - - if (typeof dataOrCallback === 'function') { - const data: Array = []; - const writer = (chunk: string | Uint8Array) => { - if (chunk instanceof Uint8Array) data.push(chunk); - else data.push(this.encoder.encode(chunk)); - }; - dataOrCallback(writer); - writeData(utils.concatUint8Arrays(...data)); + yield this.generator.generateFile(filePath, stat); } else { - writeData(dataOrCallback); - } - } - - constructor({ mode }: { mode: 'generate' | 'parse' } = { mode: 'parse' }) { - if (mode === 'generate') { - this.state = VirtualTarState.GENERATOR; - this.generator = new Generator(); - this.chunks = []; - } else { - this.state = VirtualTarState.PARSER; - this.parser = new Parser(); - this.workingData = []; + yield this.generator.generateDirectory(filePath, stat); } } public addFile( filePath: string, stat: FileStat, - data?: Uint8Array | string, + data: () => AsyncGenerator, ): void; - - public addFile( - filePath: string, - stat: FileStat, - data?: - | Uint8Array - | string - | ((writer: (chunk: string | Uint8Array) => void) => void), - ): void; - + public addFile(filePath: string, stat: FileStat, data: Uint8Array): void; + public addFile(filePath: string, stat: FileStat, data: string): void; public addFile( filePath: string, stat: FileStat, - data?: + data: | Uint8Array | string - | ((writer: (chunk: string | Uint8Array) => void) => void), + | (() => AsyncGenerator), ): void { if (this.state !== VirtualTarState.GENERATOR) { throw new errors.ErrorVirtualTarInvalidState( 'VirtualTar is not in generator mode', ); } - this.addEntry(filePath, 'file', stat, data); + + const globalThis = this; + this.queue.push(async function* () { + // Generate the header chunks (including extended header) + yield* globalThis.generateHeader(filePath, stat, 'file'); + if (globalThis.resolveWaitP != null) { + globalThis.resolveWaitP(); + globalThis.resolveWaitP = undefined; + } + + // The base case of generating data is to have a async generator yielding + // data, but in case the data is passed as an entire buffer or a string, + // we need to chunk it up and wrap it in the async generator. + let gen: AsyncGenerator; + if (typeof data === 'function') { + // Ensure the data is properly converted into Uint8Arrays + gen = (async function* () { + for await (const chunk of data()) { + if (typeof chunk === 'string') { + yield globalThis.encoder.encode(chunk); + } else { + yield chunk; + } + if (globalThis.resolveWaitP != null) { + globalThis.resolveWaitP(); + globalThis.resolveWaitP = undefined; + } + } + })(); + } else { + // Ensure that the data is being chunked up to 512 bytes + gen = (async function* () { + if (data instanceof Uint8Array) { + for ( + let offset = 0; + offset < data.byteLength; + offset += constants.BLOCK_SIZE + ) { + const chunk = data.slice(offset, offset + constants.BLOCK_SIZE); + yield globalThis.generator.generateData(chunk); + if (globalThis.resolveWaitP != null) { + globalThis.resolveWaitP(); + globalThis.resolveWaitP = undefined; + } + } + } else { + while (data.length > 0) { + const chunk = this.encoder.encode( + data.slice(0, constants.BLOCK_SIZE), + ); + yield globalThis.generator.generateData(chunk); + data = data.slice(constants.BLOCK_SIZE); + if (globalThis.resolveWaitP != null) { + globalThis.resolveWaitP(); + globalThis.resolveWaitP = undefined; + } + } + } + })(); + } + }); } public addDirectory(filePath: string, stat?: FileStat): void { @@ -149,142 +194,171 @@ class VirtualTar { 'VirtualTar is not in generator mode', ); } - this.addEntry(filePath, 'directory', stat); + + const globalThis = this; + this.queue.push(async function* () { + yield* globalThis.generateHeader(filePath, stat, 'directory'); + }); } - public finalize(): Uint8Array { + public finalize(): void { if (this.state !== VirtualTarState.GENERATOR) { throw new errors.ErrorVirtualTarInvalidState( 'VirtualTar is not in generator mode', ); } - this.chunks.push(this.generator.generateEnd()); - this.chunks.push(this.generator.generateEnd()); - return utils.concatUint8Arrays(...this.chunks); + this.queue.push(async function* () { + yield globalThis.generator.generateEnd(); + yield globalThis.generator.generateEnd(); + }); + this.ended = true; } - public push(chunk: Uint8Array): void { - if (this.state !== VirtualTarState.PARSER) { - throw new errors.ErrorVirtualTarInvalidState( - 'VirtualTar is not in parser mode', - ); + public async settled(): Promise { + this.settledP = new Promise((resolve) => { + this.resolveSettledP = resolve; + }); + await this.settledP; + } + + public async *yieldChunks(): AsyncGenerator { + while (true) { + const gen = this.queue.shift(); + if (gen == null) { + // We have gone through all the buffered tasks. Check if we have ended + // yet or we are still going. + if (this.ended) break; + if (this.resolveSettledP != null) this.resolveSettledP(); + + // Wait until more data is available + const waitP = new Promise((resolve) => { + this.resolveWaitP = resolve; + }); + await waitP; + continue; + } + + yield* gen(); } - this.accumulator = utils.concatUint8Arrays(this.accumulator, chunk); } - public next(): ParsedFile | ParsedDirectory | ParsedEmpty { + public write(chunk: Uint8Array): void { if (this.state !== VirtualTarState.PARSER) { throw new errors.ErrorVirtualTarInvalidState( 'VirtualTar is not in parser mode', ); } - if (this.accumulator.byteLength < constants.BLOCK_SIZE) { - return { type: 'empty', awaitingData: true }; - } - const chunk = this.accumulator.slice(0, constants.BLOCK_SIZE); - this.accumulator = this.accumulator.slice(constants.BLOCK_SIZE); - const token = this.parser.write(chunk); + // Update the working accumulator + this.workingAccumulator = utils.concatUint8Arrays( + this.workingAccumulator, + chunk, + ); - if (token == null) { - return { type: 'empty', awaitingData: false }; - } + while (this.workingAccumulator.byteLength >= constants.BLOCK_SIZE) { + const block = this.workingAccumulator.slice(0, constants.BLOCK_SIZE); + this.workingAccumulator = this.workingAccumulator.slice( + constants.BLOCK_SIZE, + ); + const token = this.parser.write(block); + if (token == null) continue; - if (token.type === 'header') { - if (token.fileType === 'metadata') { - this.workingToken = { type: 'metadata' }; - return { type: 'empty', awaitingData: false }; - } + if (token.type === 'header') { + // If we have an extended header, then set the working header to the + // extended type and continue + if (token.fileType === 'metadata') { + this.workingToken = { type: 'metadata' }; + continue; + } - // If we have additional metadata, then use it to override token data - let filePath = token.filePath; - if (this.workingMetadata != null) { - filePath = this.workingMetadata.path ?? filePath; - this.workingMetadata = undefined; - } + // If we have additional metadata, then use it to override token data + let filePath = token.filePath; + if (this.workingMetadata != null) { + filePath = this.workingMetadata.path ?? filePath; + this.workingMetadata = undefined; + } - if (token.fileType === 'directory') { - return { - type: 'directory', - path: filePath, - stat: { - size: token.fileSize, - mode: token.fileMode, - mtime: token.fileMtime, - uid: token.ownerUid, - gid: token.ownerGid, - uname: token.ownerUserName, - gname: token.ownerGroupName, - }, - }; - } else if (token.fileType === 'file') { - this.workingToken = { - type: 'file', - path: filePath, - stat: { - size: token.fileSize, - mode: token.fileMode, - mtime: token.fileMtime, - uid: token.ownerUid, - gid: token.ownerGid, - uname: token.ownerUserName, - gname: token.ownerGroupName, - }, - content: new Uint8Array(token.fileSize), - }; - } - } else { - if (this.workingToken == null) { - throw new errors.ErrorVirtualTarInvalidState( - 'Received data token before header token', - ); - } - if (token.type === 'end') { - throw new errors.ErrorVirtualTarInvalidState( - 'Received end token before header token', - ); - } + if (token.fileType === 'directory') { + this.directoryCallback({ + type: 'directory', + path: filePath, + stat: { + size: token.fileSize, + mode: token.fileMode, + mtime: token.fileMtime, + uid: token.ownerUid, + gid: token.ownerGid, + uname: token.ownerUserName, + gname: token.ownerGroupName, + }, + }); + continue; + } else if (token.fileType === 'file') { + this.fileCallback( + { + type: 'file', + path: filePath, + stat: { + size: token.fileSize, + mode: token.fileMode, + mtime: token.fileMtime, + uid: token.ownerUid, + gid: token.ownerGid, + uname: token.ownerUserName, + gname: token.ownerGroupName, + }, + }, + async function* (): AsyncGenerator { + while (true) { + const chunk = this.workingData.shift(); + if (chunk == null) { + await new Promise((resolve) => { + this.resolveWaitDataP = resolve; + }); + } + yield chunk.data; + if (chunk.ended) break; + } + }, + ); + continue; + } + } else if (token.type === 'data') { + if (this.workingToken == null) { + throw new errors.ErrorVirtualTarInvalidState( + 'Received data token before header token', + ); + } - // Token is of type 'data' after this - const { data, end } = token; - this.workingData.push(data); + this.workingData.push(token); - if (end) { - // Concat the working data into a single Uint8Array - const data = utils.concatUint8Arrays(...this.workingData); - this.workingData = []; + // If we are working on a file, then signal that we have gotten more + // data. + if ( + this.workingToken.type === 'file' && + this.resolveWaitDataP != null + ) { + this.resolveWaitDataP(); + } - // If the current working token is a metadata token, then decode the - // accumulated header. Otherwise, we have obtained all the data for - // a file. Set the content of the file then return it. - if (this.workingToken.type === 'metadata') { + // If we are working on a metadata token, then we need to collect the + // entire data array as we need to decode it to file stat which needs to + // sit in memory anyways. + if (token.end && this.workingToken.type === 'metadata') { + // Concat the working data into a single Uint8Array + const data = utils.concatUint8Arrays( + ...this.workingData.map(({ data }) => data), + ); + this.workingData = []; + + // Decode the extended header this.workingMetadata = utils.decodeExtendedHeader(data); - return { type: 'empty', awaitingData: false }; - } else if (this.workingToken.type === 'file') { - this.workingToken.content.set(data); - const fileToken = this.workingToken; - this.workingToken = undefined; - return fileToken; } + } else { + // Token is of type end + this.endCallback(); } } - return { type: 'empty', awaitingData: false }; - } - - public parseAvailable(): Array { - if (this.state !== VirtualTarState.PARSER) { - throw new errors.ErrorVirtualTarInvalidState( - 'VirtualTar is not in parser mode', - ); - } - - const parsedTokens: Array = []; - let token; - while (token.type !== 'empty' && !token.awaitingData) { - token = this.next(); - parsedTokens.push(token); - } - return parsedTokens; } } diff --git a/src/types.ts b/src/types.ts index f9325c4..95f06bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -105,7 +105,6 @@ type ParsedFile = { type: 'file'; path: string; stat: FileStat; - content: Uint8Array; }; type ParsedDirectory = { From ab47bc13f587997c2d63a55899b00c222f955110 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Fri, 14 Mar 2025 15:38:57 +1100 Subject: [PATCH 15/19] chore: added tests for VirtualTar --- src/Generator.ts | 10 +- src/Parser.ts | 93 ++--------- src/VirtualTar.ts | 102 ++++++++---- src/types.ts | 9 +- src/utils.ts | 339 ++++++++++++++++++++++++-------------- tests/Generator.test.ts | 183 +++++++------------- tests/Parser.test.ts | 149 ++++++++--------- tests/VirtualTar.test.ts | 246 +++++++++++++++++++++++++++ tests/integration.test.ts | 204 +++++++++++------------ tests/types.ts | 1 - tests/utils/fastcheck.ts | 102 ++++++------ 11 files changed, 826 insertions(+), 612 deletions(-) create mode 100644 tests/VirtualTar.test.ts diff --git a/src/Generator.ts b/src/Generator.ts index f64fa70..d488d51 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -1,8 +1,8 @@ import type { FileType, FileStat } from './types'; import { GeneratorState, HeaderSize } from './types'; +import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; -import * as constants from './constants'; /** * The TAR headers follow this structure: @@ -174,7 +174,7 @@ class Generator { return data; } else { // Make sure we don't attempt to write extra data - if (data.byteLength !== this.remainingBytes) { + if (data.byteLength > this.remainingBytes) { throw new errors.ErrorVirtualTarGeneratorBlockSize( `Expected data to be ${this.remainingBytes} bytes but received ${data.byteLength} bytes`, ); @@ -210,8 +210,10 @@ class Generator { this.state = GeneratorState.ENDED; break; default: - throw new errors.ErrorVirtualTarGeneratorEndOfArchive( - 'Exactly two null chunks should be generated consecutively to end archive', + throw new errors.ErrorVirtualTarGeneratorInvalidState( + `Expected state ${GeneratorState[GeneratorState.HEADER]} or ${ + GeneratorState[GeneratorState.NULL] + } but got ${GeneratorState[this.state]}`, ); } return new Uint8Array(constants.BLOCK_SIZE); diff --git a/src/Parser.ts b/src/Parser.ts index 10313d8..664fb86 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -1,6 +1,5 @@ import type { TokenHeader, TokenData, TokenEnd } from './types'; import { ParserState } from './types'; -import { HeaderOffset, HeaderSize, EntryType } from './types'; import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; @@ -11,11 +10,7 @@ class Parser { protected parseHeader(array: Uint8Array): TokenHeader { // Validate header by checking checksum and magic string - const headerChecksum = utils.extractOctal( - array, - HeaderOffset.CHECKSUM, - HeaderSize.CHECKSUM, - ); + const headerChecksum = utils.decodeChecksum(array); const calculatedChecksum = utils.calculateChecksum(array); if (headerChecksum !== calculatedChecksum) { @@ -24,22 +19,14 @@ class Parser { ); } - const ustarMagic = utils.extractString( - array, - HeaderOffset.USTAR_NAME, - HeaderSize.USTAR_NAME, - ); + const ustarMagic = utils.decodeUstarMagic(array); if (ustarMagic !== constants.USTAR_NAME) { throw new errors.ErrorVirtualTarParserInvalidHeader( `Expected ustar magic to be '${constants.USTAR_NAME}', got '${ustarMagic}'`, ); } - const ustarVersion = utils.extractString( - array, - HeaderOffset.USTAR_VERSION, - HeaderSize.USTAR_VERSION, - ); + const ustarVersion = utils.decodeUstarVersion(array); if (ustarVersion !== constants.USTAR_VERSION) { throw new errors.ErrorVirtualTarParserInvalidHeader( `Expected ustar version to be '${constants.USTAR_VERSION}', got '${ustarVersion}'`, @@ -48,69 +35,14 @@ class Parser { // Extract the relevant metadata from the header const filePath = utils.decodeFilePath(array); - const fileSize = utils.extractOctal( - array, - HeaderOffset.FILE_SIZE, - HeaderSize.FILE_SIZE, - ); - const fileMtime = new Date( - utils.extractOctal( - array, - HeaderOffset.FILE_MTIME, - HeaderSize.FILE_MTIME, - ) * 1000, - ); - const fileMode = utils.extractOctal( - array, - HeaderOffset.FILE_MODE, - HeaderSize.FILE_MODE, - ); - const ownerGid = utils.extractOctal( - array, - HeaderOffset.OWNER_GID, - HeaderSize.OWNER_GID, - ); - const ownerUid = utils.extractOctal( - array, - HeaderOffset.OWNER_UID, - HeaderSize.OWNER_UID, - ); - const ownerName = utils.extractString( - array, - HeaderOffset.LINK_NAME, - HeaderSize.LINK_NAME, - ); - const ownerGroupName = utils.extractString( - array, - HeaderOffset.OWNER_GROUPNAME, - HeaderSize.OWNER_GROUPNAME, - ); - const ownerUserName = utils.extractString( - array, - HeaderOffset.OWNER_USERNAME, - HeaderSize.OWNER_USERNAME, - ); - let fileType: 'file' | 'directory' | 'metadata'; - const type = utils.extractString( - array, - HeaderOffset.TYPE_FLAG, - HeaderSize.TYPE_FLAG, - ); - switch (type) { - case EntryType.FILE: - fileType = 'file'; - break; - case EntryType.DIRECTORY: - fileType = 'directory'; - break; - case EntryType.EXTENDED: - fileType = 'metadata'; - break; - default: - throw new errors.ErrorVirtualTarParserInvalidHeader( - `Got invalid file type ${type}`, - ); - } + const fileSize = utils.decodeFileSize(array); + const fileMtime = utils.decodeFileMtime(array); + const fileMode = utils.decodeFileMode(array); + const ownerUid = utils.decodeOwnerUid(array); + const ownerGid = utils.decodeOwnerGid(array); + const ownerUserName = utils.decodeOwnerUserName(array); + const ownerGroupName = utils.decodeOwnerGroupName(array); + const fileType = utils.decodeFileType(array); return { type: 'header', @@ -121,7 +53,6 @@ class Parser { fileSize, ownerGid, ownerUid, - ownerName, ownerUserName, ownerGroupName, }; @@ -165,7 +96,7 @@ class Parser { this.state = ParserState.DATA; this.remainingBytes = headerToken.fileSize; } - } else if (headerToken.fileType === 'metadata') { + } else if (headerToken.fileType === 'extended') { // A header might not have any data but a metadata header will always // be followed by data. this.state = ParserState.DATA; diff --git a/src/VirtualTar.ts b/src/VirtualTar.ts index f43ed39..d94b309 100644 --- a/src/VirtualTar.ts +++ b/src/VirtualTar.ts @@ -2,8 +2,6 @@ import type { FileStat, ParsedFile, ParsedDirectory, - ParsedMetadata, - ParsedEmpty, MetadataKeywords, TokenData, } from './types'; @@ -22,7 +20,7 @@ class VirtualTar { protected queue: Array<() => AsyncGenerator>; protected encoder = new TextEncoder(); protected workingAccumulator: Uint8Array; - protected workingToken: ParsedFile | ParsedMetadata | undefined; + protected workingTokenType: 'file' | 'extended' | undefined; protected workingData: Array; protected workingDataQueue: Array; protected workingMetadata: @@ -38,6 +36,7 @@ class VirtualTar { ) => Promise; protected directoryCallback: (header: ParsedDirectory) => Promise; protected endCallback: () => void; + protected callbacks: Array>; constructor({ mode, @@ -67,6 +66,8 @@ class VirtualTar { this.parser = new Parser(); this.workingData = []; this.workingDataQueue = []; + this.workingAccumulator = new Uint8Array(); + this.callbacks = []; this.directoryCallback = onDirectory ?? (() => Promise.resolve()); this.fileCallback = onFile ?? (() => Promise.resolve()); this.endCallback = onEnd ?? (() => {}); @@ -140,19 +141,46 @@ class VirtualTar { // we need to chunk it up and wrap it in the async generator. let gen: AsyncGenerator; if (typeof data === 'function') { + let workingBuffer: Array = []; + let bufferSize = 0; + // Ensure the data is properly converted into Uint8Arrays gen = (async function* () { for await (const chunk of data()) { + let chunkBytes: Uint8Array; if (typeof chunk === 'string') { - yield globalThis.encoder.encode(chunk); + chunkBytes = globalThis.encoder.encode(chunk); } else { - yield chunk; + chunkBytes = chunk; } - if (globalThis.resolveWaitP != null) { - globalThis.resolveWaitP(); - globalThis.resolveWaitP = undefined; + workingBuffer.push(chunkBytes); + bufferSize += chunkBytes.byteLength; + + while (bufferSize >= constants.BLOCK_SIZE) { + // Flatten buffer into one Uint8Array + const fullBuffer = utils.concatUint8Arrays(...workingBuffer); + + yield globalThis.generator.generateData( + fullBuffer.slice(0, constants.BLOCK_SIZE), + ); + + // Remove processed bytes from buffer + const remaining = fullBuffer.slice(constants.BLOCK_SIZE); + workingBuffer = []; + if (remaining.byteLength > 0) workingBuffer.push(remaining); + bufferSize = remaining.byteLength; + + if (globalThis.resolveWaitP != null) { + globalThis.resolveWaitP(); + globalThis.resolveWaitP = undefined; + } } } + if (bufferSize !== 0) { + yield globalThis.generator.generateData( + utils.concatUint8Arrays(...workingBuffer), + ); + } })(); } else { // Ensure that the data is being chunked up to 512 bytes @@ -163,7 +191,10 @@ class VirtualTar { offset < data.byteLength; offset += constants.BLOCK_SIZE ) { - const chunk = data.slice(offset, offset + constants.BLOCK_SIZE); + const chunk = data.subarray( + offset, + offset + constants.BLOCK_SIZE, + ); yield globalThis.generator.generateData(chunk); if (globalThis.resolveWaitP != null) { globalThis.resolveWaitP(); @@ -172,7 +203,7 @@ class VirtualTar { } } else { while (data.length > 0) { - const chunk = this.encoder.encode( + const chunk = globalThis.encoder.encode( data.slice(0, constants.BLOCK_SIZE), ); yield globalThis.generator.generateData(chunk); @@ -185,6 +216,7 @@ class VirtualTar { } })(); } + yield* gen; }); } @@ -207,6 +239,7 @@ class VirtualTar { 'VirtualTar is not in generator mode', ); } + const globalThis = this; this.queue.push(async function* () { yield globalThis.generator.generateEnd(); yield globalThis.generator.generateEnd(); @@ -215,10 +248,14 @@ class VirtualTar { } public async settled(): Promise { - this.settledP = new Promise((resolve) => { - this.resolveSettledP = resolve; - }); - await this.settledP; + if (this.state === VirtualTarState.GENERATOR) { + this.settledP = new Promise((resolve) => { + this.resolveSettledP = resolve; + }); + await this.settledP; + } else { + await Promise.allSettled(this.callbacks); + } } public async *yieldChunks(): AsyncGenerator { @@ -265,10 +302,15 @@ class VirtualTar { if (token.type === 'header') { // If we have an extended header, then set the working header to the - // extended type and continue - if (token.fileType === 'metadata') { - this.workingToken = { type: 'metadata' }; + // extended type and continue. Otherwise, if we have a file, then set + // the token type to file. + if (token.fileType === 'extended') { + this.workingTokenType = 'extended'; continue; + } else if (token.fileType === 'file') { + this.workingTokenType = 'file'; + } else { + this.workingTokenType = undefined; } // If we have additional metadata, then use it to override token data @@ -279,7 +321,7 @@ class VirtualTar { } if (token.fileType === 'directory') { - this.directoryCallback({ + const p = this.directoryCallback({ type: 'directory', path: filePath, stat: { @@ -292,9 +334,11 @@ class VirtualTar { gname: token.ownerGroupName, }, }); + this.callbacks.push(p); continue; } else if (token.fileType === 'file') { - this.fileCallback( + const globalThis = this; + const p = this.fileCallback( { type: 'file', path: filePath, @@ -309,22 +353,27 @@ class VirtualTar { }, }, async function* (): AsyncGenerator { + // Return early if no data will be coming + if (token.fileSize === 0) return; + while (true) { - const chunk = this.workingData.shift(); + const chunk = globalThis.workingData.shift(); if (chunk == null) { await new Promise((resolve) => { - this.resolveWaitDataP = resolve; + globalThis.resolveWaitDataP = resolve; }); + continue; } yield chunk.data; - if (chunk.ended) break; + if (chunk.end) break; } }, ); + this.callbacks.push(p); continue; } } else if (token.type === 'data') { - if (this.workingToken == null) { + if (this.workingTokenType == null) { throw new errors.ErrorVirtualTarInvalidState( 'Received data token before header token', ); @@ -334,17 +383,14 @@ class VirtualTar { // If we are working on a file, then signal that we have gotten more // data. - if ( - this.workingToken.type === 'file' && - this.resolveWaitDataP != null - ) { + if (this.resolveWaitDataP != null) { this.resolveWaitDataP(); } // If we are working on a metadata token, then we need to collect the // entire data array as we need to decode it to file stat which needs to // sit in memory anyways. - if (token.end && this.workingToken.type === 'metadata') { + if (token.end && this.workingTokenType === 'extended') { // Concat the working data into a single Uint8Array const data = utils.concatUint8Arrays( ...this.workingData.map(({ data }) => data), diff --git a/src/types.ts b/src/types.ts index 95f06bc..e998eb3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,14 +60,13 @@ type FileStat = { type TokenHeader = { type: 'header'; - fileType: 'file' | 'directory' | 'metadata'; + fileType: FileType; filePath: string; fileMode: number; ownerUid: number; ownerGid: number; fileSize: number; fileMtime: Date; - ownerName: string; ownerUserName: string; ownerGroupName: string; }; @@ -113,8 +112,8 @@ type ParsedDirectory = { stat: FileStat; }; -type ParsedMetadata = { - type: 'metadata'; +type ParsedExtended = { + type: 'extended'; }; type ParsedEmpty = { @@ -130,7 +129,7 @@ export type { TokenEnd, ParsedFile, ParsedDirectory, - ParsedMetadata, + ParsedExtended, ParsedEmpty, }; diff --git a/src/utils.ts b/src/utils.ts index e663943..bd6f60c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,4 @@ +import type { FileType } from './types'; import { EntryType, MetadataKeywords, HeaderOffset, HeaderSize } from './types'; import * as errors from './errors'; import * as constants from './constants'; @@ -36,10 +37,14 @@ function calculateChecksum(array: Uint8Array): number { }); } -function dateToUnixTime(date: Date): number { +function dateToTarTime(date: Date): number { return Math.round(date.getTime() / 1000); } +function tarTimeToDate(time: number): Date { + return new Date(time * 1000); +} + // Returns a view of the array with the given offset and length. Note that the // returned value is a view and not a copy, so any modifications to the data // will affect the original data. @@ -93,26 +98,6 @@ function extractDecimal( return value.length > 0 ? parseInt(value, 10) : 0; } -function decodeFilePath(array: Uint8Array): string { - const fileNamePrefix = extractString( - array, - HeaderOffset.FILE_NAME_PREFIX, - HeaderSize.FILE_NAME_PREFIX, - ); - - const fileNameSuffix = extractString( - array, - HeaderOffset.FILE_NAME, - HeaderSize.FILE_NAME, - ); - - if (fileNamePrefix !== '') { - return fileNamePrefix + fileNameSuffix; - } else { - return fileNameSuffix; - } -} - function isNullBlock(array: Uint8Array): boolean { for (let i = 0; i < constants.BLOCK_SIZE; i++) { if (array[i] !== 0) return false; @@ -141,6 +126,102 @@ function writeBytesToArray( return i; } +function encodeExtendedHeader( + data: Partial>, +): Uint8Array { + const encoder = new TextEncoder(); + let totalByteSize = 0; + const entries: Array = []; + + // For extended PAX headers, the format of metadata is as follows: + // =\n + // Where is the total length of the line including the key-value + // pair, the separator \n character, the space between the size and + // the line, and the size characters itself. Note \n is written using two + // characters but it is a single ASCII byte. + for (const [key, value] of Object.entries(data)) { + let size = key.length + value.length + 3; // Initial guess (' ', =, \n) + size += size.toString().length; // Adjust for size itself + + const entry = `${size} ${key}=${value}\n`; + entries.push(entry); + + // Update the total byte length of the header with the entry's size + totalByteSize += size; + } + + // The entries are encoded later to reduce memory allocation + const output = new Uint8Array(totalByteSize); + let offset = 0; + + for (const entry of entries) { + // Older browsers and runtimes might return written as undefined. That is + // not a concern for us. + const { written } = encoder.encodeInto(entry, output.subarray(offset)); + if (!written) throw new Error('TMP not written'); + offset += written; + } + + return output; +} + +function decodeExtendedHeader( + array: Uint8Array, +): Partial> { + const decoder = new TextDecoder(); + const data: Partial> = {}; + + // Track offset and remaining bytes in the array + let offset = 0; + let remainingBytes = array.byteLength; + + while (remainingBytes > 0) { + const size = extractDecimal(array, offset, undefined, ' '); + const fullLine = decoder.decode(array.subarray(offset, offset + size)); + + const sizeSeparatorIndex = fullLine.indexOf(' '); + if (sizeSeparatorIndex === -1) { + throw new Error('TMP invalid ennnntry'); + } + const line = fullLine.substring(sizeSeparatorIndex + 1); + + const entrySeparatorIndex = line.indexOf('='); + if (entrySeparatorIndex === -1) { + throw new Error('TMP invalid ennnntry'); + } + const key = line.substring(0, entrySeparatorIndex); + const _value = line.substring(entrySeparatorIndex + 1); + + if (!Object.values(MetadataKeywords).includes(key as MetadataKeywords)) { + throw new Error('TMP key doesnt exist'); + } + + // Remove the trailing newline + const value = _value.substring(0, _value.length - 1); + switch (key as MetadataKeywords) { + case MetadataKeywords.FILE_PATH: { + data[MetadataKeywords.FILE_PATH] = value; + } + } + + offset += size; + remainingBytes -= size; + } + + return data; +} + +function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array { + const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + return result; +} + function writeFilePath(header: Uint8Array, filePath: string): void { // Return fileName.slice(offset, offset + size).padEnd(size, padding); // If the length of the file path is less than 100 bytes, then we write it to @@ -225,7 +306,7 @@ function writeFileMtime(header: Uint8Array, mtime?: Date): void { // The file mtime is stored in this chunk. As the mtime is not modified when // extracting a TAR file, the mtime can be preserved while still getting // deterministic archives. - const date = mtime != null ? dateToUnixTime(mtime) : ''; + const date = mtime != null ? dateToTarTime(mtime) : ''; writeBytesToArray( header, pad(date, HeaderSize.FILE_MTIME, '0', '\0'), @@ -261,6 +342,26 @@ function writeFileType( ); } +function writeOwnerUserName(header: Uint8Array, username?: string): void { + const uname = username ?? ''; + writeBytesToArray( + header, + uname.padEnd(HeaderSize.OWNER_USERNAME, '\0'), + HeaderOffset.OWNER_USERNAME, + HeaderSize.OWNER_USERNAME, + ); +} + +function writeOwnerGroupName(header: Uint8Array, groupname?: string): void { + const gname = groupname ?? ''; + writeBytesToArray( + header, + gname.padEnd(HeaderSize.OWNER_GROUPNAME, '\0'), + HeaderOffset.OWNER_GROUPNAME, + HeaderSize.OWNER_GROUPNAME, + ); +} + function writeUstarMagic(header: Uint8Array): void { // This value is the USTAR magic string which makes this file appear as // a tar file. Without this, the file cannot be parsed and extracted. @@ -289,144 +390,136 @@ function writeChecksum(header: Uint8Array, checksum: number): void { ); } -function writeOwnerUserName(header: Uint8Array, username?: string): void { - writeBytesToArray( - header, - pad(username ?? '', HeaderSize.OWNER_USERNAME, '0', '\0'), - HeaderOffset.OWNER_USERNAME, - HeaderSize.OWNER_USERNAME, +function decodeFilePath(array: Uint8Array): string { + const fileNamePrefix = extractString( + array, + HeaderOffset.FILE_NAME_PREFIX, + HeaderSize.FILE_NAME_PREFIX, ); -} -function writeOwnerGroupName(header: Uint8Array, groupname?: string): void { - writeBytesToArray( - header, - pad(groupname ?? '', HeaderSize.OWNER_GROUPNAME, '0', '\0'), - HeaderOffset.OWNER_GROUPNAME, - HeaderSize.OWNER_GROUPNAME, + const fileNameSuffix = extractString( + array, + HeaderOffset.FILE_NAME, + HeaderSize.FILE_NAME, ); -} - -function encodeExtendedHeader( - data: Partial>, -): Uint8Array { - const encoder = new TextEncoder(); - let totalByteSize = 0; - const entries: Array = []; - - // For extended PAX headers, the format of metadata is as follows: - // =\n - // Where is the total length of the line including the key-value - // pair, the separator \n character, the space between the size and - // the line, and the size characters itself. Note \n is written using two - // characters but it is a single ASCII byte. - for (const [key, value] of Object.entries(data)) { - let size = key.length + value.length + 3; // Initial guess (' ', =, \n) - size += size.toString().length; // Adjust for size itself - - const entry = `${size} ${key}=${value}\n`; - entries.push(entry); - // Update the total byte length of the header with the entry's size - totalByteSize += size; - } - - // The entries are encoded later to reduce memory allocation - const output = new Uint8Array(totalByteSize); - let offset = 0; - - for (const entry of entries) { - // Older browsers and runtimes might return written as undefined. That is - // not a concern for us. - const { written } = encoder.encodeInto(entry, output.subarray(offset)); - if (!written) throw new Error('TMP not written'); - offset += written; + if (fileNamePrefix !== '') { + return fileNamePrefix + fileNameSuffix; + } else { + return fileNameSuffix; } - - return output; } -function decodeExtendedHeader( - array: Uint8Array, -): Partial> { - const decoder = new TextDecoder(); - const data: Partial> = {}; +function decodeFileMode(array: Uint8Array): number { + return extractOctal(array, HeaderOffset.FILE_MODE, HeaderSize.FILE_MODE); +} - // Track offset and remaining bytes in the array - let offset = 0; - let remainingBytes = array.byteLength; +function decodeOwnerUid(array: Uint8Array): number { + return extractOctal(array, HeaderOffset.OWNER_UID, HeaderSize.OWNER_UID); +} - while (remainingBytes > 0) { - const size = extractDecimal(array, offset, undefined, ' '); - const fullLine = decoder.decode(array.subarray(offset, offset + size)); +function decodeOwnerGid(array: Uint8Array): number { + return extractOctal(array, HeaderOffset.OWNER_GID, HeaderSize.OWNER_GID); +} - const sizeSeparatorIndex = fullLine.indexOf(' '); - if (sizeSeparatorIndex === -1) { - throw new Error('TMP invalid ennnntry'); - } - const line = fullLine.substring(sizeSeparatorIndex + 1); +function decodeFileSize(array: Uint8Array): number { + return extractOctal(array, HeaderOffset.FILE_SIZE, HeaderSize.FILE_SIZE); +} - const entrySeparatorIndex = line.indexOf('='); - if (entrySeparatorIndex === -1) { - throw new Error('TMP invalid ennnntry'); - } - const key = line.substring(0, entrySeparatorIndex); - const _value = line.substring(entrySeparatorIndex + 1); +function decodeFileMtime(array: Uint8Array): Date { + return tarTimeToDate( + extractOctal(array, HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME), + ); +} - if (!Object.values(MetadataKeywords).includes(key as MetadataKeywords)) { - throw new Error('TMP key doesnt exist'); - } +function decodeOwnerUserName(array: Uint8Array): string { + return extractString( + array, + HeaderOffset.OWNER_USERNAME, + HeaderSize.OWNER_USERNAME, + ); +} - // Remove the trailing newline - const value = _value.substring(0, _value.length - 1); - switch (key as MetadataKeywords) { - case MetadataKeywords.FILE_PATH: { - data[MetadataKeywords.FILE_PATH] = value; - } - } +function decodeOwnerGroupName(array: Uint8Array): string { + return extractString( + array, + HeaderOffset.OWNER_GROUPNAME, + HeaderSize.OWNER_GROUPNAME, + ); +} - offset += size; - remainingBytes -= size; +function decodeFileType(array): FileType { + const type = extractString( + array, + HeaderOffset.TYPE_FLAG, + HeaderSize.TYPE_FLAG, + ); + switch (type) { + case EntryType.FILE: + return 'file'; + case EntryType.DIRECTORY: + return 'directory'; + case EntryType.EXTENDED: + return 'extended'; + default: + throw new errors.ErrorVirtualTarParserInvalidHeader( + `Got invalid file type ${type}`, + ); } +} - return data; +function decodeUstarMagic(array: Uint8Array): string { + return extractString(array, HeaderOffset.USTAR_NAME, HeaderSize.USTAR_NAME); } -function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array { - const totalLength = arrays.reduce((acc, arr) => acc + arr.length, 0); - const result = new Uint8Array(totalLength); - let offset = 0; - for (const arr of arrays) { - result.set(arr, offset); - offset += arr.length; - } - return result; +function decodeUstarVersion(array: Uint8Array): string { + return extractString( + array, + HeaderOffset.USTAR_VERSION, + HeaderSize.USTAR_VERSION, + ); +} + +function decodeChecksum(array: Uint8Array): number { + return extractOctal(array, HeaderOffset.CHECKSUM, HeaderSize.CHECKSUM); } export { never, pad, calculateChecksum, - dateToUnixTime, + dateToTarTime, + tarTimeToDate, extractBytes, extractString, extractOctal, extractDecimal, - decodeFilePath, isNullBlock, writeBytesToArray, + encodeExtendedHeader, + decodeExtendedHeader, + concatUint8Arrays, writeFilePath, writeFileMode, writeOwnerUid, writeOwnerGid, writeFileSize, writeFileMtime, + writeOwnerUserName, + writeOwnerGroupName, writeFileType, writeUstarMagic, writeChecksum, - writeOwnerUserName, - writeOwnerGroupName, - encodeExtendedHeader, - decodeExtendedHeader, - concatUint8Arrays, + decodeFilePath, + decodeFileMode, + decodeOwnerUid, + decodeOwnerGid, + decodeFileSize, + decodeFileMtime, + decodeOwnerUserName, + decodeOwnerGroupName, + decodeFileType, + decodeUstarMagic, + decodeUstarVersion, + decodeChecksum, }; diff --git a/tests/Generator.test.ts b/tests/Generator.test.ts index f955974..d9bd966 100644 --- a/tests/Generator.test.ts +++ b/tests/Generator.test.ts @@ -1,12 +1,11 @@ -import type { VirtualFile, VirtualDirectory } from './types'; import fs from 'fs'; import os from 'os'; import path from 'path'; import fc from 'fast-check'; import { test } from '@fast-check/jest'; import * as tar from 'tar'; -import { EntryType, GeneratorState } from '@/types'; import Generator from '@/Generator'; +import { EntryType, GeneratorState } from '@/types'; import * as tarConstants from '@/constants'; import * as tarErrors from '@/errors'; import * as tarUtils from '@/utils'; @@ -34,13 +33,13 @@ describe('generating archive', () => { expect(uid).toEqual(file.stat.uid); expect(gid).toEqual(file.stat.gid); expect(size).toEqual(file.stat.size); - expect(mtime).toEqual(tarUtils.dateToUnixTime(file.stat.mtime!)); + expect(mtime).toEqual(tarUtils.dateToTarTime(file.stat.mtime!)); expect(format).toEqual('ustar'); expect(version).toEqual('00'); }, ); - test.prop([utils.dirArb(0)])( + test.prop([utils.dirArb()])( 'should generate a valid directory header', (file) => { // Generate and split the header @@ -60,7 +59,7 @@ describe('generating archive', () => { expect(uid).toEqual(file.stat.uid); expect(gid).toEqual(file.stat.gid); expect(size).toEqual(0); - expect(mtime).toEqual(tarUtils.dateToUnixTime(file.stat.mtime!)); + expect(mtime).toEqual(tarUtils.dateToTarTime(file.stat.mtime!)); expect(format).toEqual('ustar'); expect(version).toEqual('00'); }, @@ -109,7 +108,7 @@ describe('generator state robustness', () => { ); test.prop( - [fc.oneof(utils.fileContentArb(), utils.fileArb(), utils.dirArb(0))], + [fc.oneof(utils.fileContentArb(), utils.fileArb(), utils.dirArb())], { numRuns: 10 }, )('should fail writing data when attempting to end archive', (data) => { const generator = new Generator(); @@ -134,7 +133,7 @@ describe('generator state robustness', () => { }); test.prop( - [fc.oneof(utils.fileContentArb(), utils.fileArb(), utils.dirArb(0))], + [fc.oneof(utils.fileContentArb(), utils.fileArb(), utils.dirArb())], { numRuns: 10 }, )('should fail writing data after ending archive', (data) => { const generator = new Generator(); @@ -161,9 +160,11 @@ describe('generator state robustness', () => { }); describe('testing against tar', () => { - test.skip.prop([utils.fileTreeArb])( + const encoder = new TextEncoder(); + + test.prop([utils.fileTreeArb()])( 'should match output of tar', - async (vfs) => { + async (fileTree) => { // Create a temp directory to use for node-tar const tempDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'js-virtualtar-test-'), @@ -174,145 +175,85 @@ describe('testing against tar', () => { const generator = new Generator(); const blocks: Array = []; - const trimmedVfs = structuredClone(vfs); - const trimStat = (entry: VirtualFile | VirtualDirectory) => { - entry.stat = { size: entry.stat.size, mode: entry.stat.mode }; - if (entry.type === 'directory') { - for (const child of entry.children) { - trimStat(child); - } - } - }; - for (const entry of trimmedVfs) trimStat(entry); - - const generateEntry = (entry: VirtualFile | VirtualDirectory) => { - // Due to operating system restrictions, node-tar cannot properly - // reproduce all the metadata at the time of extracting files. The - // mtime defaults to extraction time, the uid and gid is fixed to the - // user who the program is running under. As fast-check is used to - // generate this data, this will always differ than the observed stat, - // so these fields will be ignored for this test. - entry.stat = { - mode: entry.stat.mode, - size: entry.stat.size, - }; - + for (const entry of fileTree) { if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { - // Push the extended metadata header - const data = tarUtils.encodeExtendedHeader({ path: entry.path }); - blocks.push(generator.generateExtended(data.byteLength)); + // Push the extended header + const extendedData = tarUtils.encodeExtendedHeader({ + path: entry.path, + }); + blocks.push(generator.generateExtended(extendedData.byteLength)); - // Push the content block + // Push each data chunk for ( let offset = 0; - offset < data.byteLength; + offset < extendedData.byteLength; offset += tarConstants.BLOCK_SIZE ) { - blocks.push( - generator.generateData( - data.subarray(offset, offset + tarConstants.BLOCK_SIZE), - ), + const chunk = extendedData.slice( + offset, + offset + tarConstants.BLOCK_SIZE, ); + blocks.push(generator.generateData(chunk)); } } - const filePath = entry.path.length <= tarConstants.STANDARD_PATH_SIZE ? entry.path : ''; - switch (entry.type) { - case 'file': { - // Generate the header - entry = entry as VirtualFile; - blocks.push(generator.generateFile(filePath, entry.stat)); - - // Generate the data - const encoder = new TextEncoder(); - let content = entry.content; - while (content.length > 0) { - const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); - blocks.push(generator.generateData(encoder.encode(dataChunk))); - content = content.slice(tarConstants.BLOCK_SIZE); - } - break; - } - - case 'directory': { - // Generate the header - entry = entry as VirtualDirectory; - blocks.push(generator.generateDirectory(filePath, entry.stat)); + if (entry.type === 'file') { + blocks.push(generator.generateFile(filePath, entry.stat)); + const data = encoder.encode(entry.content); - // Perform the same operation on all children - for (const file of entry.children) { - generateEntry(file); - } - break; + // Push each data chunk + for ( + let offset = 0; + offset < data.byteLength; + offset += tarConstants.BLOCK_SIZE + ) { + const chunk = data.slice( + offset, + offset + tarConstants.BLOCK_SIZE, + ); + blocks.push(generator.generateData(chunk)); } - - default: - throw new Error('Invalid type'); + } else { + blocks.push(generator.generateDirectory(filePath, entry.stat)); } - }; + } - for (const entry of vfs) generateEntry(entry); blocks.push(generator.generateEnd()); blocks.push(generator.generateEnd()); - // Write the archive to fs + // Write the archive to disk const archivePath = path.join(tempDir, 'archive.tar'); - const tarFile = await fs.promises.open(archivePath, 'w+'); - for (const block of blocks) await tarFile.write(block); - await tarFile.close(); + const fd = await fs.promises.open(archivePath, 'w'); + for await (const chunk of blocks) { + await fd.write(chunk); + } + await fd.close(); - const vfsPath = path.join(tempDir, 'vfs'); - await fs.promises.mkdir(vfsPath, { recursive: true }); + // Extract the archive from disk await tar.extract({ file: archivePath, - cwd: vfsPath, - preservePaths: true, + cwd: tempDir, }); - - // Reconstruct the vfs and compare the contents to actual vfs - const traverse = async (currentPath: string) => { - const entries = await fs.promises.readdir(currentPath, { - withFileTypes: true, - }); - const vfsEntries: Array = []; - - for (const entry of entries) { - const fullPath = path.join(currentPath, entry.name); - const relativePath = path.relative(vfsPath, fullPath); - const stats = await fs.promises.stat(fullPath); - - if (entry.isDirectory()) { - // Sometimes, the size of a directory on disk might not be 0 bytes - // due to the storage of additional metadata. This is different from - // the way tar stores directories, so the size is being manually set. - const entry: VirtualDirectory = { - type: 'directory', - path: relativePath + '/', - children: await traverse(fullPath), - stat: { size: 0, mode: stats.mode }, - }; - vfsEntries.push(entry); - } else { - const content = await fs.promises.readFile(fullPath); - const entry: VirtualFile = { - type: 'file', - path: relativePath, - content: content.toString(), - stat: { size: stats.size, mode: stats.mode }, - }; - vfsEntries.push(entry); - } + await fs.promises.rm(archivePath); + + for (const entry of fileTree) { + // Note that writing files to disk will change some of the file stat + // and metadata, so they are not being tested against the input file + // tree. + if (entry.type === 'file') { + const filePath = path.join(tempDir, entry.path); + const content = await fs.promises.readFile(filePath, 'utf8'); + expect(content).toBe(entry.content); + } else { + const dirPath = path.join(tempDir, entry.path); + const stat = await fs.promises.stat(dirPath); + expect(stat.isDirectory()).toBe(true); } - - return vfsEntries; - }; - - const reconstructedVfs = await traverse(vfsPath); - expect(utils.deepSort(reconstructedVfs)).toEqual(utils.deepSort(vfs)); + } } finally { await fs.promises.rm(tempDir, { force: true, recursive: true }); } diff --git a/tests/Parser.test.ts b/tests/Parser.test.ts index 229fcb9..44fad57 100644 --- a/tests/Parser.test.ts +++ b/tests/Parser.test.ts @@ -3,8 +3,8 @@ import type { MetadataKeywords } from '@/types'; import fs from 'fs'; import path from 'path'; import os from 'os'; -import { test } from '@fast-check/jest'; import fc from 'fast-check'; +import { test } from '@fast-check/jest'; import * as tar from 'tar'; import Parser from '@/Parser'; import { HeaderOffset, ParserState } from '@/types'; @@ -194,53 +194,53 @@ describe('parsing extended metadata', () => { }); describe('testing against tar', () => { - test.skip.prop([utils.fileTreeArb], { numRuns: 1 })( + let tempDir: string | undefined; + + afterEach(async () => { + if (tempDir) { + await fs.promises.rm(tempDir, { force: true, recursive: true }); + } + }); + + test.prop([utils.fileTreeArb()], { numRuns: 100 })( 'should match output of tar', - async (vfs) => { + async (fileTree) => { // Create a temp directory to use for node-tar const tempDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'js-virtualtar-test-'), ); try { - const vfsPath = path.join(tempDir, 'vfs'); - await fs.promises.mkdir(vfsPath); + const fileTreePath = path.join(tempDir, 'tree'); + await fs.promises.mkdir(fileTreePath); // Write the vfs to disk for tar to archive - const writeVfs = async (entry: VirtualFile | VirtualDirectory) => { - // Due to operating system restrictions, all the generated metadata - // cannot be written to disk. The mode, mtime, uid, and gid is - // determined by external variables, and as such, will not be tested. - delete entry.stat.mode; - delete entry.stat.mtime; - delete entry.stat.uid; - delete entry.stat.gid; - - const entryPath = path.join(vfsPath, entry.path); + const writeFileTree = async (entry: VirtualFile | VirtualDirectory) => { + const entryPath = path.join(fileTreePath, entry.path); if (entry.type === 'directory') { - await fs.promises.mkdir(entryPath); - for (const file of entry.children) await writeVfs(file); + await fs.promises.mkdir(entryPath, { recursive: true }); } else { await fs.promises.writeFile(entryPath, entry.content); } }; - for (const entry of vfs) await writeVfs(entry); + for (const entry of fileTree) await writeFileTree(entry); // Use tar to archive the file const archivePath = path.join(tempDir, 'archive.tar'); - const entries = await fs.promises.readdir(vfsPath); - await new Promise((resolve) => { - tar - .create( - { - cwd: vfsPath, - preservePaths: true, - }, - entries, - ) - .pipe(fs.createWriteStream(archivePath)) - .on('close', resolve); - }); + const entries = await fs.promises.readdir(fileTreePath); + const archive = tar.create( + { + cwd: fileTreePath, + preservePaths: true, + }, + entries, + ); + + const fd = await fs.promises.open(archivePath, 'w'); + for await (const chunk of archive) { + await fd.write(chunk); + } + await fd.close(); const chunks: Uint8Array[] = []; const stream = fs.createReadStream(archivePath, { highWaterMark: 512 }); @@ -249,10 +249,10 @@ describe('testing against tar', () => { } const parser = new Parser(); - const decoder = new TextDecoder(); - const reconstructedVfs: Array = []; - const pathStack: Map = new Map(); - let currentEntry: VirtualFile; + const encoder = new TextEncoder(); + const reconstructedTree: Record = {}; + let workingPath: string | undefined = undefined; + let workingData: Uint8Array = new Uint8Array(); let extendedData: Uint8Array | undefined; let dataOffset = 0; @@ -262,7 +262,6 @@ describe('testing against tar', () => { switch (token.type) { case 'header': { - let parsedEntry: VirtualFile | VirtualDirectory | undefined; let extendedMetadata: | Partial> | undefined; @@ -270,30 +269,26 @@ describe('testing against tar', () => { extendedMetadata = tarUtils.decodeExtendedHeader(extendedData); } - const fullPath = extendedMetadata?.path?.trim() + const fullPath = extendedMetadata?.path ? extendedMetadata.path : token.filePath; + if (workingPath != null) { + reconstructedTree[workingPath] = workingData; + workingData = new Uint8Array(); + workingPath = undefined; + } + switch (token.fileType) { case 'file': { - parsedEntry = { - type: 'file', - path: fullPath, - content: '', - stat: { size: token.fileSize }, - }; + workingPath = fullPath; break; } case 'directory': { - parsedEntry = { - type: 'directory', - path: fullPath, - children: [], - stat: { size: token.fileSize }, - }; + reconstructedTree[fullPath] = null; break; } - case 'metadata': { + case 'extended': { extendedData = new Uint8Array(token.fileSize); extendedMetadata = {}; break; @@ -301,35 +296,6 @@ describe('testing against tar', () => { default: throw new Error('Invalid state'); } - // If parsed entry has not been reassigned, then it was a metadata - // header. Continue to fetch extended metadata. - if (parsedEntry == null) continue; - - const parentPath = path.dirname(fullPath); - - // If this entry is a directory, then it is pushed to the root of - // the reconstructed virtual file system and into a map at the same - // time. This allows us to add new children to the directory by - // looking up the path in a map rather than modifying the value in - // the reconstructed file system. - - if (parentPath === '.') { - reconstructedVfs.push(parsedEntry); - } else { - // It is guaranteed that in a valid tar file, the parent will - // always exist. - const parent: VirtualDirectory = pathStack.get( - parentPath + '/', - ); - parent.children.push(parsedEntry); - } - - if (parsedEntry.type === 'directory') { - pathStack.set(fullPath, parsedEntry); - } else { - // Type narrowing doesn't work well with manually specified types - currentEntry = parsedEntry as VirtualFile; - } // If we were using the extended metadata for this header, reset it // for the next header. @@ -341,19 +307,36 @@ describe('testing against tar', () => { case 'data': { if (extendedData == null) { - // It is guaranteed that in a valid tar file, a data block will - // always come after a header block for a file. - currentEntry!['content'] += decoder.decode(token.data); + workingData = tarUtils.concatUint8Arrays( + workingData, + token.data, + ); } else { extendedData.set(token.data, dataOffset); dataOffset += token.data.byteLength; } break; } + + case 'end': { + // Finalise adding the last file into the tree + if (workingPath != null) { + reconstructedTree[workingPath] = workingData; + workingData = new Uint8Array(); + workingPath = undefined; + } + } } } - expect(utils.deepSort(reconstructedVfs)).toEqual(utils.deepSort(vfs)); + for (const entry of fileTree) { + if (entry.type === 'file') { + const content = encoder.encode(entry.content); + expect(reconstructedTree[entry.path]).toEqual(content); + } else { + expect(reconstructedTree[entry.path]).toBeNull(); + } + } } finally { await fs.promises.rm(tempDir, { force: true, recursive: true }); } diff --git a/tests/VirtualTar.test.ts b/tests/VirtualTar.test.ts new file mode 100644 index 0000000..a2befbb --- /dev/null +++ b/tests/VirtualTar.test.ts @@ -0,0 +1,246 @@ +import type { VirtualFile, VirtualDirectory } from './types'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { test } from '@fast-check/jest'; +import * as tar from 'tar'; +import VirtualTar from '@/VirtualTar'; +import { VirtualTarState } from '@/types'; +import * as utils from './utils'; + +describe('generator', () => { + let tempDir; + + beforeEach(async () => { + tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'js-virtualtar-test-'), + ); + }); + + afterEach(async () => { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + }); + + test('should set state to generation', async () => { + const tar = new VirtualTar({ mode: 'generate' }); + // @ts-ignore accessing protected member for state analysis + expect(tar.state).toEqual(VirtualTarState.GENERATOR); + }); + + test('should write data to file', async () => { + // Set the file names and their data + const fileName1 = 'file1.txt'; + const fileName2 = 'file2.txt'; + const fileName3 = 'file3.txt'; + const fileData = 'testing'; + const fileMode = 0o777; + + const vtar = new VirtualTar({ mode: 'generate' }); + + // Write file to archive + vtar.addFile( + fileName1, + { size: fileData.length, mode: fileMode }, + fileData, + ); + vtar.addFile( + fileName2, + { size: fileData.length, mode: fileMode }, + Buffer.from(fileData), + ); + vtar.addFile( + fileName3, + { size: fileData.length, mode: fileMode }, + async function* () { + const halfway = Math.floor(fileData.length / 2); + const prefix = fileData.slice(0, halfway); + const suffix = fileData.slice(halfway); + + // Mixing string and Uint8Array data + yield Buffer.from(prefix); + yield suffix; + }, + ); + vtar.finalize(); + + const archivePath = path.join(tempDir, 'archive.tar'); + const fd = await fs.promises.open(archivePath, 'w'); + for await (const chunk of vtar.yieldChunks()) { + await fd.write(chunk); + } + await fd.close(); + + await tar.extract({ + file: archivePath, + cwd: tempDir, + }); + + // Check if each file has been written correctly + const extractedData1 = await fs.promises.readFile( + path.join(tempDir, fileName1), + ); + const extractedData2 = await fs.promises.readFile( + path.join(tempDir, fileName2), + ); + const extractedData3 = await fs.promises.readFile( + path.join(tempDir, fileName3), + ); + expect(extractedData1.toString()).toEqual(fileData); + expect(extractedData2.toString()).toEqual(fileData); + expect(extractedData3.toString()).toEqual(fileData); + }); + + test('should write a directory to the archive', async () => { + // Set the file names and their data + const dirName = 'dir'; + const dirMode = 0o777; + + const vtar = new VirtualTar({ mode: 'generate' }); + + // Write directory to archive + vtar.addDirectory(dirName, { mode: dirMode }); + vtar.finalize(); + + const archivePath = path.join(tempDir, 'archive.tar'); + const fd = await fs.promises.open(archivePath, 'w'); + for await (const chunk of vtar.yieldChunks()) { + await fd.write(chunk); + } + await fd.close(); + + await tar.extract({ + file: archivePath, + cwd: tempDir, + }); + await fs.promises.rm(archivePath); + + // Check if the directory has been written correctly + const directories = await fs.promises.readdir(path.join(tempDir)); + expect(directories).toEqual([dirName]); + }); +}); + +describe('parser', () => { + let tempDir; + + beforeEach(async () => { + tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'js-virtualtar-test-'), + ); + }); + + afterEach(async () => { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + }); + + test('should set state to parsing', async () => { + const tar = new VirtualTar({ mode: 'parse' }); + // @ts-ignore accessing protected member for state analysis + expect(tar.state).toEqual(VirtualTarState.PARSER); + }); + + test('should read files and directories', async () => { + // Set the file names and their data + const dirName = 'dir'; + const fileName1 = 'file.txt'; + const fileName2 = 'dir/file.txt'; + const fileData = 'testing'; + + await fs.promises.mkdir(path.join(tempDir, dirName)); + await fs.promises.writeFile(path.join(tempDir, fileName1), fileData); + await fs.promises.writeFile(path.join(tempDir, fileName2), fileData); + + const archive = tar.create( + { + cwd: tempDir, + preservePaths: true, + }, + [fileName1, dirName, fileName2], + ); + + const entries: Record = {}; + + // Read files and directories and add it to the entries record + const vtar = new VirtualTar({ + mode: 'parse', + onFile: async (header, data) => { + const content: Array = []; + for await (const chunk of data()) { + content.push(chunk); + } + const fileContent = Buffer.concat(content).toString(); + entries[header.path] = fileContent; + }, + onDirectory: async (header) => { + entries[header.path] = undefined; + }, + }); + + // Enqueue each generated chunk from the archive + for await (const chunk of archive) { + vtar.write(chunk); + } + + // Make sure all the callbacks settle + await vtar.settled(); + + expect(entries[dirName]).toBeUndefined(); + expect(entries[fileName1]).toEqual(fileData); + expect(entries[fileName2]).toEqual(fileData); + }); +}); + +describe('integration tests', () => { + test.prop([utils.fileTreeArb()])( + 'archiving and unarchiving a file tree', + async (fileTree) => { + const generator = new VirtualTar({ mode: 'generate' }); + + for (const entry of fileTree) { + if (entry.type === 'file') { + generator.addFile(entry.path, entry.stat, entry.content); + } else { + generator.addDirectory(entry.path, entry.stat); + } + } + generator.finalize(); + + const archive = generator.yieldChunks(); + const entries: Array = []; + + const parser = new VirtualTar({ + mode: 'parse', + onFile: async (header, data) => { + const content: Array = []; + for await (const chunk of data()) { + content.push(chunk); + } + const fileContent = Buffer.concat(content).toString(); + entries.push({ + type: 'file', + path: header.path, + stat: header.stat, + content: fileContent, + }); + }, + onDirectory: async (header) => { + entries.push({ + type: 'directory', + path: header.path, + stat: header.stat, + }); + }, + }); + + for await (const chunk of archive) { + parser.write(chunk); + } + + await parser.settled(); + + expect(utils.deepSort(entries)).toContainAllValues( + utils.deepSort(fileTree), + ); + }, + ); +}); diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 3b64a23..5c02fd3 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -1,6 +1,4 @@ -import type { VirtualFile, VirtualDirectory } from './types'; -import type { MetadataKeywords } from '@/types'; -import path from 'path'; +import type { FileStat, MetadataKeywords } from '@/types'; import { test } from '@fast-check/jest'; import Generator from '@/Generator'; import Parser from '@/Parser'; @@ -9,71 +7,57 @@ import * as tarConstants from '@/constants'; import * as utils from './utils'; describe('integration testing', () => { - test.skip.prop([utils.fileTreeArb])( + test.prop([utils.fileTreeArb()])( 'should archive and unarchive a virtual file system', - (vfs) => { + (fileTree) => { const generator = new Generator(); const blocks: Array = []; + const encoder = new TextEncoder(); - const generateArchive = (entry: VirtualFile | VirtualDirectory) => { + for (const entry of fileTree) { if (entry.path.length > tarConstants.STANDARD_PATH_SIZE) { - // Push the extended metadata header - const data = tarUtils.encodeExtendedHeader({ path: entry.path }); - blocks.push(generator.generateExtended(data.byteLength)); + // Push the extended header + const extendedData = tarUtils.encodeExtendedHeader({ + path: entry.path, + }); + blocks.push(generator.generateExtended(extendedData.byteLength)); - // Push the content + // Push each data chunk for ( let offset = 0; - offset < data.byteLength; + offset < extendedData.byteLength; offset += tarConstants.BLOCK_SIZE ) { - blocks.push( - generator.generateData( - data.subarray(offset, offset + tarConstants.BLOCK_SIZE), - ), + const chunk = extendedData.slice( + offset, + offset + tarConstants.BLOCK_SIZE, ); + blocks.push(generator.generateData(chunk)); } } + const filePath = + entry.path.length <= tarConstants.STANDARD_PATH_SIZE + ? entry.path + : ''; - const filePath = entry.path.length <= 255 ? entry.path : ''; - - switch (entry.type) { - case 'file': { - // Generate the header - entry = entry as VirtualFile; - blocks.push(generator.generateFile(filePath, entry.stat)); - - // Generate the data - const encoder = new TextEncoder(); - let content = entry.content; - while (content.length > 0) { - const dataChunk = content.slice(0, tarConstants.BLOCK_SIZE); - blocks.push(generator.generateData(encoder.encode(dataChunk))); - content = content.slice(tarConstants.BLOCK_SIZE); - } - break; - } - - case 'directory': { - // Generate the header - entry = entry as VirtualDirectory; - blocks.push(generator.generateDirectory(filePath, entry.stat)); + if (entry.type === 'file') { + blocks.push(generator.generateFile(filePath, entry.stat)); + const data = encoder.encode(entry.content); - // Perform the same operation on all children - for (const file of entry.children) { - generateArchive(file); - } - break; + // Push each data chunk + for ( + let offset = 0; + offset < data.byteLength; + offset += tarConstants.BLOCK_SIZE + ) { + const chunk = data.slice(offset, offset + tarConstants.BLOCK_SIZE); + blocks.push(generator.generateData(chunk)); } - - default: - tarUtils.never('Invalid type'); + } else { + blocks.push(generator.generateDirectory(filePath, entry.stat)); } - }; - - for (const entry of vfs) { - generateArchive(entry); } + blocks.push(generator.generateEnd()); blocks.push(generator.generateEnd()); @@ -82,10 +66,16 @@ describe('integration testing', () => { // the parsed virtual file system matches the input. const parser = new Parser(); - const decoder = new TextDecoder(); - const reconstructedVfs: Array = []; - const pathStack: Map = new Map(); - let currentEntry: VirtualFile; + const reconstructedTree: Record< + string, + { + data?: Uint8Array; + stat: FileStat; + } + > = {}; + let workingPath: string | undefined = undefined; + let workingStat: FileStat | undefined = undefined; + let workingData: Uint8Array = new Uint8Array(); let extendedData: Uint8Array | undefined; let dataOffset = 0; @@ -95,7 +85,6 @@ describe('integration testing', () => { switch (token.type) { case 'header': { - let parsedEntry: VirtualFile | VirtualDirectory | undefined; let extendedMetadata: | Partial> | undefined; @@ -103,42 +92,41 @@ describe('integration testing', () => { extendedMetadata = tarUtils.decodeExtendedHeader(extendedData); } - const fullPath = extendedMetadata?.path?.trim() + const fullPath = extendedMetadata?.path ? extendedMetadata.path : token.filePath; + if (workingPath != null && workingStat != null) { + reconstructedTree[workingPath] = { + stat: workingStat, + data: workingData, + }; + workingData = new Uint8Array(); + workingPath = undefined; + workingStat = undefined; + } + + const fileStat: FileStat = { + size: token.fileSize, + mtime: token.fileMtime, + mode: token.fileMode, + uid: token.ownerUid, + gid: token.ownerGid, + uname: token.ownerUserName, + gname: token.ownerGroupName, + }; + switch (token.fileType) { case 'file': { - parsedEntry = { - type: 'file', - path: fullPath, - content: '', - stat: { - mode: token.fileMode, - uid: token.ownerUid, - gid: token.ownerGid, - size: token.fileSize, - mtime: token.fileMtime, - }, - }; + workingPath = fullPath; + workingStat = fileStat; break; } case 'directory': { - parsedEntry = { - type: 'directory', - path: fullPath, - children: [], - stat: { - mode: token.fileMode, - uid: token.ownerUid, - gid: token.ownerGid, - size: token.fileSize, - mtime: token.fileMtime, - }, - }; + reconstructedTree[fullPath] = { stat: fileStat }; break; } - case 'metadata': { + case 'extended': { extendedData = new Uint8Array(token.fileSize); extendedMetadata = {}; break; @@ -146,33 +134,6 @@ describe('integration testing', () => { default: throw new Error('Invalid state'); } - // If parsed entry has not been reassigned, then it was a metadata - // header. Continue to fetch extended metadata. - if (parsedEntry == null) continue; - - const parentPath = path.dirname(fullPath); - - // If this entry is a directory, then it is pushed to the root of - // the reconstructed virtual file system and into a map at the same - // time. This allows us to add new children to the directory by - // looking up the path in a map rather than modifying the value in - // the reconstructed file system. - - if (parentPath === '.') { - reconstructedVfs.push(parsedEntry); - } else { - // It is guaranteed that in a valid tar file, the parent will - // always exist. - const parent: VirtualDirectory = pathStack.get(parentPath + '/'); - parent.children.push(parsedEntry); - } - - if (parsedEntry.type === 'directory') { - pathStack.set(fullPath, parsedEntry); - } else { - // Type narrowing doesn't work well with manually specified types - currentEntry = parsedEntry as VirtualFile; - } // If we were using the extended metadata for this header, reset it // for the next header. @@ -184,19 +145,38 @@ describe('integration testing', () => { case 'data': { if (extendedData == null) { - // It is guaranteed that in a valid tar file, a data block will - // always come after a header block for a file. - currentEntry!['content'] += decoder.decode(token.data); + workingData = tarUtils.concatUint8Arrays(workingData, token.data); } else { extendedData.set(token.data, dataOffset); dataOffset += token.data.byteLength; } break; } + + case 'end': { + // Finalise adding the last file into the tree + if (workingPath != null && workingStat != null) { + reconstructedTree[workingPath] = { + stat: workingStat, + data: workingData, + }; + workingData = new Uint8Array(); + workingPath = undefined; + workingStat = undefined; + } + } } } - expect(reconstructedVfs).toContainAllValues(vfs); + for (const entry of fileTree) { + expect(entry.stat).toMatchObject(reconstructedTree[entry.path].stat); + if (entry.type === 'file') { + const content = encoder.encode(entry.content); + expect(reconstructedTree[entry.path].data).toEqual(content); + } else { + expect(reconstructedTree[entry.path].data).toBeUndefined(); + } + } }, ); }); diff --git a/tests/types.ts b/tests/types.ts index ec8d2a7..8c71c52 100644 --- a/tests/types.ts +++ b/tests/types.ts @@ -11,7 +11,6 @@ type VirtualDirectory = { type: 'directory'; path: string; stat: FileStat; - children: Array; }; type VirtualMetadata = { diff --git a/tests/utils/fastcheck.ts b/tests/utils/fastcheck.ts index 5b7061e..43e7e35 100644 --- a/tests/utils/fastcheck.ts +++ b/tests/utils/fastcheck.ts @@ -42,6 +42,12 @@ const mtimeArb: fc.Arbitrary = fc }) .map((date) => new Date(Math.floor(date.getTime() / 1000) * 1000)); +const unameGnameArb: fc.Arbitrary = fc.string({ + minLength: 1, + maxLength: 32, + size: 'small', +}); + /** * Due to the large amount of conditions, using a string primitive arbitrary * takes unfeasibly long to generate values, especially for larger path lengths. @@ -69,7 +75,7 @@ const filenameArb = ( ): fc.Arbitrary => { // Most of these characters are disallowed by windows const restrictedCharacters = '/\\*?"<>|:'; - const filterRegex = /^(\.|\.\.|con|prn|aux|nul|tty|null|zero|full)$/i; + const filterRegex = /^(\.|\.\.|con|prn|aux|nul|tty|null|zero|full)$|^(@|~)/i; const charCodes = fc.array( fc @@ -112,6 +118,8 @@ const statDataArb = ( gid: uidgidArb, size: sizeArb(type, content), mtime: mtimeArb, + uname: unameGnameArb, + gname: unameGnameArb, }) .noShrink(); @@ -124,7 +132,10 @@ const fileArb = ( }: { minFilePathSize?: number; maxFilePathSize?: number; - } = {}, + } = { + minFilePathSize: 1, + maxFilePathSize: 512, + }, ): fc.Arbitrary => { // Generate file-specific records const fileData = fc.record({ @@ -148,7 +159,6 @@ const fileArb = ( }; const dirArb = ( - depth: number, parentPath: string = '', { minFilePathSize, @@ -156,70 +166,53 @@ const dirArb = ( }: { minFilePathSize?: number; maxFilePathSize?: number; - } = {}, + } = { + minFilePathSize: 1, + maxFilePathSize: 512, + }, ): fc.Arbitrary => { - // Generate directory-specific records - const dirData = fc.record({ - type: fc.constant<'directory'>('directory'), - path: filenameArb(parentPath, { - minLength: minFilePathSize, - maxLength: maxFilePathSize, - }).map( - (name) => - `${ - !parentPath.endsWith('/') && parentPath !== '' - ? parentPath + '/' - : parentPath - }${name}/`, - ), + const dirPathArb = filenameArb(parentPath, { + minLength: minFilePathSize, + maxLength: maxFilePathSize, }); - // Add either subdirectories or files as children of the directory - const populatedDir = dirData.chain((dir) => { - // By default, there is a 1 in 4 chance of a subdirectory being created under - // this directory. However, if we have reached the maximum depth of recursion, - // then the directory weight drops to zero, ensuring all entries will be a - // file. - const dirWeight = depth > 0 ? 1 : 0; - - const fileOrDir = fc.oneof( - { - weight: 3, - arbitrary: fileArb(dir.path, undefined, { - minFilePathSize, - maxFilePathSize, - }), - }, - { - weight: dirWeight, - arbitrary: dirArb(depth - 1, dir.path, { - minFilePathSize, - maxFilePathSize, - }), - }, - ); + const slashedPathArb = dirPathArb.map((path) => + path.endsWith('/') ? path : path + '/', + ); - const children = fc.array(fileOrDir, { minLength: 0, maxLength: 4 }); - return children.map((children) => ({ ...dir, children })); + const dirData = fc.record({ + type: fc.constant<'directory'>('directory'), + path: slashedPathArb, + stat: statDataArb('directory'), }); - const dirWithStat = populatedDir.chain((dir) => - statDataArb('directory').map((stat) => ({ ...dir, stat })), - ); - - return dirWithStat.noShrink(); + return dirData.noShrink(); }; /** * Uses arbitraries generating files and directories to create a virtual file * system as a JSON object. */ -const fileTreeArb: fc.Arbitrary> = fc - .array(fc.oneof(fileArb(), dirArb(5)), { +const fileTreeArb = (): fc.Arbitrary> => { + const allEntries = fc.array(fc.oneof(fileArb(), dirArb()), { minLength: 1, maxLength: 10, - }) - .noShrink(); + }); + + const filteredEntries = allEntries.chain((entries) => { + const uniquePaths = new Set(); + const uniqueEntries = entries.filter((entry) => { + if (uniquePaths.has(entry.path)) { + return false; + } + uniquePaths.add(entry.path); + return true; + }); + return fc.constant(uniqueEntries); + }); + + return filteredEntries.noShrink(); +}; const tarEntryArb = ({ minFilePathSize, @@ -233,7 +226,7 @@ const tarEntryArb = ({ }> => { const data = fc.oneof( fileArb(undefined, undefined, { minFilePathSize, maxFilePathSize }), - dirArb(0, undefined, { minFilePathSize, maxFilePathSize }), + dirArb(undefined, { minFilePathSize, maxFilePathSize }), ); const headers = data.map((data) => { @@ -301,6 +294,7 @@ export { uidgidArb, sizeArb, mtimeArb, + unameGnameArb, filenameArb, fileContentArb, statDataArb, From 962263274af56d1de75759acb44f31c614318442 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Fri, 14 Mar 2025 18:32:28 +1100 Subject: [PATCH 16/19] docs: updated documentation --- src/Generator.ts | 150 ++++++++++++++++++++++++++++++++++++---------- src/Parser.ts | 92 ++++++++++++++++++++++++++++ src/VirtualTar.ts | 103 ++++++++++++++++++++++++++++++- src/constants.ts | 6 ++ 4 files changed, 316 insertions(+), 35 deletions(-) diff --git a/src/Generator.ts b/src/Generator.ts index d488d51..f0d2074 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -5,42 +5,67 @@ import * as errors from './errors'; import * as utils from './utils'; /** - * The TAR headers follow this structure: - * Start Size Description - * ------------------------------ - * 0 100 File name (first 100 bytes) - * 100 8 File mode (null-padded octal) - * 108 8 Owner user id (null-padded octal) - * 116 8 Owner group id (null-padded octal) - * 124 12 File size in bytes (null-padded octal, 0 for directories) - * 136 12 Mtime (null-padded octal) - * 148 8 Checksum (fill with ASCII spaces for computation) - * 156 1 Type flag ('0' for file, '5' for directory) - * 157 100 Link name (null-terminated ASCII/UTF-8) - * 257 6 'ustar\0' (magic string) - * 263 2 '00' (ustar version) - * 265 32 Owner user name (null-terminated ASCII/UTF-8) - * 297 32 Owner group name (null-terminated ASCII/UTF-8) - * 329 8 Device major (unset in this implementation) - * 337 8 Device minor (unset in this implementation) - * 345 155 File name (last 155 bytes, total 255 bytes, null-padded) - * 500 12 '\0' (unused) + * The Generator can be used to generate blocks for a tar archive. The generator + * can create three kinds of headers: FILE, DIRECTORY, and EXTENDED. The file and + * directory is expected, but the extended header is able to store additional + * metadata that does not fit in the standard header. * - * Note that all numbers are in stringified octal format. + * This class can also be used to generate data chunks padded to 512 bytes. Note + * that the chunk size shouldn't exceed 512 bytes. + * + * Note that the generator maintains an internal state and must be used for + * operations like generating data chunks, end chunks, or headers, otherwise an + * error will be thrown. + * + * For reference, this is the structure of a tar header. + * + * | Start | Size | Description | + * |--------|------|-----------------------------------------------------------| + * | 0 | 100 | File name (first 100 bytes) | + * | 100 | 8 | File mode (null-padded octal) | + * | 108 | 8 | Owner user ID (null-padded octal) | + * | 116 | 8 | Owner group ID (null-padded octal) | + * | 124 | 12 | File size in bytes (null-padded octal, 0 for directories) | + * | 136 | 12 | Mtime (null-padded octal) | + * | 148 | 8 | Checksum (fill with ASCII spaces for computation) | + * | 156 | 1 | Type flag ('0' for file, '5' for directory) | + * | 157 | 100 | Link name (null-terminated ASCII/UTF-8) | + * | 257 | 6 | 'ustar\0' (magic string) | + * | 263 | 2 | '00' (ustar version) | + * | 265 | 32 | Owner user name (null-terminated ASCII/UTF-8) | + * | 297 | 32 | Owner group name (null-terminated ASCII/UTF-8) | + * | 329 | 8 | Device major (unset in this implementation) | + * | 337 | 8 | Device minor (unset in this implementation) | + * | 345 | 155 | File name (last 155 bytes, total 255 bytes, null-padded) | + * | 500 | 12 | '\0' (unused) | + * + * Note that all numbers are in stringified octal format, as opposed to the + * numbers used in the extended header, which are all in stringified decimal. * * The following data will be left blank (null): * - Link name - * - Owner user name - * - Owner group name * - Device major * - Device minor * - * This is because this implementation does not interact with linked files. - * Owner user name and group name cannot be extracted via regular stat-ing, - * so it is left blank. In virtual situations, this field won't be useful - * anyways. The device major and minor are specific to linux kernel, which - * is not relevant to this virtual tar implementation. This is the reason - * these fields have been left blank. + * This is because this implementation does not interact with linked files. + * The device major and minor are specific to linux kernel, which is not + * relevant to this virtual tar implementation. This is the reason these fields + * have been left blank. + * + * The data for extended headers is formatted slightly differently, with the + * general format following this structure. + * =\n + * + * Here, the stands for the byte length of the entire line (including the + * size number itself, the space, the equals, and the \n). Unlike in regular + * strings, the end marker for a key-value pair is the \n (newline) character. + * Moreover, unlike the USTAR header, the numbers are written in stringified + * decimal format. + * + * The key can be any supported metadata key, and the value is binary data + * storing the actual value. These are the currently supported keys for + * the extended metadata: + * - path (corresponding to file path if it is longer than 255 characters) */ class Generator { protected state: GeneratorState = GeneratorState.HEADER; @@ -85,6 +110,7 @@ class Generator { filePath = filePath.endsWith('/') ? filePath : filePath + '/'; } + // Write the relevant sections in the header with the provided data utils.writeUstarMagic(header); utils.writeFileType(header, type); utils.writeFilePath(header, filePath); @@ -103,10 +129,27 @@ class Generator { return header; } + /** + * Generates a file header based on the file path and the stat. Note that the + * stat must provide a size for the file, but all other fields are optional. + * If the file path is longer than 255 characters, then an error will be + * thrown. An extended header needs to be generated first, then the file path + * can be set to an empty string. + * + * The content of the file must follow this header in separate chunks. + * + * @param filePath the path of the file relative to the tar root + * @param stat the stats of the file + * @returns one 512-byte chunk corresponding to the header + * + * @see {@link generateExtended} for generating headers with extended metadata + * @see {@link generateDirectory} for generating directory headers instead + * @see {@link generateData} for generating data chunks + */ generateFile(filePath: string, stat: FileStat): Uint8Array { if (this.state === GeneratorState.HEADER) { // Make sure the size is valid - if (stat.size == null) { + if (stat.size == null || stat.size < 0) { throw new errors.ErrorVirtualTarGeneratorInvalidStat( 'Files must have valid file sizes', ); @@ -130,6 +173,19 @@ class Generator { ); } + /** + * Generates a directory header based on the file path and the stat. Note that + * the size is ignored and set to 0 for directories. If the file path is longer + * than 255 characters, then an error will be thrown. An extended header needs + * to be generated first, then the file path can be set to an empty string. + * + * @param filePath the path of the file relative to the tar root + * @param stat the stats of the file + * @returns one 512-byte chunk corresponding to the header + * + * @see {@link generateExtended} for generating headers with extended metadata + * @see {@link generateFile} for generating file headers instead + */ generateDirectory(filePath: string, stat?: FileStat): Uint8Array { if (this.state === GeneratorState.HEADER) { // The size is zero for directories. Override this value in the stat if @@ -147,6 +203,14 @@ class Generator { ); } + /** + * Generates an extended metadata header based on the total size of the data + * following the header. If there is no need for extended metadata, then avoid + * using this, as it would just waste space. + * + * @param size the size of the binary data block containing the metadata + * @returns one 512-byte chunk corresponding to the header + */ generateExtended(size: number): Uint8Array { if (this.state === GeneratorState.HEADER) { this.state = GeneratorState.DATA; @@ -160,6 +224,22 @@ class Generator { ); } + /** + * Generates a data block. The input must be 512 bytes in size or smaller. The + * input data cannot be chunked smaller than 512 bytes. For example, if the + * file size is 1023 bytes, then you need to provide a 512-byte chunk first, + * then provide the remaining 511-byte chunk later. You can not chunk it up + * like sending over the first 100 bytes, then sending over the next 512. + * + * This method is used to generate blocks for both a file and the exnteded + * header. + * + * @param data a block of binary data (512-bytes at largest) + * @returns one 512-byte padded chunk corresponding to the data block + * + * @see {@link generateExtended} for generating headers with extended metadata + * @see {@link generateFile} for generating file headers preceeding data block + */ generateData(data: Uint8Array): Uint8Array { if (this.state === GeneratorState.DATA) { if (data.byteLength > constants.BLOCK_SIZE) { @@ -198,9 +278,13 @@ class Generator { ); } - // Creates a single null block. A null block is a block filled with all zeros. - // This is needed to end the archive, as two of these blocks mark the end of - // archive. + /** + * Generates a null chunk. Two invocations are needed to create a valid + * archive end marker. After two invocations, the generator state will be + * set to ENDED and no further data can be fed through the generator. + * + * @returns one 512-byte null chunk + */ generateEnd(): Uint8Array { switch (this.state) { case GeneratorState.HEADER: diff --git a/src/Parser.ts b/src/Parser.ts index 664fb86..4c5e76f 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -4,6 +4,64 @@ import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; +/** + * The Parser is used to parse blocks from a tar archive. Each written chunk can + * return either a token or undefined. Undefined will only be returned when + * parsing the first null chunk which signifies that the archive has ended. The + * tokens can be either a header token corresponding to either a file, a + * directory, or an extended header, a data token returning the data, and an end + * token signifiying the ending of the archive. + * + * For reference, this is the structure of a tar header. + * + * | Start | Size | Description | + * |--------|------|-----------------------------------------------------------| + * | 0 | 100 | File name (first 100 bytes) | + * | 100 | 8 | File mode (null-padded octal) | + * | 108 | 8 | Owner user ID (null-padded octal) | + * | 116 | 8 | Owner group ID (null-padded octal) | + * | 124 | 12 | File size in bytes (null-padded octal, 0 for directories) | + * | 136 | 12 | Mtime (null-padded octal) | + * | 148 | 8 | Checksum (fill with ASCII spaces for computation) | + * | 156 | 1 | Type flag ('0' for file, '5' for directory) | + * | 157 | 100 | Link name (null-terminated ASCII/UTF-8) | + * | 257 | 6 | 'ustar\0' (magic string) | + * | 263 | 2 | '00' (ustar version) | + * | 265 | 32 | Owner user name (null-terminated ASCII/UTF-8) | + * | 297 | 32 | Owner group name (null-terminated ASCII/UTF-8) | + * | 329 | 8 | Device major (unset in this implementation) | + * | 337 | 8 | Device minor (unset in this implementation) | + * | 345 | 155 | File name (last 155 bytes, total 255 bytes, null-padded) | + * | 500 | 12 | '\0' (unused) | + * + * Note that all numbers are in stringified octal format, as opposed to the + * numbers used in the extended header, which are all in stringified decimal. + * + * The following data will be left blank (null): + * - Link name + * - Device major + * - Device minor + * + * This is because this implementation does not interact with linked files. + * The device major and minor are specific to linux kernel, which is not + * relevant to this virtual tar implementation. This is the reason these fields + * have been left blank. + * + * The data for extended headers is formatted slightly differently, with the + * general format following this structure. + * =\n + * + * Here, the stands for the byte length of the entire line (including the + * size number itself, the space, the equals, and the \n). Unlike in regular + * strings, the end marker for a key-value pair is the \n (newline) character. + * Moreover, unlike the USTAR header, the numbers are written in stringified + * decimal format. + * + * The key can be any supported metadata key, and the value is binary data + * storing the actual value. These are the currently supported keys for + * the extended metadata: + * - path (corresponding to file path if it is longer than 255 characters) + */ class Parser { protected state: ParserState = ParserState.HEADER; protected remainingBytes = 0; @@ -67,6 +125,40 @@ class Parser { } } + /** + * Each chunk in a tar archive is exactly 512 bytes long. This chunk needs to + * be written to the parser, which will return a single token. This token can + * be one of a header token, a data token, an end token, or undefined. The + * undefined token is only returned when the chunk does not correspond to an + * actual token. For example, the first null chunk in the archive end marker + * will return an undefined. The second null chunk will return an end token. + * + * The header token can return different types of headers. The three supported + * headers are FILE, DIRECTORY, and EXTENDED. Note that the file stat is + * returned with each header. It might contain default values if it was not + * set in the header. The default value for strings is '', for numbers is 0, + * and for dates is Date(0), which is 11:00 AM 1 January 1970. + * + * Note that extended headers will not be automatically parsed. If some + * metadata was put into the extended header instead, then it will need to be + * parsed separately to get the information out, and the metadata field in the + * header will contain the default value for its type. + * + * A data header is pretty simple, containing the bytes of the file. Note that + * this is not aligned to the 512-byte boundary. For example, if a file has + * 513 bytes of data, then the first chunk will return the 512 bytes of data, + * and the next data chunk will return 1 byte, removing the padding. The data + * token also has another field, `end`. This is a boolean which is true when + * the last chunk of data is being sent. The expected token after an ended + * data token is a header or an end token. + * + * The end token signifies that the archive has ended. This sets the internal + * state to ENDED, and no further data can be written to it and attempts to + * write any additional data will throw an error. + * + * @param data a single 512-byte chunk from the tar file + * @returns a parsed token, or undefined if no tokens can be returned + */ write(data: Uint8Array): TokenHeader | TokenData | TokenEnd | undefined { if (data.byteLength !== constants.BLOCK_SIZE) { throw new errors.ErrorVirtualTarParserBlockSize( diff --git a/src/VirtualTar.ts b/src/VirtualTar.ts index d94b309..079f1de 100644 --- a/src/VirtualTar.ts +++ b/src/VirtualTar.ts @@ -12,6 +12,11 @@ import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; +/** + * VirtualTar is a library used to create tar files using a virtual file system + * or a file tree. This library aims to provide a generator-parser pair to + * create tar files without the reliance on a file system. + */ class VirtualTar { protected ended: boolean; protected state: VirtualTarState; @@ -38,6 +43,39 @@ class VirtualTar { protected endCallback: () => void; protected callbacks: Array>; + /** + * Create a new VirtualTar object initialized to a set mode. If the mode is + * set to generate an archive, then operations involving parsing an archive + * would be unavailable, and vice-versa. + * + * When parsing a tar file, optional callbacks are used to perform actions + * on events. The `onFile` callback is triggered when a file is parsed. The + * file data is queued up via an AsyncGenerator. The `onDirectory` callback + * functions similarly, but without the presence of data. The `onEnd` callback + * is triggered when the parser has generated an end token marking the archive + * as completely parsed. + * + * Note that the file and directory callbacks aren't awaited, and instead + * appended to an internal buffer of callbacks. Thus, {@link settled} can be + * used to wait for all the pending promises to be completed. + * + * Note that each callback is not blocking, so it is possible that two + * callbacks might try to modify the same resource. + * + * This method works slightly differently if an archive is being generated. + * The operation of adding files to the archive will be added to an internal + * buffer tracking all such 'operations', and the generated data can be + * extracted via {@link yieldChunks}. In this case, awaiting {@link settled} + * will wait until this internal queue of operations is empty. + * + * @param mode one of 'generate' or 'parse' + * @param onFile optional callback when a file has been parsed + * @param onDirectory optional callback when a directory has been parsed + * @param onEnd optional callback when the archive has ended + * + * @see {@link settled} + * @see {@link yieldChunks} + */ constructor({ mode, onFile, @@ -106,6 +144,15 @@ class VirtualTar { } } + /** + * Queue up an operation to add a file to the archive. + * + * Only usable when generating an archive. + * + * @param filePath path of the file relative to the tar root + * @param stat the stats of the file + * @param data either a generator yielding data, a buffer, or a string + */ public addFile( filePath: string, stat: FileStat, @@ -220,6 +267,14 @@ class VirtualTar { }); } + /** + * Queue up an operation to add a directory to the archive. + * + * Only usable when generating an archive. + * + * @param filePath path of the directory relative to the tar root + * @param stat the stats of the directory + */ public addDirectory(filePath: string, stat?: FileStat): void { if (this.state !== VirtualTarState.GENERATOR) { throw new errors.ErrorVirtualTarInvalidState( @@ -233,6 +288,12 @@ class VirtualTar { }); } + /** + * Queue up an operation to finalize the archive by adding two null chunks + * indicating the end of archive. + * + * Only usable when generating an archive. + */ public finalize(): void { if (this.state !== VirtualTarState.GENERATOR) { throw new errors.ErrorVirtualTarInvalidState( @@ -247,6 +308,19 @@ class VirtualTar { this.ended = true; } + /** + * While generating, this waits for the internal queue of operations to empty + * before resolving. Note that if nothing is consuming the data in the queue, + * then this promise will keep waiting. + * + * While parsing, this waits for the internal queue of callbacks to resolve. + * Note that each callback is not blocking, so it is possible that two + * callbacks might try to modify the same resource. + * + * Only usable when generating an archive. + * + * @see {@link yieldChunks} to consume the operations and yield binary chunks + */ public async settled(): Promise { if (this.state === VirtualTarState.GENERATOR) { this.settledP = new Promise((resolve) => { @@ -258,6 +332,12 @@ class VirtualTar { } } + /** + * Returns a generator which yields 512-byte chunks as they are generated from + * the queued operations. + * + * Only usable when generating an archive. + */ public async *yieldChunks(): AsyncGenerator { while (true) { const gen = this.queue.shift(); @@ -279,6 +359,21 @@ class VirtualTar { } } + /** + * Writes a chunk to the internal buffer. If the size of the internal buffer + * is larger than or equal to 512 bytes, then the chunks are consumed from + * the buffer until the buffer falls below this limit. + * + * Upon yielding a file or directory token, the relevant data is passed along + * to the relevant callback. Note tha the callbacks are queued, so call + * {@link settle} to wait for all the pending callbacks to resolve. The end + * callback is synchronous, so it is executed immeidately instead of being + * queued. + * + * Only usable when parsing an archive. + * + * @param chunk a chunk of the (or the entire) binary tar file + */ public write(chunk: Uint8Array): void { if (this.state !== VirtualTarState.PARSER) { throw new errors.ErrorVirtualTarInvalidState( @@ -401,8 +496,12 @@ class VirtualTar { this.workingMetadata = utils.decodeExtendedHeader(data); } } else { - // Token is of type end - this.endCallback(); + // Token is of type end. Clean up the pending promises then trigger the + // end callback. + (async () => { + await this.settled(); + this.endCallback(); + })(); } } } diff --git a/src/constants.ts b/src/constants.ts index 6ab9b20..0b84307 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,10 @@ +// Each block in a tar file must be exactly 512 bytes export const BLOCK_SIZE = 512; + +// A standard header can fit a path with size 255 bytes before an extended +// header is needed to store the additional data. export const STANDARD_PATH_SIZE = 255; + +// Magic values to indicate a header being a valid tar header export const USTAR_NAME = 'ustar'; export const USTAR_VERSION = '00'; From b5a91742654c956be9d863f1b8550ab8cf165cf5 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Fri, 14 Mar 2025 18:38:40 +1100 Subject: [PATCH 17/19] docs: added tar stream format in the docs --- src/Generator.ts | 18 ++++++++++++++++++ src/Parser.ts | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Generator.ts b/src/Generator.ts index f0d2074..add40d5 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -66,6 +66,24 @@ import * as utils from './utils'; * storing the actual value. These are the currently supported keys for * the extended metadata: * - path (corresponding to file path if it is longer than 255 characters) + * + * The high-level diagram of a tar file looks like the following. + * - [File header] + * - [Data] + * - [Data] + * - [Extended header] + * - [Data] + * - [File header] + * - [Data] + * - [Data] + * - [Directory header] + * - [Null chunk] + * - [Null chunk] + * + * A file header preceedes file data. A directory header has no data. An + * extended header is the same as a file header, but it has differnet metadata + * than one, and must be immediately followed by either a file or a directory + * header. Two null chunks are always at the end, marking the end of archive. */ class Generator { protected state: GeneratorState = GeneratorState.HEADER; diff --git a/src/Parser.ts b/src/Parser.ts index 4c5e76f..a777468 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -61,6 +61,24 @@ import * as utils from './utils'; * storing the actual value. These are the currently supported keys for * the extended metadata: * - path (corresponding to file path if it is longer than 255 characters) + * + * The high-level diagram of a tar file looks like the following. + * - [File header] + * - [Data] + * - [Data] + * - [Extended header] + * - [Data] + * - [File header] + * - [Data] + * - [Data] + * - [Directory header] + * - [Null chunk] + * - [Null chunk] + * + * A file header preceedes file data. A directory header has no data. An + * extended header is the same as a file header, but it has differnet metadata + * than one, and must be immediately followed by either a file or a directory + * header. Two null chunks are always at the end, marking the end of archive. */ class Parser { protected state: ParserState = ParserState.HEADER; From 7736870a044de86d539bc4553e80bc5025eda3e2 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 19 Mar 2025 14:48:23 +1100 Subject: [PATCH 18/19] feat: split VirtualTar to VirtualTarParser and VirtualTarGenerator --- package.json | 2 +- src/Generator.ts | 26 +- src/Parser.ts | 38 ++- src/VirtualTar.ts | 510 ------------------------------ src/VirtualTarGenerator.ts | 261 +++++++++++++++ src/VirtualTarParser.ts | 335 ++++++++++++++++++++ src/constants.ts | 40 +++ src/index.ts | 2 + src/types.ts | 86 +---- src/utils.ts | 179 ++++++----- tests/Parser.test.ts | 4 +- tests/VirtualTar.test.ts | 246 -------------- tests/VirtualTarGenerator.test.ts | 112 +++++++ tests/VirtualTarParser.test.ts | 69 ++++ tests/index.test.ts | 15 +- tests/integration.test.ts | 58 +++- 16 files changed, 1033 insertions(+), 950 deletions(-) delete mode 100644 src/VirtualTar.ts create mode 100644 src/VirtualTarGenerator.ts create mode 100644 src/VirtualTarParser.ts delete mode 100644 tests/VirtualTar.test.ts create mode 100644 tests/VirtualTarGenerator.test.ts create mode 100644 tests/VirtualTarParser.test.ts diff --git a/package.json b/package.json index a7ba8a3..ef26ccd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@matrixai/js-virtualtar", - "version": "0.0.1", + "version": "1.16.3", "author": "Matrix AI", "contributors": [ { diff --git a/src/Generator.ts b/src/Generator.ts index add40d5..0992e6e 100644 --- a/src/Generator.ts +++ b/src/Generator.ts @@ -1,5 +1,5 @@ import type { FileType, FileStat } from './types'; -import { GeneratorState, HeaderSize } from './types'; +import { GeneratorState } from './types'; import * as constants from './constants'; import * as errors from './errors'; import * as utils from './utils'; @@ -106,18 +106,21 @@ class Generator { ); } - if (stat?.uname != null && stat?.uname.length > HeaderSize.OWNER_USERNAME) { + if ( + stat?.uname != null && + stat?.uname.length > constants.HEADER_SIZE.OWNER_USERNAME + ) { throw new errors.ErrorVirtualTarGeneratorInvalidStat( - `The username must not exceed ${HeaderSize.OWNER_USERNAME} bytes`, + `The username must not exceed ${constants.HEADER_SIZE.OWNER_USERNAME} bytes`, ); } if ( stat?.gname != null && - stat?.gname.length > HeaderSize.OWNER_GROUPNAME + stat?.gname.length > constants.HEADER_SIZE.OWNER_GROUPNAME ) { throw new errors.ErrorVirtualTarGeneratorInvalidStat( - `The groupname must not exceed ${HeaderSize.OWNER_GROUPNAME} bytes`, + `The groupname must not exceed ${constants.HEADER_SIZE.OWNER_GROUPNAME} bytes`, ); } @@ -129,16 +132,16 @@ class Generator { } // Write the relevant sections in the header with the provided data - utils.writeUstarMagic(header); - utils.writeFileType(header, type); utils.writeFilePath(header, filePath); utils.writeFileMode(header, stat.mode); utils.writeOwnerUid(header, stat.uid); utils.writeOwnerGid(header, stat.gid); - utils.writeOwnerUserName(header, stat.uname); - utils.writeOwnerGroupName(header, stat.gname); utils.writeFileSize(header, stat.size); utils.writeFileMtime(header, stat.mtime); + utils.writeFileType(header, type); + utils.writeUstarMagic(header); + utils.writeOwnerUserName(header, stat.uname); + utils.writeOwnerGroupName(header, stat.gname); // The checksum can only be calculated once the entire header has been // written. This is why the checksum is calculated and written at the end. @@ -307,10 +310,10 @@ class Generator { switch (this.state) { case GeneratorState.HEADER: this.state = GeneratorState.NULL; - break; + return new Uint8Array(constants.BLOCK_SIZE); case GeneratorState.NULL: this.state = GeneratorState.ENDED; - break; + return new Uint8Array(constants.BLOCK_SIZE); default: throw new errors.ErrorVirtualTarGeneratorInvalidState( `Expected state ${GeneratorState[GeneratorState.HEADER]} or ${ @@ -318,7 +321,6 @@ class Generator { } but got ${GeneratorState[this.state]}`, ); } - return new Uint8Array(constants.BLOCK_SIZE); } } diff --git a/src/Parser.ts b/src/Parser.ts index a777468..f472ebf 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -84,10 +84,10 @@ class Parser { protected state: ParserState = ParserState.HEADER; protected remainingBytes = 0; - protected parseHeader(array: Uint8Array): TokenHeader { + protected parseHeader(header: Uint8Array): TokenHeader { // Validate header by checking checksum and magic string - const headerChecksum = utils.decodeChecksum(array); - const calculatedChecksum = utils.calculateChecksum(array); + const headerChecksum = utils.decodeChecksum(header); + const calculatedChecksum = utils.calculateChecksum(header); if (headerChecksum !== calculatedChecksum) { throw new errors.ErrorVirtualTarParserInvalidHeader( @@ -95,14 +95,14 @@ class Parser { ); } - const ustarMagic = utils.decodeUstarMagic(array); + const ustarMagic = utils.decodeUstarMagic(header); if (ustarMagic !== constants.USTAR_NAME) { throw new errors.ErrorVirtualTarParserInvalidHeader( `Expected ustar magic to be '${constants.USTAR_NAME}', got '${ustarMagic}'`, ); } - const ustarVersion = utils.decodeUstarVersion(array); + const ustarVersion = utils.decodeUstarVersion(header); if (ustarVersion !== constants.USTAR_VERSION) { throw new errors.ErrorVirtualTarParserInvalidHeader( `Expected ustar version to be '${constants.USTAR_VERSION}', got '${ustarVersion}'`, @@ -110,15 +110,15 @@ class Parser { } // Extract the relevant metadata from the header - const filePath = utils.decodeFilePath(array); - const fileSize = utils.decodeFileSize(array); - const fileMtime = utils.decodeFileMtime(array); - const fileMode = utils.decodeFileMode(array); - const ownerUid = utils.decodeOwnerUid(array); - const ownerGid = utils.decodeOwnerGid(array); - const ownerUserName = utils.decodeOwnerUserName(array); - const ownerGroupName = utils.decodeOwnerGroupName(array); - const fileType = utils.decodeFileType(array); + const filePath = utils.decodeFilePath(header); + const fileMode = utils.decodeFileMode(header); + const ownerUid = utils.decodeOwnerUid(header); + const ownerGid = utils.decodeOwnerGid(header); + const fileSize = utils.decodeFileSize(header); + const fileMtime = utils.decodeFileMtime(header); + const fileType = utils.decodeFileType(header); + const ownerUserName = utils.decodeOwnerUserName(header); + const ownerGroupName = utils.decodeOwnerGroupName(header); return { type: 'header', @@ -212,6 +212,12 @@ class Parser { this.state = ParserState.DATA; this.remainingBytes = headerToken.fileSize; } + + // Only the file header and the extended header can potentially have + // additional data blocks following them. This needs to be tracked in + // the parser state. Directory headers don't have this issue and doesn't + // need any additional processing. + return headerToken; } @@ -225,7 +231,7 @@ class Parser { case ParserState.NULL: { if (utils.isNullBlock(data)) { this.state = ParserState.ENDED; - return { type: 'end' } as TokenEnd; + return { type: 'end' }; } else { throw new errors.ErrorVirtualTarParserEndOfArchive( 'Received garbage data after first end marker', @@ -234,7 +240,7 @@ class Parser { } default: - utils.never('Unexpected state'); + utils.never(`Unexpected state: ${this.state}`); } } } diff --git a/src/VirtualTar.ts b/src/VirtualTar.ts deleted file mode 100644 index 079f1de..0000000 --- a/src/VirtualTar.ts +++ /dev/null @@ -1,510 +0,0 @@ -import type { - FileStat, - ParsedFile, - ParsedDirectory, - MetadataKeywords, - TokenData, -} from './types'; -import { VirtualTarState } from './types'; -import Generator from './Generator'; -import Parser from './Parser'; -import * as constants from './constants'; -import * as errors from './errors'; -import * as utils from './utils'; - -/** - * VirtualTar is a library used to create tar files using a virtual file system - * or a file tree. This library aims to provide a generator-parser pair to - * create tar files without the reliance on a file system. - */ -class VirtualTar { - protected ended: boolean; - protected state: VirtualTarState; - protected generator: Generator; - protected parser: Parser; - protected queue: Array<() => AsyncGenerator>; - protected encoder = new TextEncoder(); - protected workingAccumulator: Uint8Array; - protected workingTokenType: 'file' | 'extended' | undefined; - protected workingData: Array; - protected workingDataQueue: Array; - protected workingMetadata: - | Partial> - | undefined; - protected resolveWaitP: (() => void) | undefined; - protected resolveWaitDataP: (() => void) | undefined; - protected settledP: Promise | undefined; - protected resolveSettledP: (() => void) | undefined; - protected fileCallback: ( - header: ParsedFile, - data: () => AsyncGenerator, - ) => Promise; - protected directoryCallback: (header: ParsedDirectory) => Promise; - protected endCallback: () => void; - protected callbacks: Array>; - - /** - * Create a new VirtualTar object initialized to a set mode. If the mode is - * set to generate an archive, then operations involving parsing an archive - * would be unavailable, and vice-versa. - * - * When parsing a tar file, optional callbacks are used to perform actions - * on events. The `onFile` callback is triggered when a file is parsed. The - * file data is queued up via an AsyncGenerator. The `onDirectory` callback - * functions similarly, but without the presence of data. The `onEnd` callback - * is triggered when the parser has generated an end token marking the archive - * as completely parsed. - * - * Note that the file and directory callbacks aren't awaited, and instead - * appended to an internal buffer of callbacks. Thus, {@link settled} can be - * used to wait for all the pending promises to be completed. - * - * Note that each callback is not blocking, so it is possible that two - * callbacks might try to modify the same resource. - * - * This method works slightly differently if an archive is being generated. - * The operation of adding files to the archive will be added to an internal - * buffer tracking all such 'operations', and the generated data can be - * extracted via {@link yieldChunks}. In this case, awaiting {@link settled} - * will wait until this internal queue of operations is empty. - * - * @param mode one of 'generate' or 'parse' - * @param onFile optional callback when a file has been parsed - * @param onDirectory optional callback when a directory has been parsed - * @param onEnd optional callback when the archive has ended - * - * @see {@link settled} - * @see {@link yieldChunks} - */ - constructor({ - mode, - onFile, - onDirectory, - onEnd, - }: { - mode: 'generate' | 'parse'; - onFile?: ( - header: ParsedFile, - data: () => AsyncGenerator, - ) => Promise; - onDirectory?: (header: ParsedDirectory) => Promise; - onEnd?: () => void; - }) { - if (mode === 'generate') { - if (onFile != null || onDirectory != null || onEnd != null) { - throw new errors.ErrorVirtualTar( - 'VirtualTar in generate mode does not support callbacks', - ); - } - this.state = VirtualTarState.GENERATOR; - this.generator = new Generator(); - this.queue = []; - } else { - this.state = VirtualTarState.PARSER; - this.parser = new Parser(); - this.workingData = []; - this.workingDataQueue = []; - this.workingAccumulator = new Uint8Array(); - this.callbacks = []; - this.directoryCallback = onDirectory ?? (() => Promise.resolve()); - this.fileCallback = onFile ?? (() => Promise.resolve()); - this.endCallback = onEnd ?? (() => {}); - } - } - - protected async *generateHeader( - filePath: string, - stat: FileStat = {}, - type: 'file' | 'directory', - ): AsyncGenerator { - if (filePath.length > constants.STANDARD_PATH_SIZE) { - // Push the extended metadata header - const data = utils.encodeExtendedHeader({ path: filePath }); - yield this.generator.generateExtended(data.byteLength); - - // Push the content - for ( - let offset = 0; - offset < data.byteLength; - offset += constants.BLOCK_SIZE - ) { - yield this.generator.generateData( - data.subarray(offset, offset + constants.BLOCK_SIZE), - ); - } - } - - filePath = filePath.length <= 255 ? filePath : ''; - - // Generate the header - if (type === 'file') { - yield this.generator.generateFile(filePath, stat); - } else { - yield this.generator.generateDirectory(filePath, stat); - } - } - - /** - * Queue up an operation to add a file to the archive. - * - * Only usable when generating an archive. - * - * @param filePath path of the file relative to the tar root - * @param stat the stats of the file - * @param data either a generator yielding data, a buffer, or a string - */ - public addFile( - filePath: string, - stat: FileStat, - data: () => AsyncGenerator, - ): void; - public addFile(filePath: string, stat: FileStat, data: Uint8Array): void; - public addFile(filePath: string, stat: FileStat, data: string): void; - public addFile( - filePath: string, - stat: FileStat, - data: - | Uint8Array - | string - | (() => AsyncGenerator), - ): void { - if (this.state !== VirtualTarState.GENERATOR) { - throw new errors.ErrorVirtualTarInvalidState( - 'VirtualTar is not in generator mode', - ); - } - - const globalThis = this; - this.queue.push(async function* () { - // Generate the header chunks (including extended header) - yield* globalThis.generateHeader(filePath, stat, 'file'); - if (globalThis.resolveWaitP != null) { - globalThis.resolveWaitP(); - globalThis.resolveWaitP = undefined; - } - - // The base case of generating data is to have a async generator yielding - // data, but in case the data is passed as an entire buffer or a string, - // we need to chunk it up and wrap it in the async generator. - let gen: AsyncGenerator; - if (typeof data === 'function') { - let workingBuffer: Array = []; - let bufferSize = 0; - - // Ensure the data is properly converted into Uint8Arrays - gen = (async function* () { - for await (const chunk of data()) { - let chunkBytes: Uint8Array; - if (typeof chunk === 'string') { - chunkBytes = globalThis.encoder.encode(chunk); - } else { - chunkBytes = chunk; - } - workingBuffer.push(chunkBytes); - bufferSize += chunkBytes.byteLength; - - while (bufferSize >= constants.BLOCK_SIZE) { - // Flatten buffer into one Uint8Array - const fullBuffer = utils.concatUint8Arrays(...workingBuffer); - - yield globalThis.generator.generateData( - fullBuffer.slice(0, constants.BLOCK_SIZE), - ); - - // Remove processed bytes from buffer - const remaining = fullBuffer.slice(constants.BLOCK_SIZE); - workingBuffer = []; - if (remaining.byteLength > 0) workingBuffer.push(remaining); - bufferSize = remaining.byteLength; - - if (globalThis.resolveWaitP != null) { - globalThis.resolveWaitP(); - globalThis.resolveWaitP = undefined; - } - } - } - if (bufferSize !== 0) { - yield globalThis.generator.generateData( - utils.concatUint8Arrays(...workingBuffer), - ); - } - })(); - } else { - // Ensure that the data is being chunked up to 512 bytes - gen = (async function* () { - if (data instanceof Uint8Array) { - for ( - let offset = 0; - offset < data.byteLength; - offset += constants.BLOCK_SIZE - ) { - const chunk = data.subarray( - offset, - offset + constants.BLOCK_SIZE, - ); - yield globalThis.generator.generateData(chunk); - if (globalThis.resolveWaitP != null) { - globalThis.resolveWaitP(); - globalThis.resolveWaitP = undefined; - } - } - } else { - while (data.length > 0) { - const chunk = globalThis.encoder.encode( - data.slice(0, constants.BLOCK_SIZE), - ); - yield globalThis.generator.generateData(chunk); - data = data.slice(constants.BLOCK_SIZE); - if (globalThis.resolveWaitP != null) { - globalThis.resolveWaitP(); - globalThis.resolveWaitP = undefined; - } - } - } - })(); - } - yield* gen; - }); - } - - /** - * Queue up an operation to add a directory to the archive. - * - * Only usable when generating an archive. - * - * @param filePath path of the directory relative to the tar root - * @param stat the stats of the directory - */ - public addDirectory(filePath: string, stat?: FileStat): void { - if (this.state !== VirtualTarState.GENERATOR) { - throw new errors.ErrorVirtualTarInvalidState( - 'VirtualTar is not in generator mode', - ); - } - - const globalThis = this; - this.queue.push(async function* () { - yield* globalThis.generateHeader(filePath, stat, 'directory'); - }); - } - - /** - * Queue up an operation to finalize the archive by adding two null chunks - * indicating the end of archive. - * - * Only usable when generating an archive. - */ - public finalize(): void { - if (this.state !== VirtualTarState.GENERATOR) { - throw new errors.ErrorVirtualTarInvalidState( - 'VirtualTar is not in generator mode', - ); - } - const globalThis = this; - this.queue.push(async function* () { - yield globalThis.generator.generateEnd(); - yield globalThis.generator.generateEnd(); - }); - this.ended = true; - } - - /** - * While generating, this waits for the internal queue of operations to empty - * before resolving. Note that if nothing is consuming the data in the queue, - * then this promise will keep waiting. - * - * While parsing, this waits for the internal queue of callbacks to resolve. - * Note that each callback is not blocking, so it is possible that two - * callbacks might try to modify the same resource. - * - * Only usable when generating an archive. - * - * @see {@link yieldChunks} to consume the operations and yield binary chunks - */ - public async settled(): Promise { - if (this.state === VirtualTarState.GENERATOR) { - this.settledP = new Promise((resolve) => { - this.resolveSettledP = resolve; - }); - await this.settledP; - } else { - await Promise.allSettled(this.callbacks); - } - } - - /** - * Returns a generator which yields 512-byte chunks as they are generated from - * the queued operations. - * - * Only usable when generating an archive. - */ - public async *yieldChunks(): AsyncGenerator { - while (true) { - const gen = this.queue.shift(); - if (gen == null) { - // We have gone through all the buffered tasks. Check if we have ended - // yet or we are still going. - if (this.ended) break; - if (this.resolveSettledP != null) this.resolveSettledP(); - - // Wait until more data is available - const waitP = new Promise((resolve) => { - this.resolveWaitP = resolve; - }); - await waitP; - continue; - } - - yield* gen(); - } - } - - /** - * Writes a chunk to the internal buffer. If the size of the internal buffer - * is larger than or equal to 512 bytes, then the chunks are consumed from - * the buffer until the buffer falls below this limit. - * - * Upon yielding a file or directory token, the relevant data is passed along - * to the relevant callback. Note tha the callbacks are queued, so call - * {@link settle} to wait for all the pending callbacks to resolve. The end - * callback is synchronous, so it is executed immeidately instead of being - * queued. - * - * Only usable when parsing an archive. - * - * @param chunk a chunk of the (or the entire) binary tar file - */ - public write(chunk: Uint8Array): void { - if (this.state !== VirtualTarState.PARSER) { - throw new errors.ErrorVirtualTarInvalidState( - 'VirtualTar is not in parser mode', - ); - } - - // Update the working accumulator - this.workingAccumulator = utils.concatUint8Arrays( - this.workingAccumulator, - chunk, - ); - - while (this.workingAccumulator.byteLength >= constants.BLOCK_SIZE) { - const block = this.workingAccumulator.slice(0, constants.BLOCK_SIZE); - this.workingAccumulator = this.workingAccumulator.slice( - constants.BLOCK_SIZE, - ); - const token = this.parser.write(block); - if (token == null) continue; - - if (token.type === 'header') { - // If we have an extended header, then set the working header to the - // extended type and continue. Otherwise, if we have a file, then set - // the token type to file. - if (token.fileType === 'extended') { - this.workingTokenType = 'extended'; - continue; - } else if (token.fileType === 'file') { - this.workingTokenType = 'file'; - } else { - this.workingTokenType = undefined; - } - - // If we have additional metadata, then use it to override token data - let filePath = token.filePath; - if (this.workingMetadata != null) { - filePath = this.workingMetadata.path ?? filePath; - this.workingMetadata = undefined; - } - - if (token.fileType === 'directory') { - const p = this.directoryCallback({ - type: 'directory', - path: filePath, - stat: { - size: token.fileSize, - mode: token.fileMode, - mtime: token.fileMtime, - uid: token.ownerUid, - gid: token.ownerGid, - uname: token.ownerUserName, - gname: token.ownerGroupName, - }, - }); - this.callbacks.push(p); - continue; - } else if (token.fileType === 'file') { - const globalThis = this; - const p = this.fileCallback( - { - type: 'file', - path: filePath, - stat: { - size: token.fileSize, - mode: token.fileMode, - mtime: token.fileMtime, - uid: token.ownerUid, - gid: token.ownerGid, - uname: token.ownerUserName, - gname: token.ownerGroupName, - }, - }, - async function* (): AsyncGenerator { - // Return early if no data will be coming - if (token.fileSize === 0) return; - - while (true) { - const chunk = globalThis.workingData.shift(); - if (chunk == null) { - await new Promise((resolve) => { - globalThis.resolveWaitDataP = resolve; - }); - continue; - } - yield chunk.data; - if (chunk.end) break; - } - }, - ); - this.callbacks.push(p); - continue; - } - } else if (token.type === 'data') { - if (this.workingTokenType == null) { - throw new errors.ErrorVirtualTarInvalidState( - 'Received data token before header token', - ); - } - - this.workingData.push(token); - - // If we are working on a file, then signal that we have gotten more - // data. - if (this.resolveWaitDataP != null) { - this.resolveWaitDataP(); - } - - // If we are working on a metadata token, then we need to collect the - // entire data array as we need to decode it to file stat which needs to - // sit in memory anyways. - if (token.end && this.workingTokenType === 'extended') { - // Concat the working data into a single Uint8Array - const data = utils.concatUint8Arrays( - ...this.workingData.map(({ data }) => data), - ); - this.workingData = []; - - // Decode the extended header - this.workingMetadata = utils.decodeExtendedHeader(data); - } - } else { - // Token is of type end. Clean up the pending promises then trigger the - // end callback. - (async () => { - await this.settled(); - this.endCallback(); - })(); - } - } - } -} - -export default VirtualTar; diff --git a/src/VirtualTarGenerator.ts b/src/VirtualTarGenerator.ts new file mode 100644 index 0000000..513db24 --- /dev/null +++ b/src/VirtualTarGenerator.ts @@ -0,0 +1,261 @@ +import type { FileStat } from './types'; +import Generator from './Generator'; +import * as constants from './constants'; +import * as utils from './utils'; + +/** + * VirtualTar is a library used to create tar files using a virtual file system + * or a file tree. This library aims to provide a generator-parser pair to + * create tar files without the reliance on a file system. + * + * This class is dedicated to generate an archive to be parsed by the parser. + * + * The operation of adding files to the archive will be added to an internal + * buffer tracking all such 'operations', and the generated data can be + * extracted via {@link yieldchunks}. In this case, awaiting {@link settled} + * will wait until this internal queue of operations is empty. + * + * @see {@link settled} + * @see {@link yieldchunks} + */ +class VirtualTarGenerator { + /** + * This flag tells the generator that no further data will be added to the + * queue. This is the exit condition required to exit yielding chunks. + */ + protected ended: boolean; + + /** + * The generator object which generates a tar chunk given some file stats. + */ + protected generator: Generator = new Generator(); + + /** + * The queue stores all the async generators which can yield chunks. The + * generators in this queue are consumed in {@link yieldChunks}. + */ + protected queue: Array<() => AsyncGenerator> = []; + + /** + * This callback resolves a promise waiting for more chunks to be added to the + * queue. + */ + protected resolveWaitChunksP: (() => void) | undefined; + + /** + * This callback resolves a promise waiting for more data to be added to a + * file. + */ + protected resolveWaitDataP: (() => void) | undefined; + + /** + * This callback resolves a promise waiting for the queue to be drained. + */ + protected resolveSettledP: (() => void) | undefined; + + protected async *generateHeader( + filePath: string, + stat: FileStat = {}, + type: 'file' | 'directory', + ): AsyncGenerator { + if (filePath.length > constants.STANDARD_PATH_SIZE) { + // Push the extended metadata header + const data = utils.encodeExtendedHeader({ path: filePath }); + yield this.generator.generateExtended(data.byteLength); + + // Push the content + for ( + let offset = 0; + offset < data.byteLength; + offset += constants.BLOCK_SIZE + ) { + yield this.generator.generateData( + data.subarray(offset, offset + constants.BLOCK_SIZE), + ); + } + } + + filePath = filePath.length <= 255 ? filePath : ''; + + // Generate the header + if (type === 'file') { + yield this.generator.generateFile(filePath, stat); + } else { + yield this.generator.generateDirectory(filePath, stat); + } + } + + /** + * Queue up an operation to add a file to the archive. + * + * @param filePath path of the file relative to the tar root + * @param stat the stats of the file + * @param data either a generator yielding data, a buffer, or a string + */ + public addFile( + filePath: string, + stat: FileStat, + data: () => AsyncGenerator, + ): void; + public addFile(filePath: string, stat: FileStat, data: Uint8Array): void; + public addFile(filePath: string, stat: FileStat, data: string): void; + public addFile( + filePath: string, + stat: FileStat, + data: + | Uint8Array + | string + | (() => AsyncGenerator), + ): void { + const encoder = new TextEncoder(); + const parentThis = this; + this.queue.push(async function* () { + // Generate the header chunks (including extended header) + yield* parentThis.generateHeader(filePath, stat, 'file'); + + // The base case of generating data is to have a async generator yielding + // data, but in case the data is passed as an entire buffer or a string, + // we need to chunk it up and wrap it in the async generator. + if (typeof data === 'function') { + let workingBuffer: Array = []; + let bufferSize = 0; + + // Ensure the data is properly converted into Uint8Arrays + for await (const chunk of data()) { + let chunkBytes: Uint8Array; + if (typeof chunk === 'string') { + chunkBytes = encoder.encode(chunk); + } else { + chunkBytes = chunk; + } + workingBuffer.push(chunkBytes); + bufferSize += chunkBytes.byteLength; + + while (bufferSize >= constants.BLOCK_SIZE) { + // Flatten buffer into one Uint8Array + const fullBuffer = utils.concatUint8Arrays(...workingBuffer); + + yield parentThis.generator.generateData( + fullBuffer.slice(0, constants.BLOCK_SIZE), + ); + + // Remove processed bytes from buffer + const remaining = fullBuffer.slice(constants.BLOCK_SIZE); + workingBuffer = []; + if (remaining.byteLength > 0) workingBuffer.push(remaining); + bufferSize = remaining.byteLength; + } + } + if (bufferSize !== 0) { + yield parentThis.generator.generateData( + utils.concatUint8Arrays(...workingBuffer), + ); + } + } else { + // Ensure that the data is being chunked up to 512 bytes + if (data instanceof Uint8Array) { + for ( + let offset = 0; + offset < data.byteLength; + offset += constants.BLOCK_SIZE + ) { + const chunk = data.subarray(offset, offset + constants.BLOCK_SIZE); + yield parentThis.generator.generateData(chunk); + } + } else { + while (data.length > 0) { + const chunk = encoder.encode(data.slice(0, constants.BLOCK_SIZE)); + yield parentThis.generator.generateData(chunk); + data = data.slice(constants.BLOCK_SIZE); + } + } + } + }); + + // We have pushed a new generator to the queue. If the data generator is + // waiting for data, then we can signal it to resume processing. + if (parentThis.resolveWaitChunksP != null) { + parentThis.resolveWaitChunksP(); + parentThis.resolveWaitChunksP = undefined; + } + } + + /** + * Queue up an operation to add a directory to the archive. + * + * @param filePath path of the directory relative to the tar root + * @param stat the stats of the directory + */ + public addDirectory(filePath: string, stat?: FileStat): void { + const parentThis = this; + this.queue.push(async function* () { + yield* parentThis.generateHeader(filePath, stat, 'directory'); + }); + } + + /** + * Queue up an operation to finalize the archive by adding two null chunks + * indicating the end of archive. + */ + public finalize(): void { + const parentThis = this; + this.queue.push(async function* () { + yield parentThis.generator.generateEnd(); + yield parentThis.generator.generateEnd(); + }); + + // We have pushed a new generator to the queue. If the data generator is + // waiting for data, then we can signal it to resume processing. + if (parentThis.resolveWaitChunksP != null) { + parentThis.resolveWaitChunksP(); + parentThis.resolveWaitChunksP = undefined; + } + + // This flag will only be read after the queue is exhausted, signalling no + // further data will be added. + this.ended = true; + } + + /** + * While generating, this waits for the internal queue of operations to empty + * before resolving. Note that if nothing is consuming the data in the queue, + * then this promise will keep waiting. + * + * While parsing, this waits for the internal queue of callbacks to resolve. + * Note that each callback is not blocking, so it is possible that two + * callbacks might try to modify the same resource. + * + * @see {@link yieldChunks} to consume the operations and yield binary chunks + */ + public async settled(): Promise { + await new Promise((resolve) => { + this.resolveSettledP = resolve; + }); + } + + /** + * Returns a generator which yields 512-byte chunks as they are generated from + * the queued operations. + */ + public async *yieldChunks(): AsyncGenerator { + while (true) { + const gen = this.queue.shift(); + if (gen == null) { + // We have gone through all the buffered tasks. Check if we have ended + // yet or we are still going. + if (this.ended) break; + if (this.resolveSettledP != null) this.resolveSettledP(); + + // Wait until more data is available + await new Promise((resolve) => { + this.resolveWaitChunksP = resolve; + }); + continue; + } + + yield* gen(); + } + } +} + +export default VirtualTarGenerator; diff --git a/src/VirtualTarParser.ts b/src/VirtualTarParser.ts new file mode 100644 index 0000000..4cd9c08 --- /dev/null +++ b/src/VirtualTarParser.ts @@ -0,0 +1,335 @@ +import type { + ParsedFile, + ParsedDirectory, + MetadataKeywords, + TokenData, + TokenHeader, +} from './types'; +import Parser from './Parser'; +import * as constants from './constants'; +import * as errors from './errors'; +import * as utils from './utils'; + +/** + * VirtualTar is a library used to create tar files using a virtual file system + * or a file tree. This library aims to provide a generator-parser pair to + * create tar files without the reliance on a file system. + * + * This class is dedicated to parse an archive generated by the generator. + */ +class VirtualTarParser { + /** + * The parser object which converts each 512-byte chunk into a parsed token. + */ + protected parser: Parser = new Parser(); + + /** + * The accumulator is used to, well, accumulate bytes until one chunk can be + * parsed by the parser. Note that there is no limit to the size of the + * writable chunk so the accumulator can grow to the size of the previously + * written chunk. + */ + protected accumulator: Uint8Array = new Uint8Array(); + + /** + * The working token is preserved in case additional data can follow the + * header token. This gives the following data tokens some context. + */ + protected workingToken: TokenHeader | undefined = undefined; + + /** + * The data queue stores all the chunks for the extended metadata block. The + * content of the extended metadata will be stored in memory anyways. + */ + protected dataQueue: Array = []; + + /** + * The extended metadata contains additional metadata in case it could not fit + * in the standard tar header. + */ + protected extendedMetadata: + | Partial> + | undefined; + + /** + * Each callback is a promise added to this set. Once the promise resolves, it + * is removed from this set. + */ + protected pendingCallbacks: Set> = new Set(); + + /** + * This callback resolves a promise which is requesting the next data chunk. + */ + protected resolveDataP: ((value: TokenData) => void) | undefined; + + /** + * This callback resolves a promise waiting for all the pending callbacks to + * resolve. + */ + protected resolveSettledP: (() => void) | undefined; + + /** + * This callback is triggered when a file entry is parsed. The file header is + * sent over as a JSON object and the file data is sent in chunks via an async + * generator. + */ + protected fileCallback: ( + header: ParsedFile, + data: () => AsyncGenerator, + ) => Promise | void; + + /** + * This callback is triggered when a directory entry is parsed. The directory + * header is sent over as a JSON object. + */ + protected directoryCallback: ( + header: ParsedDirectory, + ) => Promise | void; + + /** + * This callback is triggered when the archive has ended. Any cleanup can be + * done here. + */ + protected endCallback: () => Promise | void; + + /** + * Create a new VirtualTarParser object which can parse tar files. + * + * When parsing a tar file, optional callbacks are used to perform actions + * on events. The `onFile` callback is triggered when a file is parsed. The + * file data is queued up via an AsyncGenerator. The `onDirectory` callback + * functions similarly, but without the presence of data. The `onEnd` callback + * is triggered when the parser has generated an end token marking the archive + * as completely parsed. + * + * Note that the file and directory callbacks aren't awaited, and instead + * appended to an internal buffer of callbacks. Thus, {@link settled} can be + * used to wait for all the pending promises to be completed. + * + * Note that each callback is not blocking, so it is possible that two + * callbacks might try to modify the same resource. + + * @param onFile optional callback when a file has been parsed + * @param onDirectory optional callback when a directory has been parsed + * @param onEnd optional callback when the archive has ended + * + * @see {@link settled} + */ + constructor({ + onFile, + onDirectory, + onEnd, + }: { + onFile?: ( + header: ParsedFile, + data: () => AsyncGenerator, + ) => Promise | void; + onDirectory?: (header: ParsedDirectory) => Promise | void; + onEnd?: () => Promise | void; + }) { + this.fileCallback = onFile ?? (() => Promise.resolve()); + this.directoryCallback = onDirectory ?? (() => Promise.resolve()); + this.endCallback = onEnd ?? (() => {}); + } + + /** + * This waits for the internal queue of callbacks to resolve. Note that each + * callback is not blocking, so it is possible that two callbacks might try to + * modify the same resource. + */ + public async settled(): Promise { + // Callbacks is already empty, so return early + if (this.pendingCallbacks.size === 0) return; + + // Otherwise wait for all callbacks to be emptied + await new Promise((resolve) => { + this.resolveSettledP = resolve; + }); + } + + /** + * Writes a chunk to the internal buffer. If the size of the internal buffer + * is larger than or equal to 512 bytes, then the chunks are consumed from + * the buffer until the buffer falls below this limit. + * + * Upon yielding a file or directory token, the relevant data is passed along + * to the relevant callback. Note tha the callbacks are queued, so call + * {@link settle} to wait for all the pending callbacks to resolve. The end + * callback is synchronous, so it is executed immeidately instead of being + * queued. + * + * Only usable when parsing an archive. + * + * @param chunk a chunk of the (or the entire) binary tar file + */ + public async write(chunk: Uint8Array): Promise { + // Update the working accumulator with the new chunk + this.accumulator = utils.concatUint8Arrays(this.accumulator, chunk); + + // Iterate over the accumulator until we run out of data + while (this.accumulator.byteLength >= constants.BLOCK_SIZE) { + // Extract the first chunk and remove that from the accumulator + const block = this.accumulator.slice(0, constants.BLOCK_SIZE); + this.accumulator = this.accumulator.slice(constants.BLOCK_SIZE); + + // Parse the next block. If the block is nullish, then we cannot parse + // anything. Continue the loop. + const token = this.parser.write(block); + if (token == null) continue; + + const type = token.type; + switch (type) { + case 'header': { + // If we get a new header token, then we are done working on the + // previous token. Unset the working token. + this.workingToken = undefined; + + // If we have additional metadata, then use it to override token data + let filePath = token.filePath; + if (this.extendedMetadata != null) { + filePath = this.extendedMetadata.path ?? filePath; + this.extendedMetadata = undefined; + } + + switch (token.fileType) { + case 'directory': { + // Call the directory callback + const p = (async () => + await this.directoryCallback({ + type: 'directory', + path: filePath, + stat: { + size: token.fileSize, + mode: token.fileMode, + mtime: token.fileMtime, + uid: token.ownerUid, + gid: token.ownerGid, + uname: token.ownerUserName, + gname: token.ownerGroupName, + }, + }))(); + this.pendingCallbacks.add(p); + + // Remove the callback from the set after the promise settles + p.finally(() => { + this.pendingCallbacks.delete(p); + // If we are waiting on settling the callbacks, then check is the + // callbacks array is empty. If it is, then we have resolved all + // pending callbacks. + if ( + this.resolveSettledP != null && + this.pendingCallbacks.size === 0 + ) { + this.resolveSettledP(); + } + }); + continue; + } + case 'file': { + // The file token can be followed up with data, so set it as the + // working token to prepare for following data tokens. + this.workingToken = token; + const parentThis = this; + + // Call the file callback + const p = (async () => { + await this.fileCallback( + { + type: 'file', + path: filePath, + stat: { + size: token.fileSize, + mode: token.fileMode, + mtime: token.fileMtime, + uid: token.ownerUid, + gid: token.ownerGid, + uname: token.ownerUserName, + gname: token.ownerGroupName, + }, + }, + async function* (): AsyncGenerator { + // If the file does not have any data, then return early + if (token.fileSize === 0) return; + + while (true) { + const chunk = await new Promise((resolve) => { + parentThis.resolveDataP = resolve; + }); + yield chunk.data; + if (chunk.end) break; + } + }, + ); + })(); + this.pendingCallbacks.add(p); + + // Remove the callback from the set after the promise settles + p.finally(() => { + this.pendingCallbacks.delete(p); + // If we are waiting on settling the callbacks, then check is the + // callbacks array is empty. If it is, then we have resolved all + // pending callbacks. + if ( + this.resolveSettledP != null && + this.pendingCallbacks.size === 0 + ) { + this.resolveSettledP(); + } + }); + continue; + } + case 'extended': + // If the token indicates extended metadata, then set the working + // token and continue. There is no additional callbacks for this + // token type. + this.workingToken = token; + continue; + + default: + utils.never(`Unexpected type ${token.fileType}`); + } + break; + } + case 'data': { + if (this.workingToken == null) { + throw new errors.ErrorVirtualTarInvalidState( + 'Received data token before header token', + ); + } + + // The value of this.workingToken.fileType can only be 'extended' or + // 'file'. + if (this.workingToken.fileType === 'extended') { + this.dataQueue.push(token.data); + + // If we have acquired all the relevant data, then we can concat the + // data. + if (token.end) { + // Concat the working data into a single Uint8Array and decode the + // extended header. + const data = utils.concatUint8Arrays(...this.dataQueue); + this.extendedMetadata = utils.decodeExtendedHeader(data); + } + } else { + if (this.resolveDataP != null) { + this.resolveDataP(token); + } else { + utils.never('Callback is not awaiting the next data token'); + } + } + break; + } + case 'end': + // Clean up the pending promises then trigger the end callback + await this.settled(); + await this.endCallback(); + break; + + default: + utils.never(`Invalid token type: ${type}`); + } + } + } +} + +export default VirtualTarParser; diff --git a/src/constants.ts b/src/constants.ts index 0b84307..ec2bc3c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,3 +8,43 @@ export const STANDARD_PATH_SIZE = 255; // Magic values to indicate a header being a valid tar header export const USTAR_NAME = 'ustar'; export const USTAR_VERSION = '00'; + +// Offset for each section of a standard ustar header +export const HEADER_OFFSET = { + FILE_NAME: 0, + FILE_MODE: 100, + OWNER_UID: 108, + OWNER_GID: 116, + FILE_SIZE: 124, + FILE_MTIME: 136, + CHECKSUM: 148, + TYPE_FLAG: 156, + LINK_NAME: 157, + USTAR_NAME: 257, + USTAR_VERSION: 263, + OWNER_USERNAME: 265, + OWNER_GROUPNAME: 297, + DEVICE_MAJOR: 329, + DEVICE_MINOR: 337, + FILE_NAME_PREFIX: 345, +}; + +// Offset for each section of a standard ustar header +export const HEADER_SIZE = { + FILE_NAME: 100, + FILE_MODE: 8, + OWNER_UID: 8, + OWNER_GID: 8, + FILE_SIZE: 12, + FILE_MTIME: 12, + CHECKSUM: 8, + TYPE_FLAG: 1, + LINK_NAME: 100, + USTAR_NAME: 6, + USTAR_VERSION: 2, + OWNER_USERNAME: 32, + OWNER_GROUPNAME: 32, + DEVICE_MAJOR: 8, + DEVICE_MINOR: 8, + FILE_NAME_PREFIX: 155, +}; diff --git a/src/index.ts b/src/index.ts index aea9a17..26cbe5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ export { default as Generator } from './Generator'; export { default as Parser } from './Parser'; +export { default as VirtualTarGenerator } from './VirtualTarGenerator'; +export { default as VirtualTarParser } from './VirtualTarParser'; export * as constants from './constants'; export * as errors from './errors'; export * as utils from './utils'; diff --git a/src/types.ts b/src/types.ts index e998eb3..3afcbac 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,47 +6,7 @@ enum EntryType { EXTENDED = 'x', } -enum MetadataKeywords { - FILE_PATH = 'path', -} - -enum HeaderOffset { - FILE_NAME = 0, - FILE_MODE = 100, - OWNER_UID = 108, - OWNER_GID = 116, - FILE_SIZE = 124, - FILE_MTIME = 136, - CHECKSUM = 148, - TYPE_FLAG = 156, - LINK_NAME = 157, - USTAR_NAME = 257, - USTAR_VERSION = 263, - OWNER_USERNAME = 265, - OWNER_GROUPNAME = 297, - DEVICE_MAJOR = 329, - DEVICE_MINOR = 337, - FILE_NAME_PREFIX = 345, -} - -enum HeaderSize { - FILE_NAME = 100, - FILE_MODE = 8, - OWNER_UID = 8, - OWNER_GID = 8, - FILE_SIZE = 12, - FILE_MTIME = 12, - CHECKSUM = 8, - TYPE_FLAG = 1, - LINK_NAME = 100, - USTAR_NAME = 6, - USTAR_VERSION = 2, - OWNER_USERNAME = 32, - OWNER_GROUPNAME = 32, - DEVICE_MAJOR = 8, - DEVICE_MINOR = 8, - FILE_NAME_PREFIX = 155, -} +type MetadataKeywords = 'path'; type FileStat = { size?: number; @@ -81,25 +41,6 @@ type TokenEnd = { type: 'end'; }; -enum ParserState { - HEADER, - DATA, - NULL, - ENDED, -} - -enum GeneratorState { - HEADER, - DATA, - NULL, - ENDED, -} - -enum VirtualTarState { - GENERATOR, - PARSER, -} - type ParsedFile = { type: 'file'; path: string; @@ -121,6 +62,20 @@ type ParsedEmpty = { awaitingData: boolean; }; +enum ParserState { + HEADER, + DATA, + NULL, + ENDED, +} + +enum GeneratorState { + HEADER, + DATA, + NULL, + ENDED, +} + export type { FileType, FileStat, @@ -131,14 +86,7 @@ export type { ParsedDirectory, ParsedExtended, ParsedEmpty, -}; - -export { - EntryType, MetadataKeywords, - HeaderOffset, - HeaderSize, - ParserState, - GeneratorState, - VirtualTarState, }; + +export { EntryType, ParserState, GeneratorState }; diff --git a/src/utils.ts b/src/utils.ts index bd6f60c..fba9275 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ -import type { FileType } from './types'; -import { EntryType, MetadataKeywords, HeaderOffset, HeaderSize } from './types'; +import type { FileType, MetadataKeywords } from './types'; +import { EntryType } from './types'; import * as errors from './errors'; import * as constants from './constants'; @@ -25,11 +25,11 @@ function pad( function calculateChecksum(array: Uint8Array): number { return array.reduce((sum, byte, index) => { - // Checksum placeholder is ASCII space, so assume checksum character is - // space while computing it. + // Checksum placeholder is ASCII space, so assume checksum section is filled + // with spaces during computation. if ( - index >= HeaderOffset.CHECKSUM && - index < HeaderOffset.CHECKSUM + HeaderSize.CHECKSUM + index >= constants.HEADER_OFFSET.CHECKSUM && + index < constants.HEADER_OFFSET.CHECKSUM + constants.HEADER_SIZE.CHECKSUM ) { return sum + 32; } @@ -38,7 +38,7 @@ function calculateChecksum(array: Uint8Array): number { } function dateToTarTime(date: Date): number { - return Math.round(date.getTime() / 1000); + return Math.floor(date.getTime() / 1000); } function tarTimeToDate(time: number): Date { @@ -192,17 +192,9 @@ function decodeExtendedHeader( const key = line.substring(0, entrySeparatorIndex); const _value = line.substring(entrySeparatorIndex + 1); - if (!Object.values(MetadataKeywords).includes(key as MetadataKeywords)) { - throw new Error('TMP key doesnt exist'); - } - // Remove the trailing newline const value = _value.substring(0, _value.length - 1); - switch (key as MetadataKeywords) { - case MetadataKeywords.FILE_PATH: { - data[MetadataKeywords.FILE_PATH] = value; - } - } + data[key] = value; offset += size; remainingBytes -= size; @@ -229,35 +221,36 @@ function writeFilePath(header: Uint8Array, filePath: string): void { // file name to it. const filePathSuffix = filePath - .slice(0, HeaderSize.FILE_NAME) - .padEnd(HeaderSize.FILE_NAME, '\0'); + .slice(0, constants.HEADER_SIZE.FILE_NAME) + .padEnd(constants.HEADER_SIZE.FILE_NAME, '\0'); - if (filePath.length < HeaderSize.FILE_NAME) { + if (filePath.length < constants.HEADER_SIZE.FILE_NAME) { writeBytesToArray( header, filePathSuffix, - HeaderOffset.FILE_NAME, - HeaderSize.FILE_NAME, + constants.HEADER_OFFSET.FILE_NAME, + constants.HEADER_SIZE.FILE_NAME, ); } else { const filePathPrefix = filePath .slice( - HeaderSize.FILE_NAME, - HeaderSize.FILE_NAME + HeaderSize.FILE_NAME_PREFIX, + constants.HEADER_SIZE.FILE_NAME, + constants.HEADER_SIZE.FILE_NAME + + constants.HEADER_SIZE.FILE_NAME_PREFIX, ) - .padEnd(HeaderSize.FILE_NAME_PREFIX, '\0'); + .padEnd(constants.HEADER_SIZE.FILE_NAME_PREFIX, '\0'); writeBytesToArray( header, filePathPrefix, - HeaderOffset.FILE_NAME, - HeaderSize.FILE_NAME, + constants.HEADER_OFFSET.FILE_NAME, + constants.HEADER_SIZE.FILE_NAME, ); writeBytesToArray( header, filePathSuffix, - HeaderOffset.FILE_NAME_PREFIX, - HeaderSize.FILE_NAME_PREFIX, + constants.HEADER_OFFSET.FILE_NAME_PREFIX, + constants.HEADER_SIZE.FILE_NAME_PREFIX, ); } } @@ -267,27 +260,27 @@ function writeFileMode(header: Uint8Array, mode?: number): void { // stored in an octal number format. writeBytesToArray( header, - pad(mode ?? '', HeaderSize.FILE_MODE, '0', '\0'), - HeaderOffset.FILE_MODE, - HeaderSize.FILE_MODE, + pad(mode ?? '', constants.HEADER_SIZE.FILE_MODE, '0', '\0'), + constants.HEADER_OFFSET.FILE_MODE, + constants.HEADER_SIZE.FILE_MODE, ); } function writeOwnerUid(header: Uint8Array, uid?: number): void { writeBytesToArray( header, - pad(uid ?? '', HeaderSize.OWNER_UID, '0', '\0'), - HeaderOffset.OWNER_UID, - HeaderSize.OWNER_UID, + pad(uid ?? '', constants.HEADER_SIZE.OWNER_UID, '0', '\0'), + constants.HEADER_OFFSET.OWNER_UID, + constants.HEADER_SIZE.OWNER_UID, ); } function writeOwnerGid(header: Uint8Array, gid?: number): void { writeBytesToArray( header, - pad(gid ?? '', HeaderSize.OWNER_GID, '0', '\0'), - HeaderOffset.OWNER_GID, - HeaderSize.OWNER_GID, + pad(gid ?? '', constants.HEADER_SIZE.OWNER_GID, '0', '\0'), + constants.HEADER_OFFSET.OWNER_GID, + constants.HEADER_SIZE.OWNER_GID, ); } @@ -296,9 +289,9 @@ function writeFileSize(header: Uint8Array, size?: number): void { // directories, and it must be set for files. writeBytesToArray( header, - pad(size ?? '', HeaderSize.FILE_SIZE, '0', '\0'), - HeaderOffset.FILE_SIZE, - HeaderSize.FILE_SIZE, + pad(size ?? '', constants.HEADER_SIZE.FILE_SIZE, '0', '\0'), + constants.HEADER_OFFSET.FILE_SIZE, + constants.HEADER_SIZE.FILE_SIZE, ); } @@ -309,9 +302,9 @@ function writeFileMtime(header: Uint8Array, mtime?: Date): void { const date = mtime != null ? dateToTarTime(mtime) : ''; writeBytesToArray( header, - pad(date, HeaderSize.FILE_MTIME, '0', '\0'), - HeaderOffset.FILE_MTIME, - HeaderSize.FILE_MTIME, + pad(date, constants.HEADER_SIZE.FILE_MTIME, '0', '\0'), + constants.HEADER_OFFSET.FILE_MTIME, + constants.HEADER_SIZE.FILE_MTIME, ); } @@ -336,9 +329,9 @@ function writeFileType( } writeBytesToArray( header, - pad(entryType, HeaderSize.TYPE_FLAG, '0', '\0'), - HeaderOffset.TYPE_FLAG, - HeaderSize.TYPE_FLAG, + pad(entryType, constants.HEADER_SIZE.TYPE_FLAG, '0', '\0'), + constants.HEADER_OFFSET.TYPE_FLAG, + constants.HEADER_SIZE.TYPE_FLAG, ); } @@ -346,9 +339,9 @@ function writeOwnerUserName(header: Uint8Array, username?: string): void { const uname = username ?? ''; writeBytesToArray( header, - uname.padEnd(HeaderSize.OWNER_USERNAME, '\0'), - HeaderOffset.OWNER_USERNAME, - HeaderSize.OWNER_USERNAME, + uname.padEnd(constants.HEADER_SIZE.OWNER_USERNAME, '\0'), + constants.HEADER_OFFSET.OWNER_USERNAME, + constants.HEADER_SIZE.OWNER_USERNAME, ); } @@ -356,9 +349,9 @@ function writeOwnerGroupName(header: Uint8Array, groupname?: string): void { const gname = groupname ?? ''; writeBytesToArray( header, - gname.padEnd(HeaderSize.OWNER_GROUPNAME, '\0'), - HeaderOffset.OWNER_GROUPNAME, - HeaderSize.OWNER_GROUPNAME, + gname.padEnd(constants.HEADER_SIZE.OWNER_GROUPNAME, '\0'), + constants.HEADER_OFFSET.OWNER_GROUPNAME, + constants.HEADER_SIZE.OWNER_GROUPNAME, ); } @@ -368,39 +361,39 @@ function writeUstarMagic(header: Uint8Array): void { writeBytesToArray( header, constants.USTAR_NAME, - HeaderOffset.USTAR_NAME, - HeaderSize.USTAR_NAME, + constants.HEADER_OFFSET.USTAR_NAME, + constants.HEADER_SIZE.USTAR_NAME, ); // This chunk stores the version of USTAR, which is '00' in this case. writeBytesToArray( header, constants.USTAR_VERSION, - HeaderOffset.USTAR_VERSION, - HeaderSize.USTAR_VERSION, + constants.HEADER_OFFSET.USTAR_VERSION, + constants.HEADER_SIZE.USTAR_VERSION, ); } function writeChecksum(header: Uint8Array, checksum: number): void { writeBytesToArray( header, - pad(checksum, HeaderSize.CHECKSUM, '0', '\0'), - HeaderOffset.CHECKSUM, - HeaderSize.CHECKSUM, + pad(checksum, constants.HEADER_SIZE.CHECKSUM, '0', '\0'), + constants.HEADER_OFFSET.CHECKSUM, + constants.HEADER_SIZE.CHECKSUM, ); } function decodeFilePath(array: Uint8Array): string { const fileNamePrefix = extractString( array, - HeaderOffset.FILE_NAME_PREFIX, - HeaderSize.FILE_NAME_PREFIX, + constants.HEADER_OFFSET.FILE_NAME_PREFIX, + constants.HEADER_SIZE.FILE_NAME_PREFIX, ); const fileNameSuffix = extractString( array, - HeaderOffset.FILE_NAME, - HeaderSize.FILE_NAME, + constants.HEADER_OFFSET.FILE_NAME, + constants.HEADER_SIZE.FILE_NAME, ); if (fileNamePrefix !== '') { @@ -411,48 +404,68 @@ function decodeFilePath(array: Uint8Array): string { } function decodeFileMode(array: Uint8Array): number { - return extractOctal(array, HeaderOffset.FILE_MODE, HeaderSize.FILE_MODE); + return extractOctal( + array, + constants.HEADER_OFFSET.FILE_MODE, + constants.HEADER_SIZE.FILE_MODE, + ); } function decodeOwnerUid(array: Uint8Array): number { - return extractOctal(array, HeaderOffset.OWNER_UID, HeaderSize.OWNER_UID); + return extractOctal( + array, + constants.HEADER_OFFSET.OWNER_UID, + constants.HEADER_SIZE.OWNER_UID, + ); } function decodeOwnerGid(array: Uint8Array): number { - return extractOctal(array, HeaderOffset.OWNER_GID, HeaderSize.OWNER_GID); + return extractOctal( + array, + constants.HEADER_OFFSET.OWNER_GID, + constants.HEADER_SIZE.OWNER_GID, + ); } function decodeFileSize(array: Uint8Array): number { - return extractOctal(array, HeaderOffset.FILE_SIZE, HeaderSize.FILE_SIZE); + return extractOctal( + array, + constants.HEADER_OFFSET.FILE_SIZE, + constants.HEADER_SIZE.FILE_SIZE, + ); } function decodeFileMtime(array: Uint8Array): Date { return tarTimeToDate( - extractOctal(array, HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME), + extractOctal( + array, + constants.HEADER_OFFSET.FILE_MTIME, + constants.HEADER_SIZE.FILE_MTIME, + ), ); } function decodeOwnerUserName(array: Uint8Array): string { return extractString( array, - HeaderOffset.OWNER_USERNAME, - HeaderSize.OWNER_USERNAME, + constants.HEADER_OFFSET.OWNER_USERNAME, + constants.HEADER_SIZE.OWNER_USERNAME, ); } function decodeOwnerGroupName(array: Uint8Array): string { return extractString( array, - HeaderOffset.OWNER_GROUPNAME, - HeaderSize.OWNER_GROUPNAME, + constants.HEADER_OFFSET.OWNER_GROUPNAME, + constants.HEADER_SIZE.OWNER_GROUPNAME, ); } -function decodeFileType(array): FileType { +function decodeFileType(array: Uint8Array): FileType { const type = extractString( array, - HeaderOffset.TYPE_FLAG, - HeaderSize.TYPE_FLAG, + constants.HEADER_OFFSET.TYPE_FLAG, + constants.HEADER_SIZE.TYPE_FLAG, ); switch (type) { case EntryType.FILE: @@ -469,19 +482,27 @@ function decodeFileType(array): FileType { } function decodeUstarMagic(array: Uint8Array): string { - return extractString(array, HeaderOffset.USTAR_NAME, HeaderSize.USTAR_NAME); + return extractString( + array, + constants.HEADER_OFFSET.USTAR_NAME, + constants.HEADER_SIZE.USTAR_NAME, + ); } function decodeUstarVersion(array: Uint8Array): string { return extractString( array, - HeaderOffset.USTAR_VERSION, - HeaderSize.USTAR_VERSION, + constants.HEADER_OFFSET.USTAR_VERSION, + constants.HEADER_SIZE.USTAR_VERSION, ); } function decodeChecksum(array: Uint8Array): number { - return extractOctal(array, HeaderOffset.CHECKSUM, HeaderSize.CHECKSUM); + return extractOctal( + array, + constants.HEADER_OFFSET.CHECKSUM, + constants.HEADER_SIZE.CHECKSUM, + ); } export { diff --git a/tests/Parser.test.ts b/tests/Parser.test.ts index 44fad57..6330158 100644 --- a/tests/Parser.test.ts +++ b/tests/Parser.test.ts @@ -7,7 +7,7 @@ import fc from 'fast-check'; import { test } from '@fast-check/jest'; import * as tar from 'tar'; import Parser from '@/Parser'; -import { HeaderOffset, ParserState } from '@/types'; +import { ParserState } from '@/types'; import * as tarErrors from '@/errors'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; @@ -90,7 +90,7 @@ describe('parsing archive blocks', () => { )( 'should fail to parse header with an invalid checksum', ({ headers }, checksum) => { - headers[0].set(checksum, HeaderOffset.CHECKSUM); + headers[0].set(checksum, tarConstants.HEADER_OFFSET.CHECKSUM); const parser = new Parser(); expect(() => parser.write(headers[0])).toThrowError( tarErrors.ErrorVirtualTarParserInvalidHeader, diff --git a/tests/VirtualTar.test.ts b/tests/VirtualTar.test.ts deleted file mode 100644 index a2befbb..0000000 --- a/tests/VirtualTar.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -import type { VirtualFile, VirtualDirectory } from './types'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { test } from '@fast-check/jest'; -import * as tar from 'tar'; -import VirtualTar from '@/VirtualTar'; -import { VirtualTarState } from '@/types'; -import * as utils from './utils'; - -describe('generator', () => { - let tempDir; - - beforeEach(async () => { - tempDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'js-virtualtar-test-'), - ); - }); - - afterEach(async () => { - await fs.promises.rm(tempDir, { recursive: true, force: true }); - }); - - test('should set state to generation', async () => { - const tar = new VirtualTar({ mode: 'generate' }); - // @ts-ignore accessing protected member for state analysis - expect(tar.state).toEqual(VirtualTarState.GENERATOR); - }); - - test('should write data to file', async () => { - // Set the file names and their data - const fileName1 = 'file1.txt'; - const fileName2 = 'file2.txt'; - const fileName3 = 'file3.txt'; - const fileData = 'testing'; - const fileMode = 0o777; - - const vtar = new VirtualTar({ mode: 'generate' }); - - // Write file to archive - vtar.addFile( - fileName1, - { size: fileData.length, mode: fileMode }, - fileData, - ); - vtar.addFile( - fileName2, - { size: fileData.length, mode: fileMode }, - Buffer.from(fileData), - ); - vtar.addFile( - fileName3, - { size: fileData.length, mode: fileMode }, - async function* () { - const halfway = Math.floor(fileData.length / 2); - const prefix = fileData.slice(0, halfway); - const suffix = fileData.slice(halfway); - - // Mixing string and Uint8Array data - yield Buffer.from(prefix); - yield suffix; - }, - ); - vtar.finalize(); - - const archivePath = path.join(tempDir, 'archive.tar'); - const fd = await fs.promises.open(archivePath, 'w'); - for await (const chunk of vtar.yieldChunks()) { - await fd.write(chunk); - } - await fd.close(); - - await tar.extract({ - file: archivePath, - cwd: tempDir, - }); - - // Check if each file has been written correctly - const extractedData1 = await fs.promises.readFile( - path.join(tempDir, fileName1), - ); - const extractedData2 = await fs.promises.readFile( - path.join(tempDir, fileName2), - ); - const extractedData3 = await fs.promises.readFile( - path.join(tempDir, fileName3), - ); - expect(extractedData1.toString()).toEqual(fileData); - expect(extractedData2.toString()).toEqual(fileData); - expect(extractedData3.toString()).toEqual(fileData); - }); - - test('should write a directory to the archive', async () => { - // Set the file names and their data - const dirName = 'dir'; - const dirMode = 0o777; - - const vtar = new VirtualTar({ mode: 'generate' }); - - // Write directory to archive - vtar.addDirectory(dirName, { mode: dirMode }); - vtar.finalize(); - - const archivePath = path.join(tempDir, 'archive.tar'); - const fd = await fs.promises.open(archivePath, 'w'); - for await (const chunk of vtar.yieldChunks()) { - await fd.write(chunk); - } - await fd.close(); - - await tar.extract({ - file: archivePath, - cwd: tempDir, - }); - await fs.promises.rm(archivePath); - - // Check if the directory has been written correctly - const directories = await fs.promises.readdir(path.join(tempDir)); - expect(directories).toEqual([dirName]); - }); -}); - -describe('parser', () => { - let tempDir; - - beforeEach(async () => { - tempDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'js-virtualtar-test-'), - ); - }); - - afterEach(async () => { - await fs.promises.rm(tempDir, { recursive: true, force: true }); - }); - - test('should set state to parsing', async () => { - const tar = new VirtualTar({ mode: 'parse' }); - // @ts-ignore accessing protected member for state analysis - expect(tar.state).toEqual(VirtualTarState.PARSER); - }); - - test('should read files and directories', async () => { - // Set the file names and their data - const dirName = 'dir'; - const fileName1 = 'file.txt'; - const fileName2 = 'dir/file.txt'; - const fileData = 'testing'; - - await fs.promises.mkdir(path.join(tempDir, dirName)); - await fs.promises.writeFile(path.join(tempDir, fileName1), fileData); - await fs.promises.writeFile(path.join(tempDir, fileName2), fileData); - - const archive = tar.create( - { - cwd: tempDir, - preservePaths: true, - }, - [fileName1, dirName, fileName2], - ); - - const entries: Record = {}; - - // Read files and directories and add it to the entries record - const vtar = new VirtualTar({ - mode: 'parse', - onFile: async (header, data) => { - const content: Array = []; - for await (const chunk of data()) { - content.push(chunk); - } - const fileContent = Buffer.concat(content).toString(); - entries[header.path] = fileContent; - }, - onDirectory: async (header) => { - entries[header.path] = undefined; - }, - }); - - // Enqueue each generated chunk from the archive - for await (const chunk of archive) { - vtar.write(chunk); - } - - // Make sure all the callbacks settle - await vtar.settled(); - - expect(entries[dirName]).toBeUndefined(); - expect(entries[fileName1]).toEqual(fileData); - expect(entries[fileName2]).toEqual(fileData); - }); -}); - -describe('integration tests', () => { - test.prop([utils.fileTreeArb()])( - 'archiving and unarchiving a file tree', - async (fileTree) => { - const generator = new VirtualTar({ mode: 'generate' }); - - for (const entry of fileTree) { - if (entry.type === 'file') { - generator.addFile(entry.path, entry.stat, entry.content); - } else { - generator.addDirectory(entry.path, entry.stat); - } - } - generator.finalize(); - - const archive = generator.yieldChunks(); - const entries: Array = []; - - const parser = new VirtualTar({ - mode: 'parse', - onFile: async (header, data) => { - const content: Array = []; - for await (const chunk of data()) { - content.push(chunk); - } - const fileContent = Buffer.concat(content).toString(); - entries.push({ - type: 'file', - path: header.path, - stat: header.stat, - content: fileContent, - }); - }, - onDirectory: async (header) => { - entries.push({ - type: 'directory', - path: header.path, - stat: header.stat, - }); - }, - }); - - for await (const chunk of archive) { - parser.write(chunk); - } - - await parser.settled(); - - expect(utils.deepSort(entries)).toContainAllValues( - utils.deepSort(fileTree), - ); - }, - ); -}); diff --git a/tests/VirtualTarGenerator.test.ts b/tests/VirtualTarGenerator.test.ts new file mode 100644 index 0000000..c6cd8ad --- /dev/null +++ b/tests/VirtualTarGenerator.test.ts @@ -0,0 +1,112 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { test } from '@fast-check/jest'; +import * as tar from 'tar'; +import { VirtualTarGenerator } from '@'; + +describe('generator', () => { + let tempDir; + + beforeEach(async () => { + tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'js-virtualtar-test-'), + ); + }); + + afterEach(async () => { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + }); + + test('should write data to file', async () => { + // Set the file names and their data + const fileName1 = 'file1.txt'; + const fileName2 = 'file2.txt'; + const fileName3 = 'file3.txt'; + const fileData = 'testing'; + const fileMode = 0o777; + + const vtar = new VirtualTarGenerator(); + + // Write file to archive + vtar.addFile( + fileName1, + { size: fileData.length, mode: fileMode }, + fileData, + ); + vtar.addFile( + fileName2, + { size: fileData.length, mode: fileMode }, + Buffer.from(fileData), + ); + vtar.addFile( + fileName3, + { size: fileData.length, mode: fileMode }, + async function* () { + const halfway = Math.floor(fileData.length / 2); + const prefix = fileData.slice(0, halfway); + const suffix = fileData.slice(halfway); + + // Mixing string and Uint8Array data + yield Buffer.from(prefix); + yield suffix; + }, + ); + vtar.finalize(); + + const archivePath = path.join(tempDir, 'archive.tar'); + const fd = await fs.promises.open(archivePath, 'w'); + for await (const chunk of vtar.yieldChunks()) { + await fd.write(chunk); + } + await fd.close(); + + await tar.extract({ + file: archivePath, + cwd: tempDir, + }); + + // Check if each file has been written correctly + const extractedData1 = await fs.promises.readFile( + path.join(tempDir, fileName1), + ); + const extractedData2 = await fs.promises.readFile( + path.join(tempDir, fileName2), + ); + const extractedData3 = await fs.promises.readFile( + path.join(tempDir, fileName3), + ); + expect(extractedData1.toString()).toEqual(fileData); + expect(extractedData2.toString()).toEqual(fileData); + expect(extractedData3.toString()).toEqual(fileData); + }); + + test('should write a directory to the archive', async () => { + // Set the file names and their data + const dirName = 'dir'; + const dirMode = 0o777; + + const vtar = new VirtualTarGenerator(); + + // Write directory to archive + vtar.addDirectory(dirName, { mode: dirMode }); + vtar.finalize(); + + const archivePath = path.join(tempDir, 'archive.tar'); + const fd = await fs.promises.open(archivePath, 'w'); + for await (const chunk of vtar.yieldChunks()) { + await fd.write(chunk); + } + await fd.close(); + + await tar.extract({ + file: archivePath, + cwd: tempDir, + }); + await fs.promises.rm(archivePath); + + // Check if the directory has been written correctly + const directories = await fs.promises.readdir(path.join(tempDir)); + expect(directories).toEqual([dirName]); + }); +}); diff --git a/tests/VirtualTarParser.test.ts b/tests/VirtualTarParser.test.ts new file mode 100644 index 0000000..7a25d1a --- /dev/null +++ b/tests/VirtualTarParser.test.ts @@ -0,0 +1,69 @@ +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { test } from '@fast-check/jest'; +import * as tar from 'tar'; +import { VirtualTarParser } from '@'; + +describe('parser', () => { + let tempDir; + + beforeEach(async () => { + tempDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'js-virtualtar-test-'), + ); + }); + + afterEach(async () => { + await fs.promises.rm(tempDir, { recursive: true, force: true }); + }); + + test('should read files and directories', async () => { + // Set the file names and their data + const dirName = 'dir'; + const fileName1 = 'file.txt'; + const fileName2 = 'dir/file.txt'; + const fileData = 'testing'; + + await fs.promises.mkdir(path.join(tempDir, dirName)); + await fs.promises.writeFile(path.join(tempDir, fileName1), fileData); + await fs.promises.writeFile(path.join(tempDir, fileName2), fileData); + + const archive = tar.create( + { + cwd: tempDir, + preservePaths: true, + }, + [fileName1, dirName, fileName2], + ); + + const entries: Record = {}; + + // Read files and directories and add it to the entries record + const vtar = new VirtualTarParser({ + onFile: async (header, data) => { + const content: Array = []; + for await (const chunk of data()) { + content.push(chunk); + } + const fileContent = Buffer.concat(content).toString(); + entries[header.path] = fileContent; + }, + onDirectory: async (header) => { + entries[header.path] = undefined; + }, + }); + + // Enqueue each generated chunk from the archive + for await (const chunk of archive) { + await vtar.write(chunk); + } + + // Make sure all the callbacks settle + await vtar.settled(); + + expect(entries[dirName]).toBeUndefined(); + expect(entries[fileName1]).toEqual(fileData); + expect(entries[fileName2]).toEqual(fileData); + }); +}); diff --git a/tests/index.test.ts b/tests/index.test.ts index 86fdb18..ec02ea0 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,7 +1,9 @@ import * as tar from '@'; describe('index', () => { - test('exports Generator, Parser, constants, errors, types, and utils', () => { + test('should have correct high-level exports', () => { + expect('VirtualTarGenerator' in tar).toBeTrue(); + expect('VirtualTarParser' in tar).toBeTrue(); expect('Generator' in tar).toBeTrue(); expect('Parser' in tar).toBeTrue(); expect('constants' in tar).toBeTrue(); @@ -10,14 +12,3 @@ describe('index', () => { expect('types' in tar).toBeTrue(); }); }); - -test('test', async () => { - const fs = await import('fs'); - const generator = new tar.Generator(); - const fd = await fs.promises.open('./tmp/test.tar', 'w+'); - await fd.write(generator.generateFile('abc/def/file.txt', { size: 3 })); - await fd.write(generator.generateData(Buffer.from('123'))); - await fd.write(generator.generateEnd()); - await fd.write(generator.generateEnd()); - await fd.close(); -}); diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 5c02fd3..1611608 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -1,14 +1,14 @@ import type { FileStat, MetadataKeywords } from '@/types'; +import type { VirtualFile, VirtualDirectory } from './types'; import { test } from '@fast-check/jest'; -import Generator from '@/Generator'; -import Parser from '@/Parser'; +import { Generator, Parser, VirtualTarGenerator, VirtualTarParser } from '@'; import * as tarUtils from '@/utils'; import * as tarConstants from '@/constants'; import * as utils from './utils'; describe('integration testing', () => { test.prop([utils.fileTreeArb()])( - 'should archive and unarchive a virtual file system', + 'should archive and unarchive a file tree using generator-parser pair', (fileTree) => { const generator = new Generator(); const blocks: Array = []; @@ -179,4 +179,56 @@ describe('integration testing', () => { } }, ); + + test.prop([utils.fileTreeArb()])( + 'should archive and unarchive a file tree using virtualtar', + async (fileTree) => { + const generator = new VirtualTarGenerator(); + + for (const entry of fileTree) { + if (entry.type === 'file') { + generator.addFile(entry.path, entry.stat, entry.content); + } else { + generator.addDirectory(entry.path, entry.stat); + } + } + generator.finalize(); + + const archive = generator.yieldChunks(); + const entries: Array = []; + + const parser = new VirtualTarParser({ + onFile: async (header, data) => { + const content: Array = []; + for await (const chunk of data()) { + content.push(chunk); + } + const fileContent = Buffer.concat(content).toString(); + entries.push({ + type: 'file', + path: header.path, + stat: header.stat, + content: fileContent, + }); + }, + onDirectory: async (header) => { + entries.push({ + type: 'directory', + path: header.path, + stat: header.stat, + }); + }, + }); + + for await (const chunk of archive) { + await parser.write(chunk); + } + + await parser.settled(); + + expect(utils.deepSort(entries)).toContainAllValues( + utils.deepSort(fileTree), + ); + }, + ); }); From 836775b086d38696cde62710b43a56ca9bdaa2ad Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Wed, 19 Mar 2025 15:16:05 +1100 Subject: [PATCH 19/19] chore: added tests for edge cases --- src/VirtualTarParser.ts | 7 +- tests/VirtualTarGenerator.test.ts | 81 ++++++++++++++++++++ tests/VirtualTarParser.test.ts | 120 ++++++++++++++++++++++++++++-- 3 files changed, 200 insertions(+), 8 deletions(-) diff --git a/src/VirtualTarParser.ts b/src/VirtualTarParser.ts index 4cd9c08..46c3def 100644 --- a/src/VirtualTarParser.ts +++ b/src/VirtualTarParser.ts @@ -126,7 +126,7 @@ class VirtualTarParser { ) => Promise | void; onDirectory?: (header: ParsedDirectory) => Promise | void; onEnd?: () => Promise | void; - }) { + } = {}) { this.fileCallback = onFile ?? (() => Promise.resolve()); this.directoryCallback = onDirectory ?? (() => Promise.resolve()); this.endCallback = onEnd ?? (() => {}); @@ -313,9 +313,10 @@ class VirtualTarParser { } else { if (this.resolveDataP != null) { this.resolveDataP(token); - } else { - utils.never('Callback is not awaiting the next data token'); } + // If the resolve callback is undefined, then nothing is waiting for + // the data. We can ignore sending over the data and continue as + // usual. } break; } diff --git a/tests/VirtualTarGenerator.test.ts b/tests/VirtualTarGenerator.test.ts index c6cd8ad..b5dc020 100644 --- a/tests/VirtualTarGenerator.test.ts +++ b/tests/VirtualTarGenerator.test.ts @@ -4,6 +4,7 @@ import os from 'os'; import { test } from '@fast-check/jest'; import * as tar from 'tar'; import { VirtualTarGenerator } from '@'; +import * as tarConstants from '@/constants'; describe('generator', () => { let tempDir; @@ -109,4 +110,84 @@ describe('generator', () => { const directories = await fs.promises.readdir(path.join(tempDir)); expect(directories).toEqual([dirName]); }); + + test('should write to archive while reading data in parallel', async () => { + // Set the file names and their data + const fileName1 = 'file1.txt'; + const fileName2 = 'file2.txt'; + const dirName = 'dir'; + const fileData = 'testing'; + + const vtar = new VirtualTarGenerator(); + + // Write file to archive in parallel to writing data to generator + const p = (async () => { + vtar.addFile(fileName1, { size: fileData.length, mode: 0o777 }, fileData); + vtar.addFile(fileName2, { size: fileData.length, mode: 0o777 }, fileData); + vtar.addDirectory(dirName); + vtar.finalize(); + })(); + + const archivePath = path.join(tempDir, 'archive.tar'); + const fd = await fs.promises.open(archivePath, 'w'); + for await (const chunk of vtar.yieldChunks()) { + await fd.write(chunk); + } + await fd.close(); + + // Cleanup promise for adding files to archive + await p; + + await tar.extract({ + file: archivePath, + cwd: tempDir, + }); + + // Check if each file has been written correctly + const extractedData1 = await fs.promises.readFile( + path.join(tempDir, fileName1), + ); + const extractedData2 = await fs.promises.readFile( + path.join(tempDir, fileName2), + ); + const dirStat = await fs.promises.stat(path.join(tempDir, dirName)); + expect(extractedData1.toString()).toEqual(fileData); + expect(extractedData2.toString()).toEqual(fileData); + expect(dirStat.isDirectory()).toBeTrue(); + }); + + test('should write file containing exactly 512 bytes of data', async () => { + // Set the file names and their data + const fileName = 'file.txt'; + const fileData = new Uint8Array(tarConstants.BLOCK_SIZE).fill(1); + + const vtar = new VirtualTarGenerator(); + + // Write file to archive in parallel to writing data to generator + vtar.addFile(fileName, { size: fileData.length, mode: 0o777 }, fileData); + vtar.finalize(); + + const archivePath = path.join(tempDir, 'archive.tar'); + const fd = await fs.promises.open(archivePath, 'w'); + for await (const chunk of vtar.yieldChunks()) { + await fd.write(chunk); + } + await fd.close(); + + await tar.extract({ + file: archivePath, + cwd: tempDir, + }); + + // Check if file has been written correctly + const extractedData = await fs.promises.readFile( + path.join(tempDir, fileName), + ); + + // The sums of all values in both the input and output buffers must be the + // same. + const fileSum = fileData.reduce((sum, value) => (sum += value)); + const dataSum = extractedData.reduce((sum, value) => (sum += value)); + expect(dataSum).toEqual(fileSum); + }); }); diff --git a/tests/VirtualTarParser.test.ts b/tests/VirtualTarParser.test.ts index 7a25d1a..84fb20c 100644 --- a/tests/VirtualTarParser.test.ts +++ b/tests/VirtualTarParser.test.ts @@ -20,7 +20,7 @@ describe('parser', () => { test('should read files and directories', async () => { // Set the file names and their data - const dirName = 'dir'; + const dirName = 'dir/'; const fileName1 = 'file.txt'; const fileName2 = 'dir/file.txt'; const fileData = 'testing'; @@ -37,7 +37,7 @@ describe('parser', () => { [fileName1, dirName, fileName2], ); - const entries: Record = {}; + const entries: Record = {}; // Read files and directories and add it to the entries record const vtar = new VirtualTarParser({ @@ -50,7 +50,7 @@ describe('parser', () => { entries[header.path] = fileContent; }, onDirectory: async (header) => { - entries[header.path] = undefined; + entries[header.path] = null; }, }); @@ -59,11 +59,121 @@ describe('parser', () => { await vtar.write(chunk); } - // Make sure all the callbacks settle - await vtar.settled(); + expect(entries[dirName]).toBeNull(); + expect(entries[fileName1]).toEqual(fileData); + expect(entries[fileName2]).toEqual(fileData); + }); + + test('should ignore files if callback is not provided', async () => { + // Set the file names and their data + const dirName = 'dir/'; + const fileName1 = 'file.txt'; + const fileName2 = 'dir/file.txt'; + const fileData = 'testing'; + + await fs.promises.mkdir(path.join(tempDir, dirName)); + await fs.promises.writeFile(path.join(tempDir, fileName1), fileData); + await fs.promises.writeFile(path.join(tempDir, fileName2), fileData); + + const archive = tar.create( + { + cwd: tempDir, + preservePaths: true, + }, + [fileName1, dirName, fileName2], + ); + + const entries: Record = {}; + + // Read files and directories and add it to the entries record + const vtar = new VirtualTarParser({ + onDirectory: async (header) => { + entries[header.path] = null; + }, + }); + + // Enqueue each generated chunk from the archive + for await (const chunk of archive) { + await vtar.write(chunk); + } + + expect(entries[dirName]).toBeNull(); + expect(entries[fileName1]).toBeUndefined(); + expect(entries[fileName2]).toBeUndefined(); + }); + + test('should ignore directories if callback is not provided', async () => { + // Set the file names and their data + const dirName = 'dir/'; + const fileName1 = 'file.txt'; + const fileName2 = 'dir/file.txt'; + const fileData = 'testing'; + + await fs.promises.mkdir(path.join(tempDir, dirName)); + await fs.promises.writeFile(path.join(tempDir, fileName1), fileData); + await fs.promises.writeFile(path.join(tempDir, fileName2), fileData); + + const archive = tar.create( + { + cwd: tempDir, + preservePaths: true, + }, + [fileName1, dirName, fileName2], + ); + + const entries: Record = {}; + + // Read files and directories and add it to the entries record + const vtar = new VirtualTarParser({ + onFile: async (header, data) => { + const content: Array = []; + for await (const chunk of data()) { + content.push(chunk); + } + const fileContent = Buffer.concat(content).toString(); + entries[header.path] = fileContent; + }, + }); + + // Enqueue each generated chunk from the archive + for await (const chunk of archive) { + await vtar.write(chunk); + } expect(entries[dirName]).toBeUndefined(); expect(entries[fileName1]).toEqual(fileData); expect(entries[fileName2]).toEqual(fileData); }); + + test('should ignore everything if callback is not provided', async () => { + // Set the file names and their data + const dirName = 'dir/'; + const fileName1 = 'file.txt'; + const fileName2 = 'dir/file.txt'; + const fileData = 'testing'; + + await fs.promises.mkdir(path.join(tempDir, dirName)); + await fs.promises.writeFile(path.join(tempDir, fileName1), fileData); + await fs.promises.writeFile(path.join(tempDir, fileName2), fileData); + + const archive = tar.create( + { + cwd: tempDir, + preservePaths: true, + }, + [fileName1, dirName, fileName2], + ); + + const entries: Record = {}; + + // Read files and directories and add it to the entries record + const vtar = new VirtualTarParser(); + + // Enqueue each generated chunk from the archive + for await (const chunk of archive) { + await vtar.write(chunk); + } + + expect(Object.keys(entries).length).toEqual(0); + }); });