diff --git a/.gitattributes b/.gitattributes index 625449502b..0d63732a99 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,20 @@ +# Don't allow people to merge changes to these generated files, because the result +# may be invalid. You need to run "rush update" again. +ce/pnpm-lock.yaml merge=binary +ce/shrinkwrap.yaml merge=binary +ce/npm-shrinkwrap.json merge=binary + + +# Disable line ending smudges entirely. * -text + +# Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic +# syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor +# may also require a special configuration to allow comments in JSON. +# +# For more information, see this issue: https://github.com/Microsoft/web-build-tools/issues/1088 +# +*.json linguist-language=JSON-with-Comments +*.png binary +*.gif binary +*.mp4 binary diff --git a/.gitignore b/.gitignore index 18cf816fb8..d3738a5bc2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,17 @@ /CMakeSettings.json /out .DS_Store -# Qt Creator CMake project files CMakeLists.txt.user .cache /vcpkg-ce.zip -/vcpkg-ce/** +node_modules/ +**/.rush/ +**/dist/ +/ce/test/**/*.d.ts +/ce/test/**/*.map +/ce/test/**/*.js +/ce/ce/vcpkg-ce.build.log +/ce/common/config/rush/pnpm-lock.yaml +/ce/test/vcpkg-ce.test.build.log +/ce/common/temp +/vcpkg-root diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..f9ba8cf65f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/README.md b/README.md index ae5ec55ddc..2e76bab1f1 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,50 @@ tracking, and edits to which libraries are available. This repository contains the contents formerly at https://github.com/microsoft/vcpkg in the "toolsrc" tree, and build support. +# Vcpkg-ce: "Configure Environment" / artifacts + +Parts of vcpkg powered by "ce" are currently in 'preview' -- there will most certainly be changes between now +and when the tool is 'released' based on feedback. + +You can use it, but be forewarned that we may change formats, commands, etc. + +Think of it as a manifest-driven desired state configuration for C/C++ projects. + +It + - integrates itself into your shell (PowerShell, CMD, bash/zsh) + - can restore artifacts according to a manifest that follows one’s code + - provides discoverability interfaces + +## Installation + +While the usage of `ce` is the same on all platforms, the installation/loading/removal is slightly different depending on the platform you're using. + +`ce` doesn't persist any changes to the environment, nor does it automatically add itself to the start-up environment. If you wish to make it load in a window, you can just execute the script. Manually adding that in your profile will load it in every new window. + +
+ +## Install/Use/Remove + +| OS | Install | Use | Remove | +|-----------------|-----------------------------------------------------|-----------------------|---------------------------------| +| **PowerShell/Pwsh** |`iex (iwr -useb https://aka.ms/vcpkg-init.ps1)` |` . ~/.vcpkg/vcpkg-init.ps1` | `rmdir -recurse ~/.vcpkg` | +| **Linux/OSX** |`. <(curl https://aka.ms/vcpkg-init.sh -L)` |` . ~/.vcpkg/vcpkg-init.sh` | `rm -rf ~/.ce` | +| **CMD Shell** |`curl -LO https://aka.ms/vcpkg-init.cmd && .\vcpkg-init.cmd` |`%USERPROFILE%\.vcpkg\vcpkg-init.cmd` | `rmdir /s /q %USERPROFILE%\.vcpkg` | + +## Glossary + +| Term | Description | +|------------|-----------------------------------------------------| +| `artifact` | An archive (.zip or .tar.gz-like), package (.nupkg, .vsix) binary inside which build tools or components thereof are stored. | +| `artifact metadata` | A description of the locations one or more artifacts describing rules for which ones are deployed given selection of a host architecture, target architecture, or other properties| +| `artifact identity` | A short string that uniquely describes a moniker that a given artifact (and its metadata) can be referenced by. They can have one of the following forms:
`full/identity/path` - the full identity of an artifact that is in the built-in artifact source
`sourcename:full/identity/path` - the full identity of an artifact that is in the artifact source specified by the sourcename prefix
`shortname` - the shortened unique name of an artifact that is in the built-in artifact source
`sourcename:shortname` - the shortened unique name of an artifact that is in the artifact source specified by the sourcename prefix
Shortened names are generated based off the shortest unique identity path in the given source. | +| `artifact source` | Also known as a “feed”. An Artifact Source is a location that hosts metadata to locate artifacts. (_There is only one source currently_) | +| `project profile` | The per-project configuration file (`environment.yaml` or `environment.json`) +| `AMF` or `Metadata` `Format` | The schema / format of the YAML/JSON files for project profiles, global settings, and artifacts metadata. | +| `activation` | The process by which a particular set of artifacts are acquired and enabled for use in a calling command program.| +| `versions` | Version numbers are specified using the Semver format. If a version for a particular operation isn't specified, a range for the latest version ( `*` ) is assumed. A version or version range can be specified using the npm semver matching syntax. When a version is stored, it can be stored using the version range specified, a space and then the version found. (ie, the first version is what was asked for, the second is what was installed. No need for a separate lock file.) | + + # Contributing Please refer to the "contributing" section of the @@ -36,6 +80,14 @@ with any additional questions or comments. The product code in this repository is licensed under the [MIT License](LICENSE.txt). The tests contain 3rd party code as documented in `NOTICE.txt`. +# Trademarks + +This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft +trademarks or logos is subject to and must follow +[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. +Any use of third-party trademarks or logos are subject to those third-party's policies. + # Telemetry vcpkg collects usage data in order to help us improve your experience. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..f7b89984f0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + \ No newline at end of file diff --git a/azure-pipelines/arch-independent-signing.signproj b/azure-pipelines/arch-independent-signing.signproj index 338b9ed67a..80a762a42f 100644 --- a/azure-pipelines/arch-independent-signing.signproj +++ b/azure-pipelines/arch-independent-signing.signproj @@ -11,14 +11,14 @@ - + Microsoft400 - + Microsoft400 - + 3PartyScriptsSHA2 diff --git a/azure-pipelines/pipelines.yml b/azure-pipelines/pipelines.yml index 428fb23435..5f7ba59312 100644 --- a/azure-pipelines/pipelines.yml +++ b/azure-pipelines/pipelines.yml @@ -1,6 +1,6 @@ jobs: - job: linux_gcc_9 - displayName: 'Ubuntu 20.04 with GCC 9' + displayName: 'Ubuntu 20.04 with GCC 9, plus vcpkg-ce' pool: vmImage: 'ubuntu-20.04' variables: @@ -11,6 +11,34 @@ jobs: git clone https://github.com/microsoft/vcpkg "$VCPKG_ROOT" -n git -C "$VCPKG_ROOT" checkout `cat vcpkg-init/vcpkg-scripts-sha.txt` displayName: "Clone vcpkg repo to serve as root" + - task: NodeTool@0 + inputs: + versionSpec: '16.x' + displayName: 'Install Node.js' + - script: | + cd ce + + npm install -g npm + rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi + + npx @microsoft/rush update + rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi + + npx @microsoft/rush rebuild + rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi + + npx @microsoft/rush test + rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi + + npx @microsoft/rush lint + rc=$?; if [ $rc -ne 0 ]; then exit $rc ; fi + + if [ -n "$(git status --porcelain)" ]; then + echo "ERROR: Working directory is dirty. Are there test output files missing from the PR?" + git status + exit 1 + fi + displayName: 'Rush install, build and test vcpkg-ce' - bash: | export CXXFLAGS="-fprofile-arcs -ftest-coverage -fPIC -O0" cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DVCPKG_DEVELOPMENT_WARNINGS=ON -DVCPKG_WARNINGS_AS_ERRORS=ON -DVCPKG_BUILD_FUZZING=ON -B build.amd64.debug diff --git a/azure-pipelines/signing.yml b/azure-pipelines/signing.yml index 7bf99c2bf6..39bde73ad8 100644 --- a/azure-pipelines/signing.yml +++ b/azure-pipelines/signing.yml @@ -66,55 +66,42 @@ jobs: filePath: vcpkg-init/lock-versions.ps1 arguments: '-Destination "$(Build.BinariesDirectory)" -VcpkgBaseVersion $(VCPKG_INITIAL_BASE_VERSION)' # Build and test vcpkg-ce - - task: PowerShell@2 - displayName: 'Download vcpkg-ce sources' - inputs: - targetType: 'inline' - script: | - $sha = Get-Content azure-pipelines/vcpkg-ce-sha.txt -Raw - $sha = $sha.Trim() - if( test-path vcpkg-ce) { rmdir -recurse -force vcpkg-ce -ea 0 } - # this will keep the .git folder, which is used by set-versions to accurately set the version of ce - git clone https://github.com/microsoft/vcpkg-ce/ vcpkg-ce - cd vcpkg-ce - git checkout $sha - pwsh: true - task: UseNode@1 displayName: Use Node 16 or later inputs: version: "16.x" - script: npm install -g @microsoft/rush displayName: Install Rush - workingDirectory: vcpkg-ce + workingDirectory: ce - script: rush update displayName: Install vcpkg-ce Dependencies - workingDirectory: vcpkg-ce + workingDirectory: ce - script: rush lint displayName: Check vcpkg-ce for Linting Errors - workingDirectory: vcpkg-ce + workingDirectory: ce - script: rush rebuild displayName: Build vcpkg-ce Packages - workingDirectory: vcpkg-ce + workingDirectory: ce - script: rush test displayName: Run vcpkg-ce Tests - workingDirectory: vcpkg-ce + workingDirectory: ce - script: | rush set-versions node -e "const c = require('./ce/package.json'); p = require('./assets/package.json') ; p.version = c.version; require('fs').writeFileSync('./assets/package.json', JSON.stringify(p,undefined,2)); console.log(``set asset version to `${p.version}``);" displayName: Set vcpkg-ce Package Versions - workingDirectory: vcpkg-ce - - script: mkdir "$(Build.BinariesDirectory)\vcpkg-ce" && rush deploy -t "$(Build.BinariesDirectory)\vcpkg-ce" + workingDirectory: ce + - script: mkdir "$(Build.BinariesDirectory)\ce" && rush deploy -t "$(Build.BinariesDirectory)\ce" displayName: Collect vcpkg-ce Dependencies - workingDirectory: vcpkg-ce + workingDirectory: ce - task: ComponentGovernanceComponentDetection@0 displayName: Detect Components inputs: - ignoreDirectories: vcpkg-ce/common/temp + ignoreDirectories: ce/common/temp # Inject the NOTICE file. This must run after component detection. - task: msospo.ospo-extension.8d7f9abb-6896-461d-9e25-4f74ed65ddb2.notice@0 displayName: Generate NOTICE File inputs: - outputfile: $(Build.BinariesDirectory)/vcpkg-ce/NOTICE.txt + outputfile: $(Build.BinariesDirectory)/ce/NOTICE.txt - task: MicroBuildSigningPlugin@3 displayName: Install MicroBuild Signing inputs: @@ -148,10 +135,10 @@ jobs: arguments: '-DestinationTarball "$(Build.BinariesDirectory)\vcpkg-standalone-bundle.tar.gz" -TempDir standalone-temp "$(Build.BinariesDirectory)\vcpkg-init.cmd" "$(Build.BinariesDirectory)\vcpkg-init.ps1" "$(Build.BinariesDirectory)\vcpkg-init"' - script: npm pack displayName: Create vcpkg-ce Pack - workingDirectory: $(Build.BinariesDirectory)/vcpkg-ce + workingDirectory: $(Build.BinariesDirectory)/ce - script: | mkdir "$(Build.ArtifactStagingDirectory)\staging" - move "$(Build.BinariesDirectory)\vcpkg-ce\vcpkg-ce-*.tgz" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-ce.tgz" + move "$(Build.BinariesDirectory)\ce\vcpkg-ce-*.tgz" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-ce.tgz" move "$(Build.BinariesDirectory)\vcpkg-standalone-bundle.tar.gz" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-standalone-bundle.tar.gz" move "$(Build.BinariesDirectory)\vcpkg-init" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-init" move "$(Build.BinariesDirectory)\vcpkg-init.ps1" "$(Build.ArtifactStagingDirectory)\staging\vcpkg-init.ps1" diff --git a/azure-pipelines/vcpkg-ce-sha.txt b/azure-pipelines/vcpkg-ce-sha.txt deleted file mode 100644 index d6f8beb8b2..0000000000 --- a/azure-pipelines/vcpkg-ce-sha.txt +++ /dev/null @@ -1 +0,0 @@ -b71a8fd4d01cde1ff34c16df2874fa1c763751b6 diff --git a/ce/.eslintignore b/ce/.eslintignore new file mode 100644 index 0000000000..0f10977a64 --- /dev/null +++ b/ce/.eslintignore @@ -0,0 +1 @@ +**/*.d.ts \ No newline at end of file diff --git a/ce/.scripts/for-each.js b/ce/.scripts/for-each.js new file mode 100644 index 0000000000..e238feb342 --- /dev/null +++ b/ce/.scripts/for-each.js @@ -0,0 +1,76 @@ +const { spawn } = require('child_process'); +const { readFileSync } = require('fs'); +const { resolve } = require('path'); + +/** Reads a json/jsonc file and returns the JSON. + * + * Necessary because rush uses jsonc ( >,< ) + */ +function read(filename) { + const txt = readFileSync(filename, "utf8") + .replace(/\r/gm, "") + .replace(/\n/gm, "«") + .replace(/\/\*.*?\*\//gm, "") + .replace(/«/gm, "\n") + .replace(/\s+\/\/.*/g, ""); + return JSON.parse(txt); +} + +const repo = `${__dirname}/..`; + +const rush = read(`${repo}/rush.json`); +const pjs = {}; + +function forEachProject(onEach) { + // load all the projects + for (const each of rush.projects) { + const packageName = each.packageName; + const projectFolder = resolve(`${repo}/${each.projectFolder}`); + const project = JSON.parse(readFileSync(`${projectFolder}/package.json`)); + onEach(packageName, projectFolder, project); + } +} + +function npmForEach(cmd) { + let count = 0; + let failing = false; + const result = {}; + const procs = []; + const t1 = process.uptime() * 100; + forEachProject((name, location, project) => { + // checks for the script first + if (project.scripts[cmd]) { + count++; + const proc = spawn("npm", ["--silent", "run", cmd], { cwd: location, shell: true, stdio: "inherit" }); + procs.push(proc); + result[name] = { + name, location, project, proc, + }; + } + }); + + procs.forEach(proc => proc.on("close", (code, signal) => { + count--; + failing || !!code; + + if (count === 0) { + const t2 = process.uptime() * 100; + + console.log('---------------------------------------------------------'); + if (failing) { + console.log(` Done : command '${cmd}' - ${Math.floor(t2 - t1) / 100} s -- Errors encountered. `) + } else { + console.log(` Done : command '${cmd}' - ${Math.floor(t2 - t1) / 100} s -- No Errors `) + } + console.log('---------------------------------------------------------'); + process.exit(failing ? 1 : 0); + } + })); + + return result; +} + +module.exports.forEachProject = forEachProject; +module.exports.npm = npmForEach; +module.exports.projectCount = rush.projects.length; +module.exports.read = read; diff --git a/ce/.scripts/npm-run.js b/ce/.scripts/npm-run.js new file mode 100644 index 0000000000..ec4511a5ea --- /dev/null +++ b/ce/.scripts/npm-run.js @@ -0,0 +1,2 @@ +// Runs the npm run command on each project that has it. +require('./for-each').npm(process.argv[2]); \ No newline at end of file diff --git a/ce/.scripts/set-versions.js b/ce/.scripts/set-versions.js new file mode 100644 index 0000000000..03de4048dc --- /dev/null +++ b/ce/.scripts/set-versions.js @@ -0,0 +1,47 @@ +const { exec } = require('child_process'); +const { writeFileSync } = require('fs'); +const { forEachProject, projectCount } = require('./for-each'); + +let count = projectCount; + + +function updateVersion(name, project, location, patch) { + const origJson = JSON.stringify(project, null, 2); + + // update the third digit + const verInfo = project.version.split('.'); + verInfo[2] = patch; + project.version = verInfo.join('.'); + + // write the file if it's changed + const newJson = JSON.stringify(project, null, 2); + if (origJson !== newJson) { + console.log(`Writing project '${name}' version to '${project.version}' in '${location}'`); + writeFileSync(`${location}/package.json`, newJson) + } + + count--; + if (count === 0) { + // last one! + // call sync-versions + require('./sync-versions'); + } +} + +if (process.argv[2] === '--reset') { + forEachProject((name, location, project) => { + updateVersion(name, project, location, 0); + }) +} else { + + // Sets the patch version on each package.json in the project. + forEachProject((name, location, project) => { + if (!process.argv[2] || process.argv[2] === name) { + exec(`git rev-list --parents HEAD --count --full-history ..`, { cwd: location }, (o, stdout) => { + const patch = (parseInt(stdout.trim()) + (Number(project.patchOffset) || -1)); + updateVersion(name, project, location, patch); + }); + } + }); +} + diff --git a/ce/.scripts/sync-versions.js b/ce/.scripts/sync-versions.js new file mode 100644 index 0000000000..36c26b28b2 --- /dev/null +++ b/ce/.scripts/sync-versions.js @@ -0,0 +1,133 @@ +const { readFileSync, writeFileSync } = require('fs'); +const { read } = require('./for-each'); + +const packageList = {}; +const rush = read(`${__dirname}/../rush.json`); +const pjs = {}; + +function writeIfChanged(filename, content) { + const orig = JSON.parse(readFileSync(filename)) + const origJson = JSON.stringify(orig, null, 2); + const json = JSON.stringify(content, null, 2); + + if (origJson !== json) { + console.log(`Writing updated file '${filename}'`) + writeFileSync(filename, json) + return true; + } + return false; +} + +function versionToInt(ver) { + let v = ver.replace(/[^\d\.]/g, '').split('.').slice(0, 3); + while (v.length < 3) { + v.unshift(0); + } + let n = 0; + for (let i = 0; i < v.length; i++) { + n = n + ((2 ** (i * 16)) * parseInt(v[v.length - 1 - i])) + } + return n; +} + +function setPeerDependencies(dependencies) { + for (const dep in dependencies) { + const ref = pjs[dep]; + if (ref) { + if (dependencies[dep] !== `~${ref.version}`) { + console.log(`updating peer depedency ${dep} to ~${ref.version}`); + dependencies[dep] = `~${ref.version}`; + } + } + } +} + + +function recordDeps(dependencies) { + for (const packageName in dependencies) { + const packageVersion = dependencies[packageName]; + if (packageList[packageName]) { + // same version? + if (packageList[packageName] === packageVersion) { + continue; + } + console.log(`${packageName} has ['${packageList[packageName]}','${packageVersion}']`); + + // pick the higher one + const v = versionToInt(packageVersion); + + if (v === 0) { + console.error(`Unparsed version ${packageName}:${packageVersion}`); + process.exit(1); + } + const v2 = versionToInt(packageList[packageName]); + if (v > v2) { + + packageList[packageName] = packageVersion; + } + } else { + packageList[packageName] = packageVersion; + } + } +} +function fixDeps(pj, dependencies) { + for (const packageName in dependencies) { + if (dependencies[packageName] !== packageList[packageName]) { + console.log(`updating ${pj}:${packageName} from '${dependencies[packageName]}' to '${packageList[packageName]}'`) + dependencies[packageName] = packageList[packageName]; + } + } +} + +// load all the projects +for (const each of rush.projects) { + const packageName = each.packageName; + const projectFolder = each.projectFolder; + pjs[packageName] = JSON.parse(readFileSync(`${__dirname}/../${projectFolder}/package.json`)); +} + +// verify that peer dependencies are the same version as they are building. +for (const pj of Object.getOwnPropertyNames(pjs)) { + const each = pjs[pj]; + setPeerDependencies(each.dependencies); + setPeerDependencies(each.devDependencies); + if (each['static-link']) { + setPeerDependencies(each['static-link'].dependencies); + } +} + +// now compare to see if someone has an external package with different version +// than everyone else. +for (const pj of Object.getOwnPropertyNames(pjs)) { + const each = pjs[pj]; + recordDeps(each.dependencies); + recordDeps(each.devDependencies); + if (each['static-link']) { + recordDeps(each['static-link'].dependencies); + } +} + +for (const pj of Object.getOwnPropertyNames(pjs)) { + const each = pjs[pj]; + fixDeps(pj, each.dependencies); + fixDeps(pj, each.devDependencies); + if (each['static-link']) { + fixDeps(pj, each['static-link'].dependencies); + } +} +var changed = 0; + +// write out the results. +for (const each of rush.projects) { + const packageName = each.packageName; + const projectFolder = each.projectFolder; + if (writeIfChanged(`${__dirname}/../${projectFolder}/package.json`, pjs[packageName])) { + changed++; + } +} + +if (changed) { + console.log(`Updated ${changed} files.`); +} else { + console.log('No changes made') +} \ No newline at end of file diff --git a/ce/.scripts/watch.js b/ce/.scripts/watch.js new file mode 100644 index 0000000000..cd3e491910 --- /dev/null +++ b/ce/.scripts/watch.js @@ -0,0 +1,33 @@ +var cp = require('child_process'); + +require('./for-each').forEachProject((packageName, projectFolder, project) => { + if (project.scripts && project.scripts.watch) { + // NOTE: We deliberately use `tsc --watch --project ${projectFolder}` here + // with cwd at the repo root instead of `npm run watch` with cwd at project + // folder. This ensures that error messages put source file paths relative + // to the repo root, which then allows VS Code to navigate to error + // locations correctly. + const tsc = `${projectFolder}/node_modules/.bin/tsc`; + const args = ['--watch', '--project', projectFolder]; + console.log(`${tsc} ${args.join(' ')}`); + + const proc = cp.spawn(tsc, args, { cwd: `${__dirname}/../`, shell: true, stdio: "inherit" }); + proc.on("error", (c, s) => { + console.log(packageName); + console.error(c); + console.error(s); + }); + proc.on('exit', (c, s) => { + console.log(packageName); + console.error(c); + console.error(s); + }); + proc.on('message', (c, s) => { + console.log(packageName); + console.error(c); + console.error(s); + }) + } +}); + + diff --git a/ce/assets/LICENSE.txt b/ce/assets/LICENSE.txt new file mode 100644 index 0000000000..e54fc3fbcb --- /dev/null +++ b/ce/assets/LICENSE.txt @@ -0,0 +1,216 @@ +MICROSOFT PRE-RELEASE SOFTWARE LICENSE TERMS + +MICROSOFT VCPKG-CE PROJECT + +These license terms are an agreement between Microsoft Corporation (or based on +where you live, one of its affiliates) and you. They apply to the pre-release +software named above. The terms also apply to any Microsoft services or updates +for the software, except to the extent those have additional terms. + +IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. + +1. INSTALLATION AND USE RIGHTS. + + a. General. You may install and use any number of copies of the software. + + b. Third Party Components. The software may include third party components + with separate legal notices or governed by other agreements, as may be + described in the “…Notice” file(s) accompanying the software. + +2. PRE-RELEASE SOFTWARE. This software is a pre-release version. It may not + operate correctly or work the way a final version will. Microsoft may change + it for the final, commercial version. Microsoft is not obligated to provide + maintenance, technical support or updates to you for the software. + +3. FEEDBACK. If you give feedback about the software to Microsoft, you give to + Microsoft, without charge, the right to use, share and commercialize your + feedback in any way and for any purpose. You will not give feedback that is + subject to a license that requires Microsoft to license its software or + documentation to third parties because we include your feedback in them. + These rights survive this agreement. + +4. DATA. + + a. Data Collection. The software may collect information about you and your + use of the software, and send that to Microsoft. Microsoft may use this + information to provide services and improve our products and services. + You may opt-out of many of these scenarios, but not all, as described in + the product documentation. There are also some features in the software + that may enable you and Microsoft to collect data from users of your + applications. If you use these features, you must comply with applicable + law, including providing appropriate notices to users of your + applications together with a copy of Microsoft’s privacy statement. Our + privacy statement is located at + https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about + data collection and use in the help documentation and our privacy + statement. Your use of the software operates as your consent to these + practices. + + b. Processing of Personal Data. To the extent Microsoft is a processor or + subprocessor of personal data in connection with the software, Microsoft + makes the commitments in the European Union General Data Protection + Regulation Terms of the Online Services Terms to all customers effective + May 25, 2018, at https://docs.microsoft.com/en-us/legal/gdpr. + +5. TIME-SENSITIVE SOFTWARE. + + a) Period. This agreement is effective on your acceptance and terminates on + the earlier of (i) 30 days following first availability of a commercial + release of the software or (ii) upon termination by Microsoft. Microsoft + may extend this agreement in its discretion. + + b) Notice. You may receive periodic reminder notices of this date through + the software. + + c) Access to data. You may not be able to access data used in the software + when it stops running. + +6. FEEDBACK. If you give feedback about the software to Microsoft, you give to + Microsoft, without charge, the right to use, share and commercialize your + feedback in any way and for any purpose. You will not give feedback that is + subject to a license that requires Microsoft to license its software or + documentation to third parties because we include your feedback in them. + These rights survive this agreement. + +7. SCOPE OF LICENSE. The software is licensed, not sold. This agreement only + gives you some rights to use the software. Microsoft reserves all other + rights. Unless applicable law gives you more rights despite this limitation, + you may use the software only as expressly permitted in this agreement. In + doing so, you must comply with any technical limitations in the software + that only allow you to use it in certain ways. You may not + + - work around any technical limitations in the software; + + - reverse engineer, decompile or disassemble the software, or otherwise + attempt to derive the source code for the software, except and to the + extent required by third party licensing terms governing use of certain + open-source components that may be included with the software; + + - remove, minimize, block or modify any notices of Microsoft or its + suppliers in the software; + + - use the software in any way that is against the law; + + - share, publish, rent, or lease the software; or, + + - provide the software as a stand-alone offering or combined with any of + your applications for others to use, or transfer the software or this + agreement to any third party. + +8. EXPORT RESTRICTIONS. You must comply with all domestic and international + export laws and regulations that apply to the software, which include + restrictions on destinations, end users and end use. For further information + on export restrictions, visit www.microsoft.com/exporting. + +9. SUPPORT SERVICES. Because this software is “as is,” we may not provide + support services for it. + +10. ENTIRE AGREEMENT. This agreement, and the terms for supplements, updates, + Internet-based services and support services that you use, are the entire + agreement for the software and support services. + +11. APPLICABLE LAW. If you acquired the software in the United States, + Washington law applies to interpretation of and claims for breach of this + agreement, and the laws of the state where you live apply to all other + claims. If you acquired the software in any other country, its laws apply. + +12. CONSUMER RIGHTS; REGIONAL VARIATIONS. This agreement describes certain legal + rights. You may have other rights, including consumer rights, under the laws + of your state or country. Separate and apart from your relationship with + Microsoft, you may also have rights with respect to the party from which you + acquired the software. This agreement does not change those other rights if + the laws of your state or country do not permit it to do so. For example, if + you acquired the software in one of the below regions, or mandatory country + law applies, then the following provisions apply to you: + + a. Australia. You have statutory guarantees under the Australian Consumer + Law and nothing in this agreement is intended to affect those rights. + + b. Canada. If you acquired this software in Canada, you may stop receiving + updates by turning off the automatic update feature, disconnecting your + device from the Internet (if and when you re-connect to the Internet, + however, the software will resume checking for and installing updates), + or uninstalling the software. The product documentation, if any, may also + specify how to turn off updates for your specific device or software. + + c. Germany and Austria. + + (i) Warranty. The properly licensed software will perform substantially + as described in any Microsoft materials that accompany the software. + However, Microsoft gives no contractual guarantee in relation to the + licensed software. + + (ii) Limitation of Liability. In case of intentional conduct, gross + negligence, claims based on the Product Liability Act, as well as, + in case of death or personal or physical injury, Microsoft is liable + according to the statutory law. + + Subject to the foregoing clause (ii), Microsoft will only be liable for + slight negligence if Microsoft is in breach of such material contractual + obligations, the fulfillment of which facilitate the due performance of + this agreement, the breach of which would endanger the purpose of this + agreement and the compliance with which a party may constantly trust in + (so-called "cardinal obligations"). In other cases of slight negligence, + Microsoft will not be liable for slight negligence. + +13. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED “AS-IS.” YOU BEAR THE RISK + OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR + CONDITIONS. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT + XCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE AND NON-INFRINGEMENT. + +14. LIMITATION ON AND EXCLUSION OF DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND + ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY + OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR + INCIDENTAL DAMAGES. + + This limitation applies to (a) anything related to the software, services, + content (including code) on third party Internet sites, or third party + applications; and (b) claims for breach of contract, breach of warranty, + guarantee or condition, strict liability, negligence, or other tort to the + extent permitted by applicable law. + + It also applies even if Microsoft knew or should have known about the + possibility of the damages. The above limitation or exclusion may not apply + to you because your country may not allow the exclusion or limitation of + incidental, consequential or other damages. + +Please note: As this software is distributed in Quebec, Canada, some of the +clauses in this agreement are provided below in French. + +Remarque : Ce logiciel étant distribué au Québec, Canada, certaines des clauses +dans ce contrat sont fournies ci-dessous en français. + +EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert +« tel quel ». Toute utilisation de ce logiciel est à votre seule risque et +péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez +bénéficier de droits additionnels en vertu du droit local sur la protection des +consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le +droit locale, les garanties implicites de qualité marchande, d’adéquation à un +usage particulier et d’absence de contrefaçon sont exclues. + +LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES +DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une +indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous +ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris +les dommages spéciaux, indirects ou accessoires et pertes de bénéfices. + +Cette limitation concerne: + +- tout ce qui est relié au logiciel, aux services ou au contenu (y compris le + code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et + +- les réclamations au titre de violation de contrat ou de garantie, ou au titre + de responsabilité stricte, de négligence ou d’une autre faute dans la limite + autorisée par la loi en vigueur. + +Elle s’applique également, même si Microsoft connaissait ou devrait connaître +l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la +limitation de responsabilité pour les dommages indirects, accessoires ou de +quelque nature que ce soit, il se peut que la limitation ou l’exclusion +ci-dessus ne s’appliquera pas à votre égard. + +EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous +pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent +contrat ne modifie pas les droits que vous confèrent les lois de votre pays si +celles-ci ne le permettent pas. diff --git a/ce/assets/NOTICE.txt b/ce/assets/NOTICE.txt new file mode 100644 index 0000000000..d3f5a12faa --- /dev/null +++ b/ce/assets/NOTICE.txt @@ -0,0 +1 @@ + diff --git a/ce/assets/package.json b/ce/assets/package.json new file mode 100644 index 0000000000..03cf13c83b --- /dev/null +++ b/ce/assets/package.json @@ -0,0 +1,39 @@ +{ + "name": "vcpkg-ce", + "version": "0.7.0", + "description": "vcpkg-ce CLI", + "main": "ce/dist/main.js", + "bin": { + "ce_": "./ce/dist/main.js" + }, + "directories": { + "doc": "docs" + }, + "engines": { + "node": ">=14.17.0" + }, + "scripts": { + "postinstall": "node ./create-links create && node ./wrapper-scripts create", + "uninstall": "node ./create-links remove && node ./wrapper-scripts remove", + "prepack": "npx rimraf ./common/temp/node_modules/.pnpm/typescript* ./common/temp/node_modules/.pnpm/translate-strings* ./common/temp/node_modules/.pnpm/ts-morph* ./common/temp/node_modules/.pnpm/@types* && node ./prepare-deploy.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/vcpkg-tool.git" + }, + "files": [ + "**/*" + ], + "keywords": [ + "vcpkg-ce", + "vcpkg", + "ce" + ], + "author": "Microsoft", + "license": "MIT", + "bugs": { + "url": "https://github.com/Microsoft/vcpkg/issues" + }, + "homepage": "https://github.com/Microsoft/vcpkg#readme", + "readme": "https://github.com/Microsoft/vcpkg/blob/master/readme.md" +} diff --git a/ce/assets/prepare-deploy.js b/ce/assets/prepare-deploy.js new file mode 100644 index 0000000000..24260ec3b9 --- /dev/null +++ b/ce/assets/prepare-deploy.js @@ -0,0 +1,10 @@ +const { unlinkSync, writeFileSync } = require('fs'); +const { join } = require('path'); + +const file = join(__dirname, 'deploy-metadata.json'); +const metadata = require(file); + +const links = metadata.links; +metadata.links = links.filter(link => link.kind !== 'folderLink' || (link.linkPath.indexOf('/@types') === -1 && link.targetPath.indexOf('/typescript@') === -1)); +writeFileSync(file, JSON.stringify(metadata, null, 2)); +unlinkSync(__filename); \ No newline at end of file diff --git a/ce/assets/scripts/ce b/ce/assets/scripts/ce new file mode 100644 index 0000000000..dc4d7cd6db --- /dev/null +++ b/ce/assets/scripts/ce @@ -0,0 +1,481 @@ +#!/bin/sh + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# wrapper script for ce. +# this is intended to be dot-sourced and then you can use the ce() function. + +# DEBUGGING : +# set | grep -i ^VCPKG + +# check to see if we've been dot-sourced (should work for most POSIX shells) +sourced=0 + +if [ -n "$ZSH_EVAL_CONTEXT" ]; then + case $ZSH_EVAL_CONTEXT in *:file) sourced=1;; esac +elif [ -n "$KSH_VERSION" ]; then + [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] && sourced=1 +elif [ -n "$BASH_VERSION" ]; then + (return 0 2>/dev/null) && sourced=1 +else # All other shells: examine $0 for known shell binary filenames + # Detects `sh` and `dash`; add additional shell filenames as needed. + case ${0##*/} in sh|dash) sourced=1;; esac +fi + +if [ $sourced -eq 0 ]; then + echo 'This script is expected to be dot-sourced so that it may load ce into the' + echo 'current environment and not require permanent changes to the system when you activate.' + echo '' + echo "You should instead run '. $(basename $0)' first to import ce into the current session." + exit +fi + +# GLOBALS +VCPKG_NODE_LATEST=16.12.0 +VCPKG_NODE_REMOTE=https://nodejs.org/dist/ +VCPKG_PWD=`pwd` + +VCPKG_init() { + VCPKG_OS="$(uname | sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/')" + VCPKG_ARCH="$(uname -m | sed -e 's/x86_64/x64/;s/i86pc/x64/;s/i686/x86/;s/aarch64/arm64/')" + + case $VCPKG_OS in + ( mingw64_nt* | msys_nt* ) VCPKG_OS="win";; + ( darwin*|*bsd*) VCPKG_IS_THIS_BSD=TRUE;; + ( aix ) VCPKG_ARCH="ppc64" ;; + esac + + if [ ! -z "$VCPKG_IS_THIS_BSD" ]; then + VCPKG_START_TIME=$(date +%s) + else + VCPKG_START_TIME=$(($(date +%s%N)/1000000)) + fi + + # find important cmdline args + VCPKG_ARGS=() + for each in "$@"; do case $each in + --reset-ce) VCPKG_RESET=TRUE;; + --remove-ce) VCPKG_REMOVE=TRUE;; + --debug) VCPKG_DEBUG=TRUE && VCPKG_ARGS+=("$each");; + *) VCPKG_ARGS+=("$each");; + esac ;done +} + +VCPKG_init "$@" +shift $# + +# at this point, we're pretty sure we've been dot-sourced +# Try to locate the $VCPKG_ROOT folder, where the ce is installed. +if [ -n "$VCPKG_ROOT" ]; then + # specify it on the command line if you like, we'll export it + export VCPKG_ROOT=$VCPKG_ROOT +else + # default is off the home folder + export VCPKG_ROOT=~/.vcpkg +fi; + +CE=${VCPKG_ROOT} +mkdir -p $CE + +VCPKG_DOWNLOADS=${CE}/downloads + +VCPKG_NODE=${CE}/downloads/bin/node +VCPKG_NPM=${CE}/bin/npm + +VCPKG_debug() { + if [ ! -z "$VCPKG_IS_THIS_BSD" ]; then + local NOW=$(date +%s) + local OFFSET="$(( $NOW - $VCPKG_START_TIME )) sec" + else + local NOW=$(($(date +%s%N)/1000000)) + local OFFSET="$(( $NOW - $VCPKG_START_TIME )) msec" + fi + + if [ ! -z "$VCPKG_DEBUG" ]; then + if [ -n "$VCPKG_NESTED" ]; then + echo "[NESTED $OFFSET] $*" + else + echo "[$OFFSET] $*" + fi + fi + + if [ -n "$VCPKG_NESTED" ]; then + echo "[NESTED $OFFSET] $*" >> $VCPKG_ROOT/log.txt + else + echo "[$OFFSET] $*" >> $VCPKG_ROOT/log.txt + fi +} + +if [ ! -z "$VCPKG_RESET" ]; then + if [ -d $CE/node_modules ]; then + echo Forcing reinstall of vcpkg-ce package + rm -rf $CE/node_modules + fi + + if [ -d $CE/bin ]; then + rm -rf $CE/bin + fi + + if [ -d $CE/lib ]; then + rm -rf $CE/lib + fi +fi + +if [ ! -z "$VCPKG_REMOVE" ]; then + if [ -d $CE/node_modules ]; then + echo Removing vcpkg-ce package + rm -rf $CE/node_modules + fi + + if [ -d $CE/bin ]; then + rm -rf $CE/bin + fi + + if [ -d $CE/lib ]; then + rm -rf $CE/lib + fi + + if [ -f $VCPKG_ROOT/ce.ps1 ]; then + rm -f $VCPKG_ROOT/ce.ps1 + fi + + if [ -f $VCPKG_ROOT/ce ]; then + rm -f $VCPKG_ROOT/ce + fi + + if [ -f $VCPKG_ROOT/NOTICE.txt ]; then + rm -f $VCPKG_ROOT/NOTICE.txt + fi + + if [ -f $VCPKG_ROOT/LICENSE.txt ]; then + rm -f $VCPKG_ROOT/LICENSE.txt + fi + + # cleanup environment. + . <(set | grep -i ^VCPKG | sed -e 's/[=| |\W].*//;s/^/unset /') + + # remove functions (zsh) + which functions > /dev/null 2>&1 && . <(functions | grep -i ^vcpkg | sed -e 's/[=| |\W].*//;s/^/unset -f /') + return +fi + +VCPKG_cleanup() { + # clear things that we're not going to need for the long term + unset VCPKG_NODE_LATEST + unset VCPKG_NODE_REMOTE + unset VCPKG_PWD + unset VCPKG_NPM + unset VCPKG_NESTED + unset VCPKG_OS + unset VCPKG_ARCH + unset VCPKG_IS_THIS_BSD + unset VCPKG_DEBUG + unset -f VCPKG_bootstrap_node > /dev/null 2>&1 + unset -f VCPKG_bootstrap_ce > /dev/null 2>&1 + unset VCPKG_REMOVE + unset VCPKG_RESET + unset VCPKG_START_TIME + unset VCPKG_ARGS + if [ -f "${Z_VCPKG_POSTSCRIPT}" ]; then + command rm "${Z_VCPKG_POSTSCRIPT}" + fi + unset Z_VCPKG_POSTSCRIPT +} +VCPKG_verify_node() { + # $1 should be the folder to check + local NODE_EXE="node" + if [ "${VCPKG_OS}" = "win" ]; then + NODE_EXE="node.exe" + fi + + local N=$(which $1/$NODE_EXE) + + if [ ! -z "$N" ]; then + if [ -f $N ]; then + if [ $($N -e "[major, minor, patch ] = process.versions.node.split('.'); console.log( !!(major>16 || major == 16 & minor >= 12) )") = "true" ]; then + VCPKG_NODE=$N + VCPKG_NPM=$(which $1/npm) + VCPKG_debug using node in $1 + return 0 + fi + fi + fi + return 1; +} + +VCPKG_find_node() { + local NODES=$(find $1 | grep -i /bin/node) + + for each in $NODES; do + local d=$(dirname "$each") + VCPKG_verify_node $d + if [ $? -eq 0 ]; then + return 0; + fi + done + return 1; +} + +VCPKG_bootstrap_node() { + VCPKG_debug starting VCPKG_bootstrap_node + + # did we put one in downloads at some point? + VCPKG_find_node $VCPKG_DOWNLOADS + if [ $? -eq 0 ]; then + return 0; + fi + + # is there one on the path? + VCPKG_find_node $(dirname $(which node)) + if [ $? -eq 0 ]; then + return 0; + fi + + local NODE_EXE="node" + if [ "${VCPKG_OS}" = "win" ]; then + NODE_EXE="node.exe" + fi + + # we don't seem to have a suitable nodejs on the path + # let's grab a well-known one, cache it and use it. + + local VCPKG_ARCHIVE_EXT=".tar.gz" + local TAR_FLAGS="-zxvf" + if [ "${VCPKG_OS}" = "win" ]; then + VCPKG_ARCHIVE_EXT=".zip" + fi + + local NODE_FULLNAME="node-v${VCPKG_NODE_LATEST}-${VCPKG_OS}-${VCPKG_ARCH}" + local NODE_URI="${VCPKG_NODE_REMOTE}v${VCPKG_NODE_LATEST}/${NODE_FULLNAME}${VCPKG_ARCHIVE_EXT}" + local VCPKG_ARCHIVE="${VCPKG_DOWNLOADS}/${NODE_FULLNAME}${VCPKG_ARCHIVE_EXT}" + + if [ ! -d "${VCPKG_DOWNLOADS}" ]; then + command mkdir -p "${VCPKG_DOWNLOADS}" + fi + + echo "Downloading node from ${NODE_URI} to ${VCPKG_ARCHIVE}" + if type noglob > /dev/null 2>&1; then + noglob curl -L -# "${NODE_URI}" -o "${VCPKG_ARCHIVE}" + else + curl -L -# "${NODE_URI}" -o "${VCPKG_ARCHIVE}" + fi + + if [ ! -f "${VCPKG_ARCHIVE}" ]; then + echo "Failed to download node binary." + return 1 + fi + + # UNPACK IT + if [ "${VCPKG_OS}" = "aix" ]; then + gunzip "${VCPKG_ARCHIVE}" | tar -xvC "${VCPKG_DOWNLOADS}" "${NODE_FULLNAME}/bin/${NODE_EXE}" >> $VCPKG_ROOT/log.txt 2>&1 + else + tar $TAR_FLAGS "${VCPKG_ARCHIVE}" -C "${VCPKG_DOWNLOADS}" >> $VCPKG_ROOT/log.txt 2>&1 + fi + + # OK, we good? + VCPKG_find_node $VCPKG_DOWNLOADS + if [ $? -eq 0 ]; then + return 0; + fi + + if [ ! -f $VCPKG_NPM ]; then + echo "ERROR! Unable to find/get npm" + return 1; + fi + + VCPKG_debug installed node in ce + return 0 +} + +VCPKG_SCRIPT=${CE}/node_modules/.bin/ce +VCPKG_MAIN=${CE}/node_modules/vcpkg-ce + +VCPKG_bootstrap_ce() { + VCPKG_debug checking for installed ce $VCPKG_SCRIPT + + if [ -f $VCPKG_SCRIPT ]; then + VCPKG_debug ce is installed. + return 0 + fi + + # it's not there! + # let's install it where we want it + + # ensure we have a node_modules here, so npm won't search for one up the tree. + command mkdir -p $CE/node_modules + + echo Installing vcpkg-ce in $VCPKG_ROOT + unset VCPKG_RESET + + cd $CE + $VCPKG_NODE $VCPKG_NPM cache clean --force >> $VCPKG_ROOT/log.txt 2>&1 + local OLD_PATH=$PATH + PATH=`dirname $VCPKG_NODE`:$PATH + if [ ! -z "$USE_LOCAL_VCPKG_PKG" ]; then + echo USING LOCAL CE PACKAGE $USE_LOCAL_VCPKG_PKG + + $VCPKG_NODE $VCPKG_NPM --force install --no-save --no-lockfile --scripts-prepend-node-path=true $USE_LOCAL_VCPKG_PKG >> $VCPKG_ROOT/log.txt 2>&1 + else + $VCPKG_NODE $VCPKG_NPM --force install --no-save --no-lockfile --scripts-prepend-node-path=true https://aka.ms/vcpkg-ce.tgz >> $VCPKG_ROOT/log.txt 2>&1 + fi + + PATH=$OLD_PATH +# go back where we were + cd $VCPKG_PWD + + cp $CE/node_modules/.bin/ce* $VCPKG_ROOT/ + + # Copy the NOTICE and LICENSE files to $VCPKG_ROOT to improve discoverability. + cp $CE/node_modules/vcpkg-ce/NOTICE.txt $VCPKG_ROOT/ + cp $CE/node_modules/vcpkg-ce/LICENSE.txt $VCPKG_ROOT/ + + if [ ! -f $VCPKG_SCRIPT ]; then + echo "ERROR! Unable to find/get ce script command $VCPKG_SCRIPT" + return 1; + fi + + VCPKG_debug ce is installed + return 0; +} + +# first, let's make sure we have a good copy of node +VCPKG_bootstrap_node + +if [ $? -eq 1 ]; then + VCPKG_debug failed to acquire node.js + VCPKG_cleanup + return 1; +fi + +VCPKG_bootstrap_ce + +# is ce installed? +if [ $? -eq 1 ]; then + VCPKG_debug failed to bootstrap ce + VCPKG_cleanup + return 1; +fi + +if [ -z $VCPKG_NESTED ]; then + VCPKG_debug executing final script: $VCPKG_SCRIPT + VCPKG_NESTED=TRUE + . $VCPKG_SCRIPT $VCPKG_ARGS + return # let the real script take over from here. +fi + +# So, we're the real script then. +VCPKG_debug 'real ce adding function' + +ce() { + # set | grep -i ^VCPKG + + local cst=$VCPKG_START_TIME + + VCPKG_init "$@" + + if [ ! -z "$VCPKG_RESET" ]; then + if [ -d $CE/node_modules ]; then + echo Forcing reinstall of vcpkg-ce package + rm -rf $CE/node_modules + fi + + if [ -d $CE/bin ]; then + rm -rf $CE/bin + fi + + if [ -d $CE/lib ]; then + rm -rf $CE/lib + fi + unset VCPKG_RESET + + if [ ! -z "$USE_LOCAL_VCPKG_SCRIPT" ]; then + echo USING LOCAL CE SCRIPT $USE_LOCAL_VCPKG_SCRIPT + . <(cat $USE_LOCAL_VCPKG_SCRIPT) "${VCPKG_ARGS[@]}" + else + . <(curl -L -# aka.ms/install-ce.sh) "${VCPKG_ARGS[@]}" + fi + + return 0 + fi + + if [ ! -z "$VCPKG_REMOVE" ]; then + if [ -d $CE/node_modules ]; then + unset VCPKG_REMOVE + echo Removing vcpkg-ce package + rm -rf $CE/node_modules + fi + + if [ -d $CE/bin ]; then + rm -rf $CE/bin + fi + + if [ -d $CE/lib ]; then + rm -rf $CE/lib + fi + + if [ -f $VCPKG_ROOT/ce.ps1 ]; then + rm -f $VCPKG_ROOT/ce.ps1 + fi + + if [ -f $VCPKG_ROOT/ce ]; then + rm -f $VCPKG_ROOT/ce + fi + + if [ -f $VCPKG_ROOT/NOTICE.txt ]; then + rm -f $VCPKG_ROOT/NOTICE.txt + fi + + if [ -f $VCPKG_ROOT/LICENSE.txt ]; then + rm -f $VCPKG_ROOT/LICENSE.txt + fi + + # cleanup environment + . <(set | grep -i ^VCPKG | sed -e 's/[=| |\W].*//;s/^/unset /') + + # remove functions (zsh) + which functions > /dev/null 2>&1 && . <(functions | grep -i ^vcpkg | sed -e 's/[=| |\W].*//;s/^/unset -f /') + return 0 + fi + + if [ ! -f $VCPKG_NODE ]; then + echo The installation of nodejs $VCPKG_NODE that ce is using is missing + echo You may need to reacquire ce with '. <(curl aka.ms/install-ce.sh -L)' + echo or fix your nodejs installation. + fi + + if [ ! -d $VCPKG_MAIN ]; then + echo The installation of ce is corrupted. $VCPKG_MAIN + echo You may need to reacquire ce with '. <(curl aka.ms/install-ce.sh -L)' + fi + + # set the response file + # Generate 32 bits of randomness, to avoid clashing with concurrent executions. + export Z_VCPKG_POSTSCRIPT="${VCPKG_ROOT}/VCPKG_tmp_$(dd if=/dev/urandom count=1 2> /dev/null | cksum | cut -f1 -d" ").sh" + + # call ce.js + # it picks up the Z_VCPKG_POSTSCRIPT environment variable to know where to dump the postscript + $VCPKG_NODE --harmony $VCPKG_MAIN ${VCPKG_ARGS[@]} + + VCPKG_debug called ce.js + # modify the environment + + # Call the post-invocation script if it is present, then delete it. + # This allows the invocation to potentially modify the caller's environment (e.g. PATH) + if [ -f "${Z_VCPKG_POSTSCRIPT}" ]; then + . "${Z_VCPKG_POSTSCRIPT}" + command rm "${Z_VCPKG_POSTSCRIPT}" + unset Z_VCPKG_POSTSCRIPT + fi + + VCPKG_cleanup + + VCPKG_START_TIME=$cst +} + +# did they dotsource and have args go ahead and run it then! +if [ -n "$VCPKG_ARGS" ]; then + ce "${VCPKG_ARGS[@]}" +fi + +VCPKG_cleanup diff --git a/ce/assets/scripts/ce.ps1 b/ce/assets/scripts/ce.ps1 new file mode 100644 index 0000000000..c1eac5894e --- /dev/null +++ b/ce/assets/scripts/ce.ps1 @@ -0,0 +1,452 @@ +@(echo off) > $null +if #ftw NEQ '' goto :init +($true){ $Error.clear(); } + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# wrapper script for ce. +# this is intended to be dot-sourced and then you can use the ce() function + +# unpack arguments if they came from CMD +$hash=@{}; +get-item env:argz* |% { $hash[$_.name] = $_.value } +if ($hash.count -gt 0) { + $args=for ($i=0; $i -lt $hash.count;$i++) { $hash["ARGZ[$i]"] } +} +# force the array to be an arraylist since we like to mutate it. +$args=[System.Collections.ArrayList][System.Array]$args + +# GLOBALS +$VCPKG_NODE_LATEST='16.12.0' +$VCPKG_NODE_REMOTE='https://nodejs.org/dist/' +$VCPKG_START_TIME=get-date + +function resolve([string]$name) { + $name = Resolve-Path $name -ErrorAction 0 -ErrorVariable _err + if (-not($name)) { return $_err[0].TargetObject } + $Error.clear() + return $name +} + +$SCRIPT:DEBUG=( $args.indexOf('--debug') -gt -1 ) + +function ce-debug() { + $t = [int32]((get-date).Subtract(($VCPKG_START_TIME)).ticks/10000) + if($SCRIPT:DEBUG) { + write-host -fore green "[$t msec] " -nonewline + write-host -fore gray $args + } + write-output "[$t msec] $args" >> $VCPKG_ROOT/log.txt +} + +function download($url, $path) { + $wc = New-Object net.webclient + + if( test-path -ea 0 $path) { + # check to see if the size is a match before downloading + $s = $wc.OpenRead($url) + $len = $wc.ResponseHeaders['Content-Length'] + $s.Dispose() + if( (get-item $path).Length -eq $len ){ + $wc.Dispose(); + ce-debug "skipping download of '$url' - '$path' is ok." + return $path; + } + } + ce-debug "Downloading '$url' -> '$path'" + $wc.DownloadFile($url, $path); + $wc.Dispose(); + if( (get-item $path).Length -ne $wc.ResponseHeaders['Content-Length'] ) { + throw "Download of '$url' failed. Check your internet connection." + } + ce-debug "Completed Download of $url" + return $path +} + +# set the home path. +if( $ENV:VCPKG_ROOT ) { + $SCRIPT:VCPKG_ROOT=(resolve $ENV:VCPKG_ROOT) + $ENV:VCPKG_ROOT=$VCPKG_ROOT +} else { + $SCRIPT:VCPKG_ROOT=(resolve "$HOME/.vcpkg") + $ENV:VCPKG_ROOT=$VCPKG_ROOT +} + +# set the download path +if( $ENV:VCPKG_DOWNLOADS ) { + $SCRIPT:VCPKG_DOWNLOADS= (resolve $ENV:VCPKG_DOWNLOADS) + $ENV:VCPKG_DOWNLOADS=$VCPKG_DOWNLOADS +} else { + $SCRIPT:VCPKG_DOWNLOADS= (resolve "$VCPKG_ROOT/downloads") + $ENV:VCPKG_DOWNLOADS=$VCPKG_DOWNLOADS +} + +$CE = "${VCPKG_ROOT}" +$MODULES= "$CE/node_modules" + +$SCRIPT:VCPKG_SCRIPT=(resolve $MODULES/.bin/ce.ps1) +$SCRIPT:CE_MODULE=(resolve $MODULES/vcpkg-ce ) + +$reset = $args.IndexOf('--reset-ce') -gt -1 +$remove = $args.IndexOf('--remove-ce') -gt -1 + +if( $reset -or -$remove ) { + $args.remove('--reset-ce'); + $args.remove('--remove-ce'); + + if( $reset ) { + write-host "Resetting vcpkg-ce" + } + + remove-item -recurse -force -ea 0 "$MODULES/.bin","$MODULES" + remove-item -force -ea 0 "${VCPKG_ROOT}/ce.ps1","${VCPKG_ROOT}/ce.cmd","${VCPKG_ROOT}/ce","${VCPKG_ROOT}/NOTICE.txt","${VCPKG_ROOT}/LICENSE.txt" + $error.clear(); + + if( $remove ) { + write-host "Removing vcpkg-ce" + exit + } +} + +function verify-node($NODE) { + if( $NODE -and (get-command -ea 0 $NODE) -and ( (& $NODE -p "/(^\d*\.\d*)/g.exec( process.versions.node)[0]") -ge 16.12 ) ) { + # it's a good version of node, let's set the variables + $SCRIPT:VCPKG_NODE=$NODE + $error.clear(); + return $TRUE; + } + $error.clear(); + return $FALSE +} + +function find-node() { + $PLACES= @($VCPKG_DOWNLOADS,"$ENV:LOCALAPPDATA/vcpkg/downloads/tools") + for( $i=0; $i -lt $PLACES.count; $i++ ) { + $p = $PLACES[$i] + if( $p ) { + $NODES= @()+((get-childitem -ea 0 $p -recurse |? {$_.name -in @('node.exe', 'node')}).FullName) + for( $j=0; $j -lt $NODES.count; $j++ ) { + $NODE=$NODES[$j] + if( verify-node $NODE ) { + return $NODE + } + } + } + } +} + +function bootstrap-node { + # if we have a custom ce node let's use that first + $NODE=find-node + if( $NODE ) { + ce-debug "Node: $NODE" + return $TRUE; + } + + # check the node on the path. + if( (verify-node ((get-command node -ea 0).source ))) { + ce-debug "Node: ${VCPKG_NODE}" + return $TRUE; + } + + # not there, or not good enough + if((($PSVersionTable.OS -match "windows") -or ($PSVersionTable.PSEdition -match 'desktop') ) ) { # windows + $NODE_OS='win' + switch($ENV:PROCESSOR_ARCHITECTURE) { + 'AMD64' { $NODE_ARCH='x64' } + 'ARM64' { $NODE_ARCH='arm64' } + Default { $NODE_ARCH='x86' } + } + $NODE_ARCHIVE_EXT=".zip" + } else { + $NODE_OS=(uname | sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/') + $NODE_ARCH=(uname -m | sed -e 's/x86_64/x64/;s/i86pc/x64/;s/i686/x86/;s/aarch64/arm64/') + if ( $NODE_OS -eq "aix" ) { $NODE_ARCH="ppc64" } #aix special + $NODE_ARCHIVE_EXT=".tar.gz" + } + + $NODE_FULLNAME="node-v${VCPKG_NODE_LATEST}-${NODE_OS}-${NODE_ARCH}" + $NODE_URI="${VCPKG_NODE_REMOTE}v${VCPKG_NODE_LATEST}/${NODE_FULLNAME}${NODE_ARCHIVE_EXT}" + $NODE_FOLDER=resolve "${VCPKG_DOWNLOADS}/${NODE_FULLNAME}" + $NODE_ARCHIVE=resolve "$VCPKG_DOWNLOADS/${NODE_FULLNAME}${NODE_ARCHIVE_EXT}" + + write-host "Installing node runtime" + + $ProgressPreference = 'SilentlyContinue' + ce-debug "Downloading Node: ${NODE_URI}" + download $NODE_URI $NODE_ARCHIVE + $shh = new-item -type directory -ea 0 $NODE_FOLDER + + switch($NODE_OS){ + 'win' { + if( get-command -ea 0 tar.exe ) { + tar "-xvf" "${NODE_ARCHIVE}" -C "${NODE_FOLDER}" 2>&1 > $null + } else { + $shh= expand-archive -path $NODE_ARCHIVE -destinationpath "$NODE_FOLDER" + } + } + 'aix' { + $shh = gunzip "${NODE_ARCHIVE}" | tar -xvC "$NODE_FOLDER" "${NODE_FULLNAME}/bin/" + } + default { + $shh = tar "-zxvf" "${NODE_ARCHIVE}" -C "$NODE_FOLDER" + } + } + + $NODE=find-node + if( $NODE ) { + ce-debug "Node: $NODE" + return $TRUE; + } + + write-error 'Unable to resolve nodejs' + return $FALSE; +} + +function bootstrap-vcpkg-ce { + if(test-path "${ce}/ce.ps1") { + if( test-path $VCPKG_SCRIPT ) { + return $TRUE; + } + + ## if we're running from an installed module location, we'll keep that. + $MODULE=(resolve ${PSScriptRoot}/node_modules/vcpkg-ce ) + + if( test-path $MODULE ) { + $SCRIPT:CE_MODULE=$MODULE + return $TRUE + } + } + + # cleanup the yarn cache. + ce-debug "Clearing YARN cache" + $shh = & $VCPKG_NODE $YARN cache clean --force 2>&1 + $error.clear(); + + write-host "Installing vcpkg-ce to ${VCPKG_ROOT}" + + if( $ENV:USE_LOCAL_VCPKG_PKG ) { + $USE_LOCAL_VCPKG_PKG=$ENV:USE_LOCAL_VCPKG_PKG + } + + $PKG = $USE_LOCAL_VCPKG_PKG + if( -not $PKG ) { + $PKG = 'https://aka.ms/vcpkg-ce.tgz' + } + pushd $CE + + $PATH = $ENV:PATH + $N_DIR=(resolve "$VCPKG_NODE/..") + $ENV:PATH="$N_DIR;$PATH" + + &$VCPKG_NODE $YARN add $PKG --no-lockfile --force --scripts-prepend-node-path=true --modules-folder=$MODULES 2>&1 >> $VCPKG_ROOT/log.txt + $ENV:PATH = $PATH + + remove-item -path $ce/package.json -ea 0 + + popd + + ce-debug 'yarn finished.' + if( $error.count -gt 0 ) { + $error |% { add-content -encoding UTF8 $VCPKG_ROOT/log.txt $_ } + $Error.clear() + } + + # we should also copy the .bin files into the $VCPKG_ROOT folder to make reactivation (without being on the PATH) easy + copy-item "$MODULES/.bin/ce*" $VCPKG_ROOT + + # Copy the NOTICE and LICENSE files to $VCPKG_ROOT to improve discoverability. + copy-item "$CE_MODULE/NOTICE.txt","$CE_MODULE/LICENSE.txt" $VCPKG_ROOT + + ce-debug "Bootstrapped vcpkg-ce: ${VCPKG_ROOT}" + + if( -not (test-path $CE_MODULE )) { + write-error "ERROR! Unable to find/get vcpkg-ce module $CE_MODULE" + return $false; + } + return $true; +} + +# ensure it's there. +$shh = new-item -type directory $CE,$MODULES,"$CE/scripts",$VCPKG_DOWNLOADS -ea 0 + +# grab the yarn cli script +$SCRIPT:YARN = resolve "$CE/scripts/yarn.js" +if( -not (test-path $SCRIPT:YARN )) { + $SCRIPT:YARN = download https://aka.ms/yarn.js $YARN +} + +if( -not (bootstrap-node )) { + write-error "Unable to acquire an appropriate version of Node." + write-error "You should install the LTS version or greater of NodeJS" + throw "Installation Unsuccessful." +} + +if( -not (bootstrap-vcpkg-ce )) { + write-error "Unable to install vcpkg-ce." + throw "Installation Unsuccessful." +} + +# export vcpkg-ce to the current shell. +$shh = New-Module -name vcpkg-ce -ArgumentList @($VCPKG_NODE,$CE_MODULE,$VCPKG_ROOT) -ScriptBlock { + param($VCPKG_NODE,$CE_MODULE,$VCPKG_ROOT) + + function resolve([string]$name) { + $name = Resolve-Path $name -ErrorAction 0 -ErrorVariable _err + if (-not($name)) { return $_err[0].TargetObject } + $Error.clear() + return $name + } + + function ce() { + if( ($args.indexOf('--remove-ce') -gt -1) -or ($args.indexOf('--reset-ce') -gt -1)) { + # we really want to do call the ps1 script to do this. + if( test-path "${VCPKG_ROOT}/ce.ps1" ) { + & "${VCPKG_ROOT}/ce.ps1" @args + } + return + } + + if( -not (test-path $CE_MODULE )) { + write-error "vcpkg-ce is not installed." + write-host -nonewline "You can reinstall vcpkg-ce by running " + write-host -fore green "iex (iwr -useb aka.ms/install-ce.ps1)" + return + } + + # setup the postscript file + # Generate 31 bits of randomness, to avoid clashing with concurrent executions. + $env:Z_VCPKG_POSTSCRIPT = resolve "${VCPKG_ROOT}/VCPKG_tmp_$(Get-Random -SetSeed $PID).ps1" + + & $VCPKG_NODE --harmony $CE_MODULE @args + + # dot-source the postscript file to modify the environment + if ($env:Z_VCPKG_POSTSCRIPT -and (Test-Path $env:Z_VCPKG_POSTSCRIPT)) { + # write-host (get-content -raw $env:Z_VCPKG_POSTSCRIPT) + $postscr = get-content -raw $env:Z_VCPKG_POSTSCRIPT + if( $postscr ) { + iex $postscr + } + Remove-Item -Force -ea 0 $env:Z_VCPKG_POSTSCRIPT,env:Z_VCPKG_POSTSCRIPT + } + } +} + +# finally, if this was run with some arguments, then let's just pass it +if( $args.length -gt 0 ) { + ce @args +} + +return +<# +:set +set ARGZ[%i%]=%1&set /a i+=1 & goto :eof + +:unset +set %1=& goto :eof + +:init +if exist $null erase $null + +:: do anything we need to before calling into powershell +if exist $null erase $null + +IF "%VCPKG_ROOT%"=="" SET VCPKG_ROOT=%USERPROFILE%\.vcpkg + +if exist %~dp0ce\node_modules\vcpkg-ce\package.json ( + :: we're running the wrapper script for a module-installed vcpkg-ce + set VCPKG_CMD=%~dpf0 + set VCPKG_MODULE=%~dp0ce\node_modules\vcpkg-ce + goto INVOKE +) + +:: we're running vcpkg-ce from the ce home folder +set VCPKG_CMD=%VCPKG_ROOT%\ce\node_modules\vcpkg-ce\ce.cmd + +:: if we're being asked to reset the install, call bootstrap +if "%1" EQU "--reset-ce" goto BOOTSTRAP + +:: if we're being asked to remove the install, call bootstrap +if "%1" EQU "--remove-ce" ( + set REMOVE_CE=TRUE + doskey ce= + goto BOOTSTRAP +) + +:: do we even have it installed? +if NOT exist "%VCPKG_CMD%" goto BOOTSTRAP + +set VCPKG_MODULE="%VCPKG_ROOT%\ce\node_modules\vcpkg-ce" + +:: if this is the actual installed vcpkg-ce, let's get to the invocation +if "%~dfp0" == "%VCPKG_CMD%" goto INVOKE + +:: this is not the 'right' ce cmd, let's forward this on to that one. +call %VCPKG_CMD% %* +set VCPKG_EXITCODE=%ERRORLEVEL% +goto :eof + +:INVOKE +:: Generate 30 bits of randomness, to avoid clashing with concurrent executions. +SET /A Z_VCPKG_POSTSCRIPT=%RANDOM% * 32768 + %RANDOM% +SET Z_VCPKG_POSTSCRIPT=%VCPKG_ROOT%\VCPKG_tmp_%Z_VCPKG_POSTSCRIPT%.cmd + +:: find the right node +if exist %VCPKG_ROOT%\ce\bin\node.exe set VCPKG_NODE=%VCPKG_ROOT%\ce\bin\node.exe +if "%VCPKG_NODE%" EQU "" ( + for %%i in (node.exe) do set VCPKG_NODE=%%~$PATH:i +) +if "%VCPKG_NODE%" EQU "" goto OHNONONODE: + +:: call the program +"%VCPKG_NODE%" --harmony "%VCPKG_MODULE%" %* +set VCPKG_EXITCODE=%ERRORLEVEL% +doskey ce="%VCPKG_CMD%" $* + +:POSTSCRIPT +:: Call the post-invocation script if it is present, then delete it. +:: This allows the invocation to potentially modify the caller's environment (e.g. PATH). +IF NOT EXIST "%Z_VCPKG_POSTSCRIPT%" GOTO :fin +CALL "%Z_VCPKG_POSTSCRIPT%" +DEL "%Z_VCPKG_POSTSCRIPT%" + +goto :fin + +:OHNONONODE +set VCPKG_EXITCODE=1 +echo "Unable to find the nodejs for ce to run." +goto fin: + +:BOOTSTRAP +:: add the cmdline args to the environment so powershell can use them +set /a i=0 & for %%a in (%*) do call :set %%a + +set POWERSHELL_EXE= +for %%i in (pwsh.exe powershell.exe) do ( + if EXIST "%%~$PATH:i" set POWERSHELL_EXE=%%~$PATH:i & goto :gotpwsh +) +:gotpwsh + +"%POWERSHELL_EXE%" -noprofile -executionpolicy unrestricted -command "iex (get-content %~dfp0 -raw)#" && set REMOVE_CE= +set VCPKG_EXITCODE=%ERRORLEVEL% + +:: clear out the argz +@for /f "delims==" %%_ in ('set ^| findstr -i argz') do call :unset %%_ + +:: if we're being asked to remove it,we're done. +if "%REMOVE_CE%" EQU "TRUE" ( + goto :fin +) + +:CREATEALIAS +doskey ce="%VCPKG_ROOT%\ce.cmd" $* + +:fin +SET Z_VCPKG_POSTSCRIPT= +SET VCPKG_CMD= +set VCPKG_NODE= + +EXIT /B %VCPKG_EXITCODE% +goto :eof +#> \ No newline at end of file diff --git a/ce/assets/wrapper-scripts.js b/ce/assets/wrapper-scripts.js new file mode 100644 index 0000000000..68ea2b3c6a --- /dev/null +++ b/ce/assets/wrapper-scripts.js @@ -0,0 +1,112 @@ +const { existsSync: exists, chmod, chmodSync } = require('fs'); +const { stat, copyFile, unlink } = require('fs').promises; +const { join } = require('path'); + +/** + * This script creates/removes custom wrapper scripts for vcpkg-ce. + */ +async function findScriptFolder() { + const root = `${__dirname}`; + let s = root; + while (true) { + s = join(s, '..'); + + // did we find a folder where the script is in the folder (windows style) + if (exists(s) && (await stat(s)).isDirectory() && ( + exists(join(s, 'ce_.ps1')) || + exists(join(s, 'ce_.cmd')) || + exists(join(s, 'ce.ps1')) || + exists(join(s, 'ce.cmd'))) + ) { + return s; + } + + // find it in a bin folder + for (const f of ['.bin', 'bin']) { + const b1 = join(s, f); + if (exists(b1) && (await stat(b1)).isDirectory() && ( + exists(join(b1, 'ce_')) || + exists(join(b1, 'ce')) || + exists(join(b1, 'ce.ps1')) || + exists(join(b1, 'ce_.ps1'))) + ) { + return b1; + } + } + + if (s === join(s, '..')) { + return undefined; + } + } +} + +async function create() { + const folder = await findScriptFolder(); + if (!folder) { + console.error("Unable to find install'd folder. Aborting.") + return process.exit(1); + } + const files = { + 'ce': { + source: 'ce', + install: process.platform !== 'win32' + }, + 'ce.ps1': { + source: 'ce.ps1', + install: true + }, + 'ce.cmd': { + source: 'ce.ps1', + install: process.platform === 'win32' + } + } + + for (const file of ['ce_', 'ce_.ps1', 'ce_.cmd']) { + // remove the normally created scripts + const target = join(folder, file); + if (exists(target)) { + await unlink(target); + } + } + + // we install all of these, because an installation from bash can still work with powershell + for (const file of Object.keys(files)) { + console.log(`file: ${file} <== ${files[file].source} if ${files[file].install}`) + if (files[file].install) { + const target = join(folder, file); + + // remove the symlink/script file if it exists + if (exists(target)) { + await unlink(target); + } + // copy the shell script into it's place + console.log(`copyFile: ${join(__dirname, "scripts", files[file].source)} ==> ${target} }`); + await copyFile(join(__dirname, "scripts", files[file].source), target); + + chmodSync(target, 0o765); + } + } +} + +async function remove() { + const folder = await findScriptFolder(); + if (!folder) { + return process.exit(0); + } + + for (const file of ['ce', 'ce.ps1', 'ce.cmd']) { + // remove the custom created scripts + const target = join(folder, file); + if (exists(target)) { + await unlink(target); + } + } +} + +if (process.argv[2] !== 'remove') { + console.error('Installing Scripts'); + create(); +} else { + console.error('After this is uninstalled, you should close this terminal.'); + remove() +} diff --git a/ce/ce.ps1 b/ce/ce.ps1 new file mode 100644 index 0000000000..733a73eaa7 --- /dev/null +++ b/ce/ce.ps1 @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ENV:NODE_OPTIONS="--enable-source-maps" + + +function resolve { + param ( [string] $name ) + $name = Resolve-Path $name -ErrorAction 0 -ErrorVariable _err + if (-not($name)) { return $_err[0].TargetObject } + $Error.clear() + return $name +} + + +if( $ENV:VCPKG_ROOT ) { + $SCRIPT:VCPKG_ROOT=(resolve $ENV:VCPKG_ROOT) + $ENV:VCPKG_ROOT=$VCPKG_ROOT +} else { + $SCRIPT:VCPKG_ROOT=(resolve "$HOME/.vcpkg") + $ENV:VCPKG_ROOT=$VCPKG_ROOT +} + +# setup the postscript file +# Generate 31 bits of randomness, to avoid clashing with concurrent executions. +$env:Z_VCPKG_POSTSCRIPT = resolve "${VCPKG_ROOT}/VCPKG_tmp_$(Get-Random -SetSeed $PID).ps1" + +node $PSScriptRoot/ce @args + +# dot-source the postscript file to modify the environment +if ($env:Z_VCPKG_POSTSCRIPT -and (Test-Path $env:Z_VCPKG_POSTSCRIPT)) { + # write-host (get-content -raw $env:Z_VCPKG_POSTSCRIPT) + $content = get-content -raw $env:Z_VCPKG_POSTSCRIPT + + if( $content ) { + iex $content + } + Remove-Item -Force $env:Z_VCPKG_POSTSCRIPT + remove-item -ea 0 -force env:Z_VCPKG_POSTSCRIPT +} diff --git a/ce/ce/.eslintignore b/ce/ce/.eslintignore new file mode 100644 index 0000000000..cc6a7fd139 --- /dev/null +++ b/ce/ce/.eslintignore @@ -0,0 +1,3 @@ +**/*.d.ts +test/scenarios/** +dist/** diff --git a/ce/ce/.eslintrc.yaml b/ce/ce/.eslintrc.yaml new file mode 100644 index 0000000000..0609828702 --- /dev/null +++ b/ce/ce/.eslintrc.yaml @@ -0,0 +1,10 @@ +--- +# configure plugins first +parser: "@typescript-eslint/parser" +plugins: +- "@typescript-eslint" +- "notice" + +# then inherit the common settings +extends: +- "../common/.default-eslintrc.yaml" \ No newline at end of file diff --git a/ce/ce/.npmignore b/ce/ce/.npmignore new file mode 100644 index 0000000000..33986ae1f9 --- /dev/null +++ b/ce/ce/.npmignore @@ -0,0 +1,20 @@ +!dist/**/* +src/ +dist/test/ +package/ +.npmignore +tsconfig.json +*.ts +changelog.md +.eslint* +!*.d.ts +*.tgz +.vscode +.scripts +attic/ +generated/ +notes.md +Examples/ +samples/ +*.log +package-deps.json diff --git a/ce/ce/LICENSE b/ce/ce/LICENSE new file mode 100644 index 0000000000..5cf7c8db62 --- /dev/null +++ b/ce/ce/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +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 diff --git a/ce/ce/amf/Requires.ts b/ce/ce/amf/Requires.ts new file mode 100644 index 0000000000..ae40b5c225 --- /dev/null +++ b/ce/ce/amf/Requires.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Scalar } from 'yaml'; +import { VersionReference as IVersionReference } from '../interfaces/metadata/version-reference'; +import { CustomScalarMap } from '../yaml/CustomScalarMap'; +import { Yaml, YAMLDictionary } from '../yaml/yaml-types'; +import { VersionReference } from './version-reference'; + +export class Requires extends CustomScalarMap { + constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(VersionReference, node, parent, key); + } + + override set(key: string, value: VersionReference | IVersionReference | string) { + if (typeof value === 'string') { + this.assert(true); // if we don't have a node at the moment, we need to create one. + this.node.set(key, new Scalar(value)); + return; + } + if (value.raw) { + this.assert(true); // if we don't have a node at the moment, we need to create one. + this.node.set(key, new Scalar(value.raw)); + } + if (value.resolved) { + this.assert(true); // if we don't have a node at the moment, we need to create one. + this.node.set(key, new Scalar(`${value.range} ${value.resolved}`)); + } else { + this.assert(true); // if we don't have a node at the moment, we need to create one. + this.node.set(key, new Scalar(value.range)); + } + } +} \ No newline at end of file diff --git a/ce/ce/amf/contact.ts b/ce/ce/amf/contact.ts new file mode 100644 index 0000000000..c296a73b69 --- /dev/null +++ b/ce/ce/amf/contact.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Dictionary } from '../interfaces/collections'; +import { Contact as IContact } from '../interfaces/metadata/contact'; +import { ValidationError } from '../interfaces/validation-error'; +import { Entity } from '../yaml/Entity'; +import { EntityMap } from '../yaml/EntityMap'; +import { Strings } from '../yaml/strings'; +import { Yaml, YAMLDictionary } from '../yaml/yaml-types'; + +export class Contact extends Entity implements IContact { + get email(): string | undefined { return this.asString(this.getMember('email')); } + set email(value: string | undefined) { this.setMember('email', value); } + + readonly roles = new Strings(undefined, this, 'role'); + /** @internal */ + override *validate(): Iterable { + yield* super.validate(); + } +} + +export class Contacts extends EntityMap implements Dictionary { + constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(Contact, node, parent, key); + } + /** @internal */ + override *validate(): Iterable { + yield* super.validate(); + if (this.exists()) { + for (const [key, contact] of this) { + yield* contact.validate(); + } + } + } +} diff --git a/ce/ce/amf/demands.ts b/ce/ce/amf/demands.ts new file mode 100644 index 0000000000..551faa296e --- /dev/null +++ b/ce/ce/amf/demands.ts @@ -0,0 +1,352 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { stream } from 'fast-glob'; +import { lstat, Stats } from 'fs'; +import { delimiter, join, resolve } from 'path'; +import { isMap, isScalar } from 'yaml'; +import { Activation } from '../artifacts/activation'; +import { i } from '../i18n'; +import { ErrorKind } from '../interfaces/error-kind'; +import { AlternativeFulfillment } from '../interfaces/metadata/alternative-fulfillment'; +import { ValidationError } from '../interfaces/validation-error'; +import { parseQuery } from '../mediaquery/media-query'; +import { Session } from '../session'; +import { Evaluator } from '../util/evaluator'; +import { cmdlineToArray, execute } from '../util/exec-cmd'; +import { createSandbox } from '../util/safeEval'; +import { Entity } from '../yaml/Entity'; +import { EntityMap } from '../yaml/EntityMap'; +import { Strings } from '../yaml/strings'; +import { Primitive, Yaml, YAMLDictionary } from '../yaml/yaml-types'; +import { Installs } from './installer'; +import { Requires } from './Requires'; +import { Settings } from './settings'; + +/** sandboxed eval function for evaluating expressions */ +const safeEval: (code: string, context?: any) => T = createSandbox(); +const hostFeatures = new Set(['x64', 'x86', 'arm', 'arm64', 'windows', 'linux', 'osx', 'freebsd']); + +const ignore = new Set(['info', 'contacts', 'error', 'message', 'warning', 'requires', 'see-also']); +/** + * A map of mediaquery to DemandBlock + */ +export class Demands extends EntityMap { + constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(DemandBlock, node, parent, key); + } + + override get keys() { + return super.keys.filter(each => !ignore.has(each)); + } + + /** @internal */ + override *validate(): Iterable { + yield* super.validate(); + + for (const [mediaQuery, demandBlock] of this) { + if (ignore.has(mediaQuery)) { + continue; + } + if (!isMap(demandBlock.node)) { + yield { + message: `Conditional demand '${mediaQuery}' is not an object`, + range: demandBlock.node!.range || [0, 0, 0], + category: ErrorKind.IncorrectType + }; + continue; + } + + const query = parseQuery(mediaQuery); + if (!query.isValid) { + yield { message: i`Error parsing conditional demand '${mediaQuery}'- ${query.error?.message}`, range: this.sourcePosition(mediaQuery)/* mediaQuery.range! */, rangeOffset: query.error, category: ErrorKind.ParseError }; + continue; + } + + yield* demandBlock.validate(); + } + } +} + +export class DemandBlock extends Entity { + #environment: Record = {}; + #activation?: Activation; + #data?: Record; + + setActivation(activation?: Activation) { + this.#activation = activation; + } + + setData(data: Record) { + this.#data = data; + } + + setEnvironment(env: Record) { + this.#environment = env; + } + + protected get evaluationBlock() { + return new Evaluator(this.#data || {}, this.#environment, this.#activation?.output || {}); + } + get error(): string | undefined { return this.usingAlternative ? this.unless.error : this.asString(this.getMember('error')); } + set error(value: string | undefined) { this.setMember('error', value); } + + get warning(): string | undefined { return this.usingAlternative ? this.unless.warning : this.asString(this.getMember('warning')); } + set warning(value: string | undefined) { this.setMember('warning', value); } + + get message(): string | undefined { return this.usingAlternative ? this.unless.warning : this.asString(this.getMember('message')); } + set message(value: string | undefined) { this.setMember('message', value); } + + get seeAlso(): Requires { + return this.usingAlternative ? this.unless.seeAlso : this._seeAlso; + } + + get requires(): Requires { + return this.usingAlternative ? this.unless.requires : this._requires; + } + + get settings(): Settings { + return this.usingAlternative ? this.unless.settings : this._settings; + } + + get install(): Installs { + return this.usingAlternative ? this.unless.install : this._install; + } + + protected readonly _seeAlso = new Requires(undefined, this, 'seeAlso'); + protected readonly _requires = new Requires(undefined, this, 'requires'); + protected readonly _settings = new Settings(undefined, this, 'settings'); + protected readonly _install = new Installs(undefined, this, 'install'); + + readonly unless!: Unless; + + protected usingAlternative: boolean | undefined; + + constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(node, parent, key); + if (key !== 'unless') { + this.unless = new Unless(undefined, this, 'unless'); + } + } + + /** + * Async Initializer. + * + * checks the alternative demand resolution. + * when this runs, if the alternative is met, the rest of the demand is redirected to the alternative. + */ + async init(session: Session): Promise { + this.#environment = session.environment; + if (this.usingAlternative === undefined && this.has('unless')) { + await this.unless.init(session); + this.usingAlternative = this.unless.usingAlternative; + } + return this; + } + + /** @internal */ + override *validate(): Iterable { + yield* super.validate(); + if (this.exists()) { + yield* this.settings.validate(); + yield* this.requires.validate(); + yield* this.seeAlso.validate(); + yield* this.install.validate(); + } + } + + override asString(value: any): string | undefined { + if (value === undefined) { + return value; + } + return this.evaluationBlock.evaluate(isScalar(value) ? value.value : value); + } + + override asPrimitive(value: any): Primitive | undefined { + if (value === undefined) { + return value; + } + if (isScalar(value)) { + value = value.value; + } + switch (typeof value) { + case 'boolean': + case 'number': + return value; + + case 'string': { + return this.evaluationBlock.evaluate(value); + } + } + return undefined; + } +} + +/** Expands string variables in a string */ +function expandStrings(sandboxData: Record, value: string) { + let n = undefined; + + // allow $PATH instead of ${PATH} -- simplifies YAML strings + value = value.replace(/\$([a-zA-Z0-9.]+)/g, '${$1}'); + + const parts = value.split(/(\${\S+?})/g).filter(each => each).map((each, i) => { + const v = each.replace(/^\${(.*)}$/, (m, match) => safeEval(match, sandboxData) ?? each); + + if (v.indexOf(delimiter) !== -1) { + n = i; + } + + return v; + }); + + if (n === undefined) { + return parts.join(''); + } + + const front = parts.slice(0, n).join(''); + const back = parts.slice(n + 1).join(''); + + return parts[n].split(delimiter).filter(each => each).map(each => `${front}${each}${back}`).join(delimiter); +} + +/** filters output and produces a sandbox context object */ +function filter(expression: string, content: string) { + const parsed = /^\/(.*)\/(\w*)$/.exec(expression); + const output = { + $content: content + }; + if (parsed) { + const filtered = new RegExp(parsed[1], parsed[2]).exec(content); + + if (filtered) { + for (const [i, v] of filtered.entries()) { + if (i === 0) { + continue; + } + output[`$${i}`] = v; + } + } + } + return output; +} + +export class Unless extends DemandBlock implements AlternativeFulfillment { + + readonly from = new Strings(undefined, this, 'from'); + readonly where = new Strings(undefined, this, 'where'); + + get run(): string | undefined { return this.asString(this.getMember('run')); } + set run(value: string | undefined) { this.setMember('run', value); } + + get select(): string | undefined { return this.asString(this.getMember('select')); } + set select(value: string | undefined) { this.setMember('select', value); } + + get matches(): string | undefined { return this.asString(this.getMember('is')); } + set matches(value: string | undefined) { this.setMember('is', value); } + + /** @internal */ + override *validate(): Iterable { + // todo: what other validations do we need? + yield* super.validate(); + if (this.has('unless')) { + yield { + message: '"unless" is not supported in an unless block', + range: this.sourcePosition('unless'), + category: ErrorKind.InvalidDefinition + }; + } + } + + override async init(session: Session): Promise { + this.setEnvironment(session.environment); + if (this.usingAlternative === undefined) { + this.usingAlternative = false; + if (this.from.length > 0 && this.where.length > 0) { + // we're doing some kind of check. + const locations = [...this.from].map(each => expandStrings(this.evaluationBlock, each).split(delimiter)).flat(); + const binaries = [...this.where].map(each => expandStrings(this.evaluationBlock, each)); + + const search = locations.map(location => binaries.map(binary => join(location, binary).replace(/\\/g, '/'))).flat(); + + // when we find an adequate match, we stop looking + // to do so and not work hrd + + const Break = {}; + for await (const item of stream(search, { + concurrency: 1, + stats: false, fs: { + lstat: (path: string, callback: (error: NodeJS.ErrnoException | null, stats: Stats) => void) => { + // if we're done iterating, always return an error. + if (this.usingAlternative) { + return callback(Break, undefined); + } + + return lstat(path, (error, stats) => { + // just return an error, as we don't want more results. + if (this.usingAlternative) { + // just return an error, as we don't want more results. + return callback(Break, undefined); + } + + // symlink'd binaries on windows give us errors when it interrogates it too much. + if (stats && stats.mode === 41398) { + stats.mode = stats.mode & ~8192; + } + return callback(error, stats); + }); + } + } + })) { + // we found something that looks promising. + let filtered = { $0: item }; + this.setData(filtered); + if (this.run) { + + const commandline = cmdlineToArray(this.run.replace('$0', item.toString())); + const result = await execute(resolve(commandline[0]), commandline.slice(1)); + if (result.code !== 0) { + continue; + } + + filtered = filter(this.select || '', result.log); + filtered.$0 = item; + + // if we have a match expression, let's check it. + if (this.matches && !safeEval(this.matches, filtered)) { + continue; // not a match, move on + } + + // it did match, or it's just presence check + this.usingAlternative = true; + // set the data output of the check + // this is used later to fill in the settings. + this.setData(filtered); + return this; + } + } + } + } + return this; + } + + override get error(): string | undefined { return this.asString(this.getMember('error')); } + override get warning(): string | undefined { return this.asString(this.getMember('warning')); } + override get message(): string | undefined { return this.asString(this.getMember('message')); } + + override get seeAlso(): Requires { + return this._seeAlso; + } + + override get requires(): Requires { + return this._requires; + } + + override get settings(): Settings { + return this._settings; + } + + override get install(): Installs { + return this._install; + } + +} diff --git a/ce/ce/amf/document-context.ts b/ce/ce/amf/document-context.ts new file mode 100644 index 0000000000..29d2ed7da9 --- /dev/null +++ b/ce/ce/amf/document-context.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { LineCounter } from 'yaml'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; + + +export interface DocumentContext { + session: Session; + filename: string; + file: Uri; + folder: Uri; + lineCounter: LineCounter; +} \ No newline at end of file diff --git a/ce/ce/amf/global-settings.ts b/ce/ce/amf/global-settings.ts new file mode 100644 index 0000000000..64cbd2d119 --- /dev/null +++ b/ce/ce/amf/global-settings.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ScalarMap } from '../yaml/ScalarMap'; + +export class GlobalSettings extends ScalarMap { + // global settings is just a map at this point. +} + diff --git a/ce/ce/amf/info.ts b/ce/ce/amf/info.ts new file mode 100644 index 0000000000..011e5c85b8 --- /dev/null +++ b/ce/ce/amf/info.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../i18n'; +import { ErrorKind } from '../interfaces/error-kind'; +import { Info as IInfo } from '../interfaces/metadata/info'; +import { ValidationError } from '../interfaces/validation-error'; +import { Entity } from '../yaml/Entity'; +import { Flags } from '../yaml/Flags'; + + +export class Info extends Entity implements IInfo { + get version(): string { return this.asString(this.getMember('version')) || ''; } + set version(value: string) { this.setMember('version', value); } + + get id(): string { return this.asString(this.getMember('id')) || ''; } + set id(value: string) { this.setMember('id', value); } + + get summary(): string | undefined { return this.asString(this.getMember('summary')); } + set summary(value: string | undefined) { this.setMember('summary', value); } + + get priority(): number | undefined { return this.asNumber(this.getMember('priority')) || 0; } + set priority(value: number | undefined) { this.setMember('priority', value); } + + get description(): string | undefined { return this.asString(this.getMember('description')); } + set description(value: string | undefined) { this.setMember('description', value); } + + private flags = new Flags(undefined, this, 'options'); + + get dependencyOnly(): boolean { return this.flags.has('dependencyOnly'); } + set dependencyOnly(value: boolean) { this.flags.set('dependencyOnly', value); } + + /** @internal */ + override *validate(): Iterable { + yield* super.validate(); + + if (!this.has('id')) { + yield { message: i`Missing identity '${'info.id'}'`, range: this, category: ErrorKind.FieldMissing }; + } else if (!this.is('id', 'string')) { + yield { message: i`info.id should be of type 'string', found '${this.kind('id')}'`, range: this.sourcePosition('id'), category: ErrorKind.IncorrectType }; + } + + if (!this.has('version')) { + yield { message: i`Missing version '${'info.version'}'`, range: this, category: ErrorKind.FieldMissing }; + } else if (!this.is('version', 'string')) { + yield { message: i`info.version should be of type 'string', found '${this.kind('version')}'`, range: this.sourcePosition('version'), category: ErrorKind.IncorrectType }; + } + if (this.is('summary', 'string') === false) { + yield { message: i`info.summary should be of type 'string', found '${this.kind('summary')}'`, range: this.sourcePosition('summary'), category: ErrorKind.IncorrectType }; + } + if (this.is('description', 'string') === false) { + yield { message: i`info.description should be of type 'string', found '${this.kind('description')}'`, range: this.sourcePosition('description'), category: ErrorKind.IncorrectType }; + } + if (this.is('options', 'sequence') === false) { + yield { message: i`info.options should be a sequence, found '${this.kind('options')}'`, range: this.sourcePosition('options'), category: ErrorKind.IncorrectType }; + } + } +} diff --git a/ce/ce/amf/installer.ts b/ce/ce/amf/installer.ts new file mode 100644 index 0000000000..8497d4799c --- /dev/null +++ b/ce/ce/amf/installer.ts @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isMap, isSeq } from 'yaml'; +import { GitInstaller } from '../interfaces/metadata/installers/git'; +import { Installer as IInstaller } from '../interfaces/metadata/installers/Installer'; +import { NupkgInstaller } from '../interfaces/metadata/installers/nupkg'; +import { UnTarInstaller } from '../interfaces/metadata/installers/tar'; +import { UnZipInstaller } from '../interfaces/metadata/installers/zip'; +import { Entity } from '../yaml/Entity'; +import { EntitySequence } from '../yaml/EntitySequence'; +import { Flags } from '../yaml/Flags'; +import { Strings } from '../yaml/strings'; +import { Node, Yaml, YAMLDictionary } from '../yaml/yaml-types'; + +export class Installs extends EntitySequence { + constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(Installer, node, parent, key); + } + + override *[Symbol.iterator](): Iterator { + if (isMap(this.node)) { + yield this.createInstance(this.node); + } + if (isSeq(this.node)) { + for (const item of this.node.items) { + yield this.createInstance(item); + } + } + } + + protected createInstance(node: Node): Installer { + if (isMap(node)) { + if (node.has('unzip')) { + return new UnzipNode(node, this); + } + if (node.has('nupkg')) { + return new NupkgNode(node, this); + } + if (node.has('untar')) { + return new UnTarNode(node, this); + } + if (node.has('git')) { + return new GitCloneNode(node, this); + } + } + throw new Error('Unsupported node type'); + } +} + + +export class Installer extends Entity implements IInstaller { + get installerKind(): string { + throw new Error('abstract type, should not get here.'); + } + + get lang() { + return this.asString(this.getMember('lang')); + } + + get nametag() { + return this.asString(this.getMember('nametag')); + } +} + +abstract class FileInstallerNode extends Installer { + get sha256() { + return this.asString(this.getMember('sha256')); + } + + set sha256(value: string | undefined) { + this.setMember('sha256', value); + } + + get sha512() { + return this.asString(this.getMember('sha512')); + } + + set sha512(value: string | undefined) { + this.setMember('sha512', value); + } + + get strip() { + return this.asNumber(this.getMember('strip')); + } + + set strip(value: number | undefined) { + this.setMember('1', value); + } + + readonly transform = new Strings(undefined, this, 'transform'); +} +class UnzipNode extends FileInstallerNode implements UnZipInstaller { + override get installerKind() { return 'unzip'; } + + readonly location = new Strings(undefined, this, 'unzip'); + +} +class UnTarNode extends FileInstallerNode implements UnTarInstaller { + override get installerKind() { return 'untar'; } + location = new Strings(undefined, this, 'untar'); + +} +class NupkgNode extends Installer implements NupkgInstaller { + get location() { + return this.asString(this.getMember('nupkg'))!; + } + + set location(value: string) { + this.setMember('nupkg', value); + } + + override get installerKind() { return 'nupkg'; } + + get strip() { + return this.asNumber(this.getMember('strip')); + } + + set strip(value: number | undefined) { + this.setMember('1', value); + } + + get sha256() { + return this.asString(this.getMember('sha256')); + } + + set sha256(value: string | undefined) { + this.setMember('sha256', value); + } + + get sha512() { + return this.asString(this.getMember('sha512')); + } + + set sha512(value: string | undefined) { + this.setMember('sha512', value); + } + + readonly transform = new Strings(undefined, this, 'transform'); + +} +class GitCloneNode extends Installer implements GitInstaller { + override get installerKind() { return 'git'; } + + get location() { + return this.asString(this.getMember('git'))!; + } + + set location(value: string) { + this.setMember('git', value); + } + + get commit() { + return this.asString(this.getMember('commit')); + } + + set commit(value: string | undefined) { + this.setMember('commit', value); + } + + private flags = new Flags(undefined, this, 'options'); + + get full() { + return this.flags.has('full'); + } + + set full(value: boolean) { + this.flags.set('full', value); + } + + get recurse() { + return this.flags.has('recurse'); + } + + set recurse(value: boolean) { + this.flags.set('recurse', value); + } + + get subdirectory() { + return this.asString(this.getMember('subdirectory')); + } + + set subdirectory(value: string | undefined) { + this.setMember('subdirectory', value); + } + + get espidf() { + return this.flags.has('espidf'); + } + + set espidf(value: boolean) { + this.flags.set('espidf', value); + } +} diff --git a/ce/ce/amf/metadata-file.ts b/ce/ce/amf/metadata-file.ts new file mode 100644 index 0000000000..e74c2337d9 --- /dev/null +++ b/ce/ce/amf/metadata-file.ts @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +import { extname } from 'path'; +import { Document, isMap, LineCounter, parseDocument, YAMLMap } from 'yaml'; +import { Activation } from '../artifacts/activation'; +import { Registry } from '../artifacts/registry'; +import { i } from '../i18n'; +import { ErrorKind } from '../interfaces/error-kind'; +import { Profile } from '../interfaces/metadata/metadata-format'; +import { ValidationError } from '../interfaces/validation-error'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { BaseMap } from '../yaml/BaseMap'; +import { toYAML } from '../yaml/yaml'; +import { Yaml, YAMLDictionary } from '../yaml/yaml-types'; +import { Contacts } from './contact'; +import { DemandBlock, Demands } from './demands'; +import { DocumentContext } from './document-context'; +import { GlobalSettings } from './global-settings'; +import { Info } from './info'; +import { Registries } from './registries'; + + +export class MetadataFile extends BaseMap implements Profile { + readonly context: DocumentContext; + session!: Session; + + private constructor(protected document: Document.Parsed, public readonly filename: string, public lineCounter: LineCounter, public readonly registry: Registry | undefined) { + super(>document.contents); + this.context = { + filename, + lineCounter, + }; + } + + async init(session: Session): Promise { + this.context.session = session; + this.context.file = session.parseUri(this.context.filename); + this.context.folder = this.context.file.parent; + return this; + } + + static async parseMetadata(uri: Uri, session: Session, registry?: Registry): Promise { + return MetadataFile.parseConfiguration(uri.path, await uri.readUTF8(), session, registry); + } + + static async parseConfiguration(filename: string, content: string, session: Session, registry?: Registry): Promise { + const lc = new LineCounter(); + if (!content || content === 'null') { + content = '{\n}'; + } + const doc = parseDocument(content, { prettyErrors: false, lineCounter: lc, strict: true }); + return new MetadataFile(doc, filename, lc, registry).init(session); + } + + info = new Info(undefined, this, 'info'); + + contacts = new Contacts(undefined, this, 'contacts'); + registries = new Registries(undefined, this, 'registries'); + globalSettings = new GlobalSettings(undefined, this, 'global'); + + // rather than re-implement it, use encapsulatiob with a demand block + private demandBlock = new DemandBlock(this.node, undefined); + + get error(): string | undefined { return this.demandBlock.error; } + set error(value: string | undefined) { this.demandBlock.error = value; } + + get warning(): string | undefined { return this.demandBlock.warning; } + set warning(value: string | undefined) { this.demandBlock.warning = value; } + + get message(): string | undefined { return this.demandBlock.message; } + set message(value: string | undefined) { this.demandBlock.message = value; } + + get seeAlso() { return this.demandBlock.seeAlso; } + get requires() { return this.demandBlock.requires; } + get settings() { return this.demandBlock.settings; } + get install() { return this.demandBlock.install; } + get unless() { return this.demandBlock.unless; } + + setActivation(activation: Activation): void { + this.demandBlock.setActivation(activation); + } + + conditionalDemands = new Demands(undefined, this, 'demands'); + + get isFormatValid(): boolean { + return this.document.errors.length === 0; + } + + get content() { + return toYAML(this.document.toString()); + } + + async save(uri: Uri = this.context.file): Promise { + // check the filename, and select the format. + let content = ''; + + switch (extname(uri.path).toLowerCase()) { + case '.yaml': + case '.yml': + // format as yaml + content = this.content; + break; + + case '.json': + content = JSON.stringify(this.document.toJSON(), null, 2); + break; + default: + throw new Error(`Unsupported file type ${extname(uri.path)}`); + } + if (!content || content === 'null') { + content = '{\n}'; + } + await uri.writeUTF8(content); + } + #errors!: Array; + get formatErrors(): Array { + const t = this; + return this.#errors || (this.#errors = this.document.errors.map(each => { + const message = each.message; + const line = each.linePos?.[0].line || 1; + const column = each.linePos?.[0].col || 1; + return t.formatMessage(each.name, message, line, column); + })); + } + + /** @internal */ formatMessage(category: ErrorKind | string, message: string, line?: number, column?: number): string { + if (line !== undefined && column !== undefined) { + return `${this.filename}:${line}:${column} ${category}, ${message}`; + } else { + return `${this.filename}: ${category}, ${message}`; + } + } + + get isValid(): boolean { + return this.validationErrors.length === 0; + } + + #validationErrors!: Array; + get validationErrors(): Array { + if (this.#validationErrors) { + return this.#validationErrors; + } + + const errs = new Set(); + for (const { message, range, rangeOffset, category } of this.validate()) { + const r = Array.isArray(range) ? range : range?.sourcePosition(); + + const { line, column } = this.positionAt(r, rangeOffset); + errs.add(this.formatMessage(category, message, line, column)); + } + this.#validationErrors = [...errs]; + return this.#validationErrors; + } + + private positionAt(range?: [number, number, number?], offset?: { line: number, column: number }) { + const { line, col } = this.lineCounter.linePos(range?.[0] || 0); + + return offset ? { + // adds the offset values (which can come from the mediaquery parser) to the line & column. If MQ doesn't have a position, it's zero. + line: line + (offset.line - 1), + column: col + (offset.column - 1), + } : + { + line, column: col + }; + } + + /** @internal */ + override *validate(): Iterable { + yield* super.validate(); + + // verify that we have info + if (!this.document.has('info')) { + yield { message: i`Missing section '${'info'}'`, range: this, category: ErrorKind.SectionNotFound }; + } else { + yield* this.info.validate(); + } + + + if (this.document.has('contacts')) { + for (const each of this.contacts.values) { + yield* each.validate(); + } + } + + const set = new Set(); + + for (const [mediaQuery, demandBlock] of this.conditionalDemands) { + + if (set.has(mediaQuery)) { + yield { message: i`Duplicate keys detected in manifest: '${mediaQuery}'`, range: demandBlock, category: ErrorKind.DuplicateKey }; + } + set.add(mediaQuery); + + yield* demandBlock.validate(); + } + yield* this.conditionalDemands.validate(); + yield* this.install.validate(); + yield* this.registries.validate(); + yield* this.contacts.validate(); + yield* this.settings.validate(); + yield* this.globalSettings.validate(); + yield* this.requires.validate(); + yield* this.seeAlso.validate(); + } + + /** @internal */override assert(recreateIfDisposed = false, node = this.node): asserts this is Yaml & { node: YAMLDictionary } { + if (!isMap(this.node)) { + this.document = parseDocument('{\n}', { prettyErrors: false, lineCounter: this.context.lineCounter, strict: true }); + this.node = >this.document.contents; + } + } +} diff --git a/ce/ce/amf/registries.ts b/ce/ce/amf/registries.ts new file mode 100644 index 0000000000..0791504967 --- /dev/null +++ b/ce/ce/amf/registries.ts @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isMap, isSeq, YAMLMap } from 'yaml'; +import { Dictionary } from '../interfaces/collections'; +import { ErrorKind } from '../interfaces/error-kind'; +import { RegistryDeclaration } from '../interfaces/metadata/metadata-format'; +import { Registry as IRegistry } from '../interfaces/metadata/registries/artifact-registry'; +import { ValidationError } from '../interfaces/validation-error'; +import { isFilePath, Uri } from '../util/uri'; +import { Entity } from '../yaml/Entity'; +import { Strings } from '../yaml/strings'; +import { Node, Yaml, YAMLDictionary, YAMLSequence } from '../yaml/yaml-types'; + +export class Registries extends Yaml implements Dictionary, Iterable<[string, RegistryDeclaration]> { + *[Symbol.iterator](): Iterator<[string, RegistryDeclaration]> { + if (isMap(this.node)) { + for (const { key, value } of this.node.items) { + const v = this.createRegistry(value); + if (v) { + yield [key, v]; + } + } + } + if (isSeq(this.node)) { + for (const item of this.node.items) { + if (isMap(item)) { + const name = this.asString(item.get('name')); + if (name) { + const v = this.createRegistry(item); + if (v) { + yield [name, v]; + } + } + } + } + } + } + + clear(): void { + this.dispose(true); + } + + override createNode() { + return new YAMLSequence(); + } + + add(name: string, location?: Uri, kind?: string): RegistryDeclaration { + if (this.get(name)) { + throw new Error(`Registry ${name} already exists.`); + } + + this.assert(true); + if (isMap(this.node)) { + throw new Error('Not Implemented as a map right now.'); + } + if (isSeq(this.node)) { + const m = new YAMLMap(); + this.node.add(m); + m.set('name', name); + m.set('location', location?.formatted); + m.set('kind', kind); + } + return this.get(name)!; + } + delete(key: string): boolean { + const n = this.node; + if (isMap(n)) { + const result = n.delete(key); + this.dispose(); + return result; + } + if (isSeq(n)) { + let removed = false; + const items = n.items; + for (let i = items.length - 1; i >= 0; i--) { + const item = items[i]; + if (isMap(item) && item.get('name') === key) { + removed ||= n.delete(i); + } + } + this.dispose(); + return removed; + } + return false; + } + get(key: string): RegistryDeclaration | undefined { + const n = this.node; + if (isMap(n)) { + return this.createRegistry(n.get(key, true)); + } + if (isSeq(n)) { + for (const item of n.items) { + if (isMap(item) && item.get('name') === key) { + return this.createRegistry(item); + } + } + } + return undefined; + } + + has(key: string): boolean { + const n = this.node; + if (isMap(n)) { + return n.has(key); + } + if (isSeq(n)) { + for (const item of n.items) { + if (isMap(item) && item.get('name') === key) { + return true; + } + } + } + return false; + } + + get length(): number { + if (isMap(this.node) || isSeq(this.node)) { + return this.node.items.length; + } + return 0; + } + get keys(): Array { + if (isMap(this.node)) { + return this.node.items.map(({ key }) => this.asString(key) || ''); + } + if (isSeq(this.node)) { + const result = new Array(); + for (const item of this.node.items) { + if (isMap(item)) { + const n = this.asString(item.get('name')); + if (n) { + result.push(n); + } + } + } + return result; + } + return []; + } + + protected createRegistry(node: Node) { + if (isMap(node)) { + const k = this.asString(node.get('kind')); + const l = this.asString(node.get('location')); + + // simplistic check to see if we're pointing to a file or a https:// url + if (k === 'artifact' && l) { + const ll = l?.toLowerCase(); + if (ll.startsWith('https://')) { + return new RemoteRegistry(node, this); + } + if (isFilePath(l)) { + return new LocalRegistry(node, this); + } + } + + } + return undefined; + } + /** @internal */ + override *validate(): Iterable { + if (this.exists()) { + for (const [key, registry] of this) { + yield* registry.validate(); + } + } + } +} + +export class Registry extends Entity implements IRegistry { + + get registryKind(): string | undefined { return this.asString(this.getMember('kind')); } + set registryKind(value: string | undefined) { this.setMember('kind', value); } + + /** @internal */ + override *validate(): Iterable { + // + if (this.registryKind === undefined) { + yield { + message: 'Registry missing \'kind\'', + range: this, + category: ErrorKind.FieldMissing, + }; + } + } +} + +class LocalRegistry extends Registry { + readonly location = new Strings(undefined, this, 'location'); + /** @internal */ + override *validate(): Iterable { + // + if (this.registryKind !== 'artifact') { + yield { + message: 'Registry \'kind\' is not correct for LocalRegistry ', + range: this, + category: ErrorKind.IncorrectType, + }; + } + } +} + +class RemoteRegistry extends Registry { + readonly location = new Strings(undefined, this, 'location'); + override *validate(): Iterable { + // + if (this.registryKind !== 'artifact') { + yield { + message: 'Registry \'kind\' is not correct for LocalRegistry ', + range: this, + category: ErrorKind.IncorrectType, + }; + } + } +} diff --git a/ce/ce/amf/settings.ts b/ce/ce/amf/settings.ts new file mode 100644 index 0000000000..68ae300167 --- /dev/null +++ b/ce/ce/amf/settings.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +import { Settings as ISettings } from '../interfaces/metadata/Settings'; +import { ValidationError } from '../interfaces/validation-error'; +import { BaseMap } from '../yaml/BaseMap'; +import { ScalarMap } from '../yaml/ScalarMap'; +import { StringsMap } from '../yaml/strings'; + +export class Settings extends BaseMap implements ISettings { + paths: StringsMap = new StringsMap(undefined, this, 'paths'); + locations: ScalarMap = new ScalarMap(undefined, this, 'locations'); + properties: StringsMap = new StringsMap(undefined, this, 'properties'); + variables: StringsMap = new StringsMap(undefined, this, 'variables'); + tools: ScalarMap = new ScalarMap(undefined, this, 'tools'); + defines: ScalarMap = new ScalarMap(undefined, this, 'defines'); + + /** @internal */ + override *validate(): Iterable { + // todo: what validations do we need? + + } +} diff --git a/ce/ce/amf/version-reference.ts b/ce/ce/amf/version-reference.ts new file mode 100644 index 0000000000..b9701e6356 --- /dev/null +++ b/ce/ce/amf/version-reference.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Range, SemVer } from 'semver'; +import { VersionReference as IVersionReference } from '../interfaces/metadata/version-reference'; +import { Yaml, YAMLScalar } from '../yaml/yaml-types'; + + +// nuget-semver parser doesn't have a ts typings package +// eslint-disable-next-line @typescript-eslint/no-var-requires +const parseRange: any = require('@snyk/nuget-semver/lib/range-parser'); + +export class VersionReference extends Yaml implements IVersionReference { + get raw(): string | undefined { + return this.node?.value || undefined; + } + + set raw(value: string | undefined) { + if (value === undefined) { + this.dispose(true); + } else { + this.node = new YAMLScalar(value); + } + } + + static override create(): YAMLScalar { + return new YAMLScalar(''); + } + + private split(): [Range, SemVer | undefined] { + + const v = this.raw; + if (v) { + + const [, a, b] = /(.+)\s+([\d\\.]+)/.exec(v) || []; + + if (/\[|\]|\(|\)/.exec(v)) { + // looks like a nuget version range. + try { + const range = parseRange(a || v); + let str = ''; + if (range._components[0].minOperator) { + str = `${range._components[0].minOperator} ${range._components[0].minOperand}`; + } + if (range._components[0].maxOperator) { + str = `${str} ${range._components[0].maxOperator} ${range._components[0].maxOperand}`; + } + const newRange = new Range(str); + newRange.raw = a || v; + + if (b) { + const ver = new SemVer(b, true); + return [newRange, ver]; + } + + return [newRange, undefined]; + + } catch (E) { + // ignore and fall thru + } + } + + if (a) { + // we have at least a range going on here. + try { + const range = new Range(a, true); + const ver = new SemVer(b, true); + return [range, ver]; + } catch (E) { + // ignore and fall thru + } + } + // the range or version didn't resolve correctly. + // must be a range alone. + return [new Range(v, true), undefined]; + } + return [new Range('*', true), undefined]; + } + get range() { + return this.split()[0]; + } + set range(ver: Range) { + this.raw = `${ver.raw} ${this.resolved?.raw || ''}`.trim(); + } + + get resolved() { + return this.split()[1]; + } + set resolved(ver: SemVer | undefined) { + this.raw = `${this.range.raw} ${ver?.raw || ''}`.trim(); + } +} \ No newline at end of file diff --git a/ce/ce/archivers/ZipUnpacker.ts b/ce/ce/archivers/ZipUnpacker.ts new file mode 100644 index 0000000000..898357b43a --- /dev/null +++ b/ce/ce/archivers/ZipUnpacker.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ProgressTrackingStream } from '../fs/streams'; +import { InstallEvents } from '../interfaces/events'; +import { Session } from '../session'; +import { PercentageScaler } from '../util/percentage-scaler'; +import { Queue } from '../util/promise'; +import { Uri } from '../util/uri'; +import { UnpackOptions } from './options'; +import { pipeline, Unpacker } from './unpacker'; +import { ZipEntry, ZipFile } from './unzip'; + +export class ZipUnpacker extends Unpacker { + constructor(private readonly session: Session) { + super(); + } + + async unpackFile(file: ZipEntry, archiveUri: Uri, outputUri: Uri, options: UnpackOptions) { + const extractPath = Unpacker.implementOutputOptions(file.name, options); + if (extractPath) { + const destination = outputUri.join(extractPath); + const fileEntry = { + archiveUri, + destination, + path: file.name, + extractPath + }; + + this.fileProgress(fileEntry, 0); + this.session.channels.debug(`unpacking ZIP file ${archiveUri}/${file.name} => ${destination}`); + await destination.parent.createDirectory(); + const readStream = await file.read(); + const mode = ((file.attr >> 16) & 0xfff); + + const writeStream = await destination.writeStream({ mtime: file.time, mode: mode ? mode : undefined }); + const progressStream = new ProgressTrackingStream(0, file.size); + progressStream.on('progress', (filePercentage) => this.fileProgress(fileEntry, filePercentage)); + await pipeline(readStream, progressStream, writeStream); + this.fileProgress(fileEntry, 100); + this.unpacked(fileEntry); + } + } + + async unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise { + this.subscribe(events); + try { + this.session.channels.debug(`unpacking ZIP ${archiveUri} => ${outputUri}`); + + const openedFile = await archiveUri.openFile(); + const zipFile = await ZipFile.read(openedFile); + + const archiveProgress = new PercentageScaler(0, zipFile.files.size); + this.progress(0); + const q = new Queue(); + let count = 0; + for (const file of zipFile.files.values()) { + + void q.enqueue(async () => { + await this.unpackFile(file, archiveUri, outputUri, options); + this.progress(archiveProgress.scalePosition(count++)); + }); + } + await q.done; + await zipFile.close(); + this.progress(100); + } finally { + this.unsubscribe(events); + } + } +} diff --git a/ce/ce/archivers/git.ts b/ce/ce/archivers/git.ts new file mode 100644 index 0000000000..8c07df9af0 --- /dev/null +++ b/ce/ce/archivers/git.ts @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { InstallEvents } from '../interfaces/events'; +import { Session } from '../session'; +import { Credentials } from '../util/credentials'; +import { execute } from '../util/exec-cmd'; +import { isFilePath, Uri } from '../util/uri'; + +export interface CloneOptions { + force?: boolean; + credentials?: Credentials; +} + +/** @internal */ +export class Git { + #session: Session; + #toolPath: string; + #targetFolder: string; + #environment: NodeJS.ProcessEnv; + + constructor(session: Session, toolPath: string, environment: NodeJS.ProcessEnv, targetFolder: Uri) { + this.#session = session; + this.#toolPath = toolPath; + this.#targetFolder = targetFolder.fsPath; + this.#environment = environment; + } + + /** + * Method that clones a git repo into a desired location and with various options. + * @param repo The Uri of the remote repository that is desired to be cloned. + * @param events The events that may need to be updated in order to track progress. + * @param options The options that will modify how the clone will be called. + * @returns Boolean representing whether the execution was completed without error, this is not necessarily + * a gaurantee that the clone did what we expected. + */ + async clone(repo: Uri, events: Partial, options: { recursive?: boolean, depth?: number } = {}) { + const remote = await isFilePath(repo) ? repo.fsPath : repo.toString(); + + const result = await execute(this.#toolPath, [ + 'clone', + remote, + this.#targetFolder, + options.recursive ? '--recursive' : '', + options.depth ? `--depth=${options.depth}` : '', + '--progress' + ], { + env: this.#environment, + onStdErrData: (chunk) => { + // generate progress events + // this.#session.channels.debug(chunk.toString()); + const regex = /\s([0-9]*?)%/; + chunk.toString().split(/^/gim).map((x: string) => x.trim()).filter((each: any) => each).forEach((line: string) => { + const match_array = line.match(regex); + if (match_array !== null) { + events.heartbeat?.(line.trim()); + } + }); + } + }); + + if (result.code) { + return false; + } + + return true; + } + + /** + * Fetches a 'tag', this could theoretically be a commit, a tag, or a branch. + * @param remoteName Remote name to fetch from. Typically will be 'origin'. + * @param events Events that may be called in order to present progress. + * @param options Options to modify how fetch is called. + * @returns Boolean representing whether the execution was completed without error, this is not necessarily + * a gaurantee that the fetch did what we expected. + */ + async fetch(remoteName: string, events: Partial, options: { commit?: string, recursive?: boolean, depth?: number } = {}) { + const result = await execute(this.#toolPath, [ + '-C', + this.#targetFolder, + 'fetch', + remoteName, + options.commit ? options.commit : '', + options.recursive ? '--recurse-submodules' : '', + options.depth ? `--depth=${options.depth}` : '' + ], { + env: this.#environment + }); + + if (result.code) { + return false; + } + + return true; + } + + /** + * Checks out a specific commit. If no commit is given, the default behavior of a checkout will be + * used. (Checking out the current branch) + * @param events Events to possibly track progress. + * @param options Passing along a commit or branch to checkout, optionally. + * @returns Boolean representing whether the execution was completed without error, this is not necessarily + * a gaurantee that the checkout did what we expected. + */ + async checkout(events: Partial, options: { commit?: string } = {}) { + const result = await execute(this.#toolPath, [ + '-C', + this.#targetFolder, + 'checkout', + options.commit ? options.commit : '' + ], { + env: this.#environment + }); + + if (result.code) { + return false; + } + + return true; + } + + /** + * Performs a reset on the git repo. + * @param events Events to possibly track progress. + * @param options Options to control how the reset is called. + * @returns Boolean representing whether the execution was completed without error, this is not necessarily + * a gaurantee that the reset did what we expected. + */ + async reset(events: Partial, options: { commit?: string, recurse?: boolean, hard?: boolean } = {}) { + const result = await execute(this.#toolPath, [ + '-C', + this.#targetFolder, + 'reset', + options.commit ? options.commit : '', + options.recurse ? '--recurse-submodules' : '', + options.hard ? '--hard' : '' + ], { + env: this.#environment + }); + + if (result.code) { + return false; + } + + return true; + } +} diff --git a/ce/ce/archivers/options.ts b/ce/ce/archivers/options.ts new file mode 100644 index 0000000000..337d89bbb2 --- /dev/null +++ b/ce/ce/archivers/options.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** Unpacker output options */ + +export interface UnpackOptions { + /** + * Strip # directories from the path + * + * Typically used to remove excessive nested folders off the front of the paths in an archive. + */ + strip?: number; + + /** + * A regular expression to transform filenames during unpack. If the resulting file name is empty, it is not emitted. + */ + transform?: Array; +} diff --git a/ce/ce/archivers/tar.ts b/ce/ce/archivers/tar.ts new file mode 100644 index 0000000000..ed8e22e1d7 --- /dev/null +++ b/ce/ce/archivers/tar.ts @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { fail } from 'assert'; +import { pipeline as origPipeline, Readable, Transform } from 'stream'; +import { extract as tarExtract, Headers } from 'tar-stream'; +import { promisify } from 'util'; +import { createGunzip } from 'zlib'; +import { ProgressTrackingStream } from '../fs/streams'; +import { UnifiedFileSystem } from '../fs/unified-filesystem'; +import { i } from '../i18n'; +import { InstallEvents } from '../interfaces/events'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { UnpackOptions } from './options'; +import { Unpacker } from './unpacker'; + +export const pipeline = promisify(origPipeline); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const bz2 = require('unbzip2-stream'); + +abstract class BasicTarUnpacker extends Unpacker { + constructor(protected readonly session: Session) { + super(); + } + + async maybeUnpackEntry(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions, header: Headers, stream: Readable): Promise { + const streamPromise = new Promise((accept, reject) => { + stream.on('end', accept); + stream.on('error', reject); + }); + + try { + const extractPath = Unpacker.implementOutputOptions(header.name, options); + let destination: Uri | undefined = undefined; + if (extractPath) { + destination = outputUri.join(extractPath); + } + + if (destination) { + switch (header?.type) { + case 'symlink': { + const linkTargetUri = destination?.parent.join(header.linkname!) || fail(''); + await destination.parent.createDirectory(); + await (this.session.fileSystem).filesystem(linkTargetUri).createSymlink(linkTargetUri, destination!); + } + return; + + case 'link': { + // this should be a 'hard-link' -- but I'm not sure if we can make hardlinks on windows. todo: find out + const linkTargetUri = outputUri.join(Unpacker.implementOutputOptions(header.linkname!, options)!); + // quick hack + await destination.parent.createDirectory(); + await (this.session.fileSystem).filesystem(linkTargetUri).createSymlink(linkTargetUri, destination!); + } + return; + + case 'directory': + this.session.channels.debug(`in ${archiveUri.fsPath} skipping directory ${header.name}`); + return; + + case 'file': + // files handle below + break; + + default: + this.session.channels.warning(i`in ${archiveUri.fsPath} skipping ${header.name} because it is a ${header?.type || ''}`); + return; + } + + const fileEntry = { + archiveUri: archiveUri, + destination: destination, + path: header.name, + extractPath: extractPath + }; + + this.session.channels.debug(`unpacking TAR ${archiveUri}/${header.name} => ${destination}`); + this.fileProgress(fileEntry, 0); + + if (header.size) { + const parentDirectory = destination.parent; + await parentDirectory.createDirectory(); + const fileProgress = new ProgressTrackingStream(0, header.size); + fileProgress.on('progress', (filePercentage) => this.fileProgress(fileEntry, filePercentage)); + fileProgress.on('progress', (filePercentage) => events?.fileProgress?.(fileEntry, filePercentage)); + const writeStream = await destination.writeStream({ mtime: header.mtime, mode: header.mode }); + await pipeline(stream, fileProgress, writeStream); + } + + this.fileProgress(fileEntry, 100); + this.unpacked(fileEntry); + } + + } finally { + stream.resume(); + await streamPromise; + } + } + + protected async unpackTar(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions, decompressor?: Transform): Promise { + this.subscribe(events); + const archiveSize = await archiveUri.size(); + const archiveFileStream = await archiveUri.readStream(0, archiveSize); + const archiveProgress = new ProgressTrackingStream(0, archiveSize); + const tarExtractor = tarExtract(); + + tarExtractor.on('entry', (header, stream, next) => + this.maybeUnpackEntry(archiveUri, outputUri, events, options, header, stream).then(() => { + this.progress(archiveProgress.currentPercentage); + next(); + }).catch(err => (next)(err))); + + if (decompressor) { + await pipeline(archiveFileStream, archiveProgress, decompressor, tarExtractor); + } else { + await pipeline(archiveFileStream, archiveProgress, tarExtractor); + } + } +} + +export class TarUnpacker extends BasicTarUnpacker { + unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise { + this.session.channels.debug(`unpacking TAR ${archiveUri} => ${outputUri}`); + return this.unpackTar(archiveUri, outputUri, events, options); + } +} + +export class TarGzUnpacker extends BasicTarUnpacker { + unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise { + this.session.channels.debug(`unpacking TAR.GZ ${archiveUri} => ${outputUri}`); + return this.unpackTar(archiveUri, outputUri, events, options, createGunzip()); + } +} + +export class TarBzUnpacker extends BasicTarUnpacker { + unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise { + this.session.channels.debug(`unpacking TAR.BZ2 ${archiveUri} => ${outputUri}`); + return this.unpackTar(archiveUri, outputUri, events, options, bz2()); + } +} diff --git a/ce/ce/archivers/unpacker.ts b/ce/ce/archivers/unpacker.ts new file mode 100644 index 0000000000..f3b4584878 --- /dev/null +++ b/ce/ce/archivers/unpacker.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { sed } from 'sed-lite'; +import { pipeline as origPipeline } from 'stream'; +import { promisify } from 'util'; +import { InstallEvents, UnpackEvents } from '../interfaces/events'; +import { ExtendedEmitter } from '../util/events'; +import { Uri } from '../util/uri'; +import { UnpackOptions } from './options'; + +export const pipeline = promisify(origPipeline); + +export interface FileEntry { + archiveUri: Uri; + destination: Uri | undefined; + path: string; + extractPath: string | undefined; +} + +/** Unpacker base class definition */ +export abstract class Unpacker extends ExtendedEmitter { + /* Event Emitters */ + + /** EventEmitter: progress, at least once per file */ + protected progress(archivePercentage: number): void { + this.emit('progress', archivePercentage); + } + protected fileProgress(entry: Readonly, filePercentage: number): void { + this.emit('fileProgress', entry, filePercentage); + } + /** EventEmitter: unpacked, emitted per file (not per archive) */ + protected unpacked(entry: Readonly) { + this.emit('unpacked', entry); + } + + abstract unpack(archiveUri: Uri, outputUri: Uri, events: Partial, options: UnpackOptions): Promise; + + /** + * Returns a new path string such that the path has prefixCount path elements removed, and directory + * separators normalized to a single forward slash. + * If prefixCount is greater than or equal to the number of path elements in the path, undefined is returned. + */ + public static stripPath(path: string, prefixCount: number): string | undefined { + const elements = path.split(/[\\/]+/); + const hasLeadingSlash = elements.length !== 0 && elements[0].length === 0; + const hasTrailingSlash = elements.length !== 0 && elements[elements.length - 1].length === 0; + let countForUndefined = prefixCount; + if (hasLeadingSlash) { + ++countForUndefined; + } + + if (hasTrailingSlash) { + ++countForUndefined; + } + + if (elements.length <= countForUndefined) { + return undefined; + } + + if (hasLeadingSlash) { + return '/' + elements.splice(prefixCount + 1).join('/'); + } + + return elements.splice(prefixCount).join('/'); + } + + /** + * Apply OutputOptions to a path before extraction. + * @param entry The initial path to a file to unpack. + * @param options Options to apply to that file name. + * @returns If the file is to be emitted, the path to use; otherwise, undefined. + */ + protected static implementOutputOptions(path: string, options: UnpackOptions): string | undefined { + if (options.strip) { + const maybeStripped = Unpacker.stripPath(path, options.strip); + if (maybeStripped) { + path = maybeStripped; + } else { + return undefined; + } + } + + if (options.transform) { + for (const transformExpr of options.transform) { + if (!path) { + break; + } + + const sedTransformExpr = sed(transformExpr); + path = sedTransformExpr(path); + } + } + + return path; + } +} diff --git a/ce/ce/archivers/unzip.ts b/ce/ce/archivers/unzip.ts new file mode 100644 index 0000000000..b22270fa71 --- /dev/null +++ b/ce/ce/archivers/unzip.ts @@ -0,0 +1,797 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * @license node-stream-zip | (c) 2020 Antelle | https://github.com/antelle/node-stream-zip/blob/master/LICENSE + * Portions copyright https://github.com/cthackers/adm-zip | https://raw.githubusercontent.com/cthackers/adm-zip/master/LICENSE + */ + +import { Readable, Transform, TransformCallback } from 'stream'; +import { constants, createInflateRaw } from 'zlib'; +import { ReadHandle } from '../fs/filesystem'; + +interface ZipParseState { + window: FileWindowBuffer; + totalReadLength: number; + minPos: number; + lastPos: any; + chunkSize: any; + firstByte: number; + sig: number; + lastBufferPosition?: number; + pos: number; + entry: ZipEntry | null; + entriesLeft: number; + move: boolean; +} + +export class ZipFile { + private centralDirectory!: CentralDirectoryHeader; + private chunkSize = 0; + private state!: ZipParseState; + private fileSize = 0; + + /** file entries in the zip file */ + public files = new Map(); + + /** folder entries in the zip file */ + public folders = new Map(); + + /** + * archive comment + */ + public comment?: string; + + static async read(readHandle: ReadHandle) { + const result = new ZipFile(readHandle); + await result.readCentralDirectory(); + return result; + } + + close() { + return this.readHandle.close(); + } + + private constructor(private readHandle: ReadHandle) { + } + + private async readUntilFound(): Promise { + let pos = this.state.lastPos; + let bufferPosition = pos - this.state.window.position; + const buffer = this.state.window.buffer; + const minPos = this.state.minPos; + while (--pos >= minPos && --bufferPosition >= 0) { + if (buffer.length - bufferPosition >= 4 && buffer[bufferPosition] === this.state.firstByte) { + // quick check first signature byte + if (buffer.readUInt32LE(bufferPosition) === this.state.sig) { + this.state.lastBufferPosition = bufferPosition; + return; + } + } + } + if (pos === minPos) { + throw new Error('Bad archive'); + } + + this.state.lastPos = pos + 1; + this.state.chunkSize *= 2; + if (pos <= minPos) { + throw new Error('Bad archive'); + } + + const expandLength = Math.min(this.state.chunkSize, pos - minPos); + await this.state.window.expandLeft(expandLength); + return this.readUntilFound(); + } + + async readCentralDirectory() { + this.fileSize = await this.readHandle.size(); + this.chunkSize = this.chunkSize || Math.round(this.fileSize / 1000); + this.chunkSize = Math.max( + Math.min(this.chunkSize, Math.min(128 * 1024, this.fileSize)), + Math.min(1024, this.fileSize) + ); + + const totalReadLength = Math.min(consts.ENDHDR + consts.MAXFILECOMMENT, this.fileSize); + this.state = { + window: new FileWindowBuffer(this.readHandle), + totalReadLength, + minPos: this.fileSize - totalReadLength, + lastPos: this.fileSize, + chunkSize: Math.min(1024, this.chunkSize), + firstByte: consts.ENDSIGFIRST, + sig: consts.ENDSIG, + }; + await this.state.window.read(this.fileSize - this.state.chunkSize, this.state.chunkSize); + await this.readUntilFound(); + + const buffer = this.state.window.buffer; + const pos = this.state.lastBufferPosition || 0; + + this.centralDirectory = new CentralDirectoryHeader(buffer.slice(pos, pos + consts.ENDHDR)); + + this.centralDirectory.headerOffset = this.state.window.position + pos; + this.comment = this.centralDirectory.commentLength ? buffer.slice(pos + consts.ENDHDR, pos + consts.ENDHDR + this.centralDirectory.commentLength).toString() : undefined; + + if ((this.centralDirectory.volumeEntries === consts.EF_ZIP64_OR_16 && this.centralDirectory.totalEntries === consts.EF_ZIP64_OR_16) || this.centralDirectory.size === consts.EF_ZIP64_OR_32 || this.centralDirectory.offset === consts.EF_ZIP64_OR_32) { + return this.readZip64CentralDirectoryLocator(); + } else { + this.state = {}; + return this.readEntries(); + } + } + + private async readZip64CentralDirectoryLocator() { + const length = consts.ENDL64HDR; + if (this.state.lastBufferPosition! > length) { + this.state.lastBufferPosition! -= length; + } else { + this.state = { + window: this.state.window, + totalReadLength: length, + minPos: this.state.window.position - length, + lastPos: this.state.window.position, + chunkSize: this.state.chunkSize, + firstByte: consts.ENDL64SIGFIRST, + sig: consts.ENDL64SIG, + }; + await this.state.window.read(this.state.lastPos - this.state.chunkSize, this.state.chunkSize); + await this.readUntilFound(); + } + + let buffer = this.state.window.buffer; + const locHeader = new CentralDirectoryLoc64Header( + buffer.slice(this.state.lastBufferPosition, this.state.lastBufferPosition! + consts.ENDL64HDR) + ); + const readLength = this.fileSize - locHeader.headerOffset; + this.state = { + window: this.state.window, + totalReadLength: readLength, + minPos: locHeader.headerOffset, + lastPos: this.state.lastPos, + chunkSize: this.state.chunkSize, + firstByte: consts.END64SIGFIRST, + sig: consts.END64SIG, + }; + await this.state.window.read(this.fileSize - this.state.chunkSize, this.state.chunkSize); + await this.readUntilFound(); + + buffer = this.state.window.buffer; + const zip64cd = new CentralDirectoryZip64Header(buffer.slice(this.state.lastBufferPosition, this.state.lastBufferPosition! + consts.END64HDR)); + this.centralDirectory.volumeEntries = zip64cd.volumeEntries; + this.centralDirectory.totalEntries = zip64cd.totalEntries; + this.centralDirectory.size = zip64cd.size; + this.centralDirectory.offset = zip64cd.offset; + + this.state = {}; + return this.readEntries(); + } + + private async readEntries() { + this.state = { + window: new FileWindowBuffer(this.readHandle), + pos: this.centralDirectory.offset, + chunkSize: this.chunkSize, + entriesLeft: this.centralDirectory.volumeEntries, + }; + + await this.state.window.read(this.state.pos, Math.min(this.chunkSize, this.fileSize - this.state.pos)); + + while (this.state.entriesLeft) { + let bufferPos = this.state.pos - this.state.window.position; + let entry = this.state.entry; + const buffer = this.state.window.buffer; + const bufferLength = buffer.length; + + if (!entry) { + entry = new ZipEntry(this, buffer, bufferPos); + entry.headerOffset = this.state.window.position + bufferPos; + this.state.entry = entry; + this.state.pos += consts.CENHDR; + bufferPos += consts.CENHDR; + } + const entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen; + const advanceBytes = entryHeaderSize + (this.state.entriesLeft > 1 ? consts.CENHDR : 0); + + // if there isn't enough bytes read, read 'em. + if (bufferLength - bufferPos < advanceBytes) { + await this.state.window.moveRight(bufferPos); + continue; + } + + entry.processFilename(buffer, bufferPos); + entry.validateName(); + if (entry.isDirectory) { + this.folders.set(entry.name, entry); + } else { + this.files.set(entry.name, entry); + } + + this.state.entry = entry = null; + this.state.entriesLeft--; + this.state.pos += entryHeaderSize; + bufferPos += entryHeaderSize; + } + } + + private dataOffset(entry: ZipEntry) { + return entry.offset + consts.LOCHDR + entry.fnameLen + entry.extraLen; + } + + /** @internal */ + async openEntry(entry: ZipEntry) { + // is this a file? + if (!entry.isFile) { + throw new Error(`Entry ${entry} not is not a file`); + } + + // let's check to see if it's encrypted. + const buffer = Buffer.alloc(consts.LOCHDR); + await this.readHandle.readComplete(buffer, 0, buffer.length, entry.offset); + + entry.parseDataHeader(buffer); + if (entry.encrypted) { + throw new Error('Entry encrypted'); + } + + const offset = this.dataOffset((entry)); + let entryStream = this.readHandle.readStream(offset, offset + entry.compressedSize - 1); + + if (entry.method === consts.STORED) { + // nothing to do + } else if (entry.method === consts.DEFLATED) { + entryStream = entryStream.pipe(createInflateRaw({ flush: constants.Z_SYNC_FLUSH })); + } else { + throw new Error('Unknown compression method: ' + entry.method); + } + + // should check CRC? + if ((entry.flags & 0x8) === 0x8) { + entryStream = entryStream.pipe(createVerifier(entry.crc, entry.size)); + } + + return entryStream; + } +} + +const consts = { + /* The local file header */ + LOCHDR: 30, // LOC header size + LOCSIG: 0x04034b50, // "PK\003\004" + LOCVER: 4, // version needed to extract + LOCFLG: 6, // general purpose bit flag + LOCHOW: 8, // compression method + LOCTIM: 10, // modification time (2 bytes time, 2 bytes date) + LOCCRC: 14, // uncompressed file crc-32 value + LOCSIZ: 18, // compressed size + LOCLEN: 22, // uncompressed size + LOCNAM: 26, // filename length + LOCEXT: 28, // extra field length + + /* The Data descriptor */ + EXTSIG: 0x08074b50, // "PK\007\008" + EXTHDR: 16, // EXT header size + EXTCRC: 4, // uncompressed file crc-32 value + EXTSIZ: 8, // compressed size + EXTLEN: 12, // uncompressed size + + /* The central directory file header */ + CENHDR: 46, // CEN header size + CENSIG: 0x02014b50, // "PK\001\002" + CENVEM: 4, // version made by + CENVER: 6, // version needed to extract + CENFLG: 8, // encrypt, decrypt flags + CENHOW: 10, // compression method + CENTIM: 12, // modification time (2 bytes time, 2 bytes date) + CENCRC: 16, // uncompressed file crc-32 value + CENSIZ: 20, // compressed size + CENLEN: 24, // uncompressed size + CENNAM: 28, // filename length + CENEXT: 30, // extra field length + CENCOM: 32, // file comment length + CENDSK: 34, // volume number start + CENATT: 36, // internal file attributes + CENATX: 38, // external file attributes (host system dependent) + CENOFF: 42, // LOC header offset + + /* The entries in the end of central directory */ + ENDHDR: 22, // END header size + ENDSIG: 0x06054b50, // "PK\005\006" + ENDSIGFIRST: 0x50, + ENDSUB: 8, // number of entries on this disk + ENDTOT: 10, // total number of entries + ENDSIZ: 12, // central directory size in bytes + ENDOFF: 16, // offset of first CEN header + ENDCOM: 20, // zip file comment length + MAXFILECOMMENT: 0xffff, + + /* The entries in the end of ZIP64 central directory locator */ + ENDL64HDR: 20, // ZIP64 end of central directory locator header size + ENDL64SIG: 0x07064b50, // ZIP64 end of central directory locator signature + ENDL64SIGFIRST: 0x50, + ENDL64OFS: 8, // ZIP64 end of central directory offset + + /* The entries in the end of ZIP64 central directory */ + END64HDR: 56, // ZIP64 end of central directory header size + END64SIG: 0x06064b50, // ZIP64 end of central directory signature + END64SIGFIRST: 0x50, + END64SUB: 24, // number of entries on this disk + END64TOT: 32, // total number of entries + END64SIZ: 40, + END64OFF: 48, + + /* Compression methods */ + STORED: 0, // no compression + SHRUNK: 1, // shrunk + REDUCED1: 2, // reduced with compression factor 1 + REDUCED2: 3, // reduced with compression factor 2 + REDUCED3: 4, // reduced with compression factor 3 + REDUCED4: 5, // reduced with compression factor 4 + IMPLODED: 6, // imploded + // 7 reserved + DEFLATED: 8, // deflated + ENHANCED_DEFLATED: 9, // deflate64 + PKWARE: 10, // PKWare DCL imploded + // 11 reserved + BZIP2: 12, // compressed using BZIP2 + // 13 reserved + LZMA: 14, // LZMA + // 15-17 reserved + IBM_TERSE: 18, // compressed using IBM TERSE + IBM_LZ77: 19, //IBM LZ77 z + + /* General purpose bit flag */ + FLG_ENC: 0, // encrypted file + FLG_COMP1: 1, // compression option + FLG_COMP2: 2, // compression option + FLG_DESC: 4, // data descriptor + FLG_ENH: 8, // enhanced deflation + FLG_STR: 16, // strong encryption + FLG_LNG: 1024, // language encoding + FLG_MSK: 4096, // mask header values + FLG_ENTRY_ENC: 1, + + /* 4.5 Extensible data fields */ + EF_ID: 0, + EF_SIZE: 2, + + /* Header IDs */ + ID_ZIP64: 0x0001, + ID_AVINFO: 0x0007, + ID_PFS: 0x0008, + ID_OS2: 0x0009, + ID_NTFS: 0x000a, + ID_OPENVMS: 0x000c, + ID_UNIX: 0x000d, + ID_FORK: 0x000e, + ID_PATCH: 0x000f, + ID_X509_PKCS7: 0x0014, + ID_X509_CERTID_F: 0x0015, + ID_X509_CERTID_C: 0x0016, + ID_STRONGENC: 0x0017, + ID_RECORD_MGT: 0x0018, + ID_X509_PKCS7_RL: 0x0019, + ID_IBM1: 0x0065, + ID_IBM2: 0x0066, + ID_POSZIP: 0x4690, + + EF_ZIP64_OR_32: 0xffffffff, + EF_ZIP64_OR_16: 0xffff, +}; + +class CentralDirectoryHeader { + volumeEntries!: number; + totalEntries!: number; + size!: number; + offset!: number; + commentLength!: number; + headerOffset!: number; + + constructor(data: Buffer) { + if (data.length !== consts.ENDHDR || data.readUInt32LE(0) !== consts.ENDSIG) { + throw new Error('Invalid central directory'); + } + // number of entries on this volume + this.volumeEntries = data.readUInt16LE(consts.ENDSUB); + // total number of entries + this.totalEntries = data.readUInt16LE(consts.ENDTOT); + // central directory size in bytes + this.size = data.readUInt32LE(consts.ENDSIZ); + // offset of first CEN header + this.offset = data.readUInt32LE(consts.ENDOFF); + // zip file comment length + this.commentLength = data.readUInt16LE(consts.ENDCOM); + } +} + +class CentralDirectoryLoc64Header { + headerOffset!: number; + + constructor(data: Buffer) { + if (data.length !== consts.ENDL64HDR || data.readUInt32LE(0) !== consts.ENDL64SIG) { + throw new Error('Invalid zip64 central directory locator'); + } + // ZIP64 EOCD header offset + this.headerOffset = readUInt64LE(data, consts.ENDSUB); + } +} + +class CentralDirectoryZip64Header { + volumeEntries!: number; + totalEntries!: number; + size!: number; + offset!: number; + + constructor(data: Buffer) { + if (data.length !== consts.END64HDR || data.readUInt32LE(0) !== consts.END64SIG) { + throw new Error('Invalid central directory'); + } + // number of entries on this volume + this.volumeEntries = readUInt64LE(data, consts.END64SUB); + // total number of entries + this.totalEntries = readUInt64LE(data, consts.END64TOT); + // central directory size in bytes + this.size = readUInt64LE(data, consts.END64SIZ); + // offset of first CEN header + this.offset = readUInt64LE(data, consts.END64OFF); + } +} + +export class ZipEntry { + /** + * file name + */ + name!: string; + + /** + * true if it's a directory entry + */ + isDirectory!: boolean; + + /** + * file comment + */ + comment!: string | null; + + /** + * version made by + */ + verMade: number; + + /** + * version needed to extract + */ + version: number; + + /** + * encrypt, decrypt flags + */ + flags: number; + + /** + * compression method + */ + method: number; + + /** + * modification time + */ + time: Date; + + /** + * uncompressed file crc-32 value + */ + crc: number; + + /** + * compressed size + */ + compressedSize: number; + + /** + * uncompressed size + */ + size: number; + + /** + * volume number start + */ + diskStart: number; + + /** + * internal file attributes + */ + inattr: number; + + /** + * external file attributes + */ + attr: number; + + /** + * LOC header offset + */ + offset: number; + + fnameLen: number; + extraLen: number; + comLen: number; + headerOffset!: number; + + constructor(private zipFile: ZipFile, data: Buffer, offset: number) { + // data should be 46 bytes and start with "PK 01 02" + if (data.length < offset + consts.CENHDR || data.readUInt32LE(offset) !== consts.CENSIG) { + throw new Error('Invalid entry header'); + } + // version made by + this.verMade = data.readUInt16LE(offset + consts.CENVEM); + // version needed to extract + this.version = data.readUInt16LE(offset + consts.CENVER); + // encrypt, decrypt flags + this.flags = data.readUInt16LE(offset + consts.CENFLG); + // compression method + this.method = data.readUInt16LE(offset + consts.CENHOW); + // modification time (2 bytes time, 2 bytes date) + const timebytes = data.readUInt16LE(offset + consts.CENTIM); + const datebytes = data.readUInt16LE(offset + consts.CENTIM + 2); + this.time = parseZipTime(timebytes, datebytes); + + // uncompressed file crc-32 value + this.crc = data.readUInt32LE(offset + consts.CENCRC); + // compressed size + this.compressedSize = data.readUInt32LE(offset + consts.CENSIZ); + // uncompressed size + this.size = data.readUInt32LE(offset + consts.CENLEN); + // filename length + this.fnameLen = data.readUInt16LE(offset + consts.CENNAM); + // extra field length + this.extraLen = data.readUInt16LE(offset + consts.CENEXT); + // file comment length + this.comLen = data.readUInt16LE(offset + consts.CENCOM); + // volume number start + this.diskStart = data.readUInt16LE(offset + consts.CENDSK); + // internal file attributes + this.inattr = data.readUInt16LE(offset + consts.CENATT); + // external file attributes + this.attr = data.readUInt32LE(offset + consts.CENATX); + // LOC header offset + this.offset = data.readUInt32LE(offset + consts.CENOFF); + } + + read(): Promise { + return this.zipFile.openEntry(this); + } + + parseDataHeader(data: Buffer) { + // 30 bytes and should start with "PK\003\004" + if (data.readUInt32LE(0) !== consts.LOCSIG) { + throw new Error('Invalid local header'); + } + // version needed to extract + this.version = data.readUInt16LE(consts.LOCVER); + // general purpose bit flag + this.flags = data.readUInt16LE(consts.LOCFLG); + // compression method + this.method = data.readUInt16LE(consts.LOCHOW); + // modification time (2 bytes time ; 2 bytes date) + const timebytes = data.readUInt16LE(consts.LOCTIM); + const datebytes = data.readUInt16LE(consts.LOCTIM + 2); + this.time = parseZipTime(timebytes, datebytes); + + // uncompressed file crc-32 value + this.crc = data.readUInt32LE(consts.LOCCRC) || this.crc; + // compressed size + const compressedSize = data.readUInt32LE(consts.LOCSIZ); + if (compressedSize && compressedSize !== consts.EF_ZIP64_OR_32) { + this.compressedSize = compressedSize; + } + // uncompressed size + const size = data.readUInt32LE(consts.LOCLEN); + if (size && size !== consts.EF_ZIP64_OR_32) { + this.size = size; + } + // filename length + this.fnameLen = data.readUInt16LE(consts.LOCNAM); + // extra field length + this.extraLen = data.readUInt16LE(consts.LOCEXT); + } + + processFilename(data: Buffer, offset: number) { + this.name = data.slice(offset, (offset += this.fnameLen)).toString(); + const lastChar = data[offset - 1]; + this.isDirectory = lastChar === 47 || lastChar === 92; + + if (this.extraLen) { + this.readExtra(data, offset); + offset += this.extraLen; + } + this.comment = this.comLen ? data.slice(offset, offset + this.comLen).toString() : null; + } + + validateName() { + if (/\\|^\w+:|^\/|(^|\/)\.\.(\/|$)/.test(this.name)) { + throw new Error('Malicious entry: ' + this.name); + } + } + + readExtra(data: Buffer, offset: number) { + let signature, size; + const maxPos = offset + this.extraLen; + while (offset < maxPos) { + signature = data.readUInt16LE(offset); + offset += 2; + size = data.readUInt16LE(offset); + offset += 2; + if (consts.ID_ZIP64 === signature) { + this.parseZip64Extra(data, offset, size); + } + offset += size; + } + } + + parseZip64Extra(data: Buffer, offset: number, length: number) { + if (length >= 8 && this.size === consts.EF_ZIP64_OR_32) { + this.size = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 8 && this.compressedSize === consts.EF_ZIP64_OR_32) { + this.compressedSize = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 8 && this.offset === consts.EF_ZIP64_OR_32) { + this.offset = readUInt64LE(data, offset); + offset += 8; + length -= 8; + } + if (length >= 4 && this.diskStart === consts.EF_ZIP64_OR_16) { + this.diskStart = data.readUInt32LE(offset); + // offset += 4; length -= 4; + } + } + + get encrypted() { + return (this.flags & consts.FLG_ENTRY_ENC) === consts.FLG_ENTRY_ENC; + } + + get isFile() { + return !this.isDirectory; + } +} + +class FileWindowBuffer { + position = 0; + buffer = Buffer.alloc(0); + constructor(public readHandle: ReadHandle) { + } + + async read(pos: number, length: number) { + if (this.buffer.length < length) { + this.buffer = Buffer.alloc(length); + } + this.position = pos; + await this.readHandle.readComplete(this.buffer, 0, length, this.position); + } + + async expandLeft(length: number) { + this.buffer = Buffer.concat([Buffer.alloc(length), this.buffer]); + this.position -= length; + if (this.position < 0) { + this.position = 0; + } + await this.readHandle.readComplete(this.buffer, 0, length, this.position); + } + + async expandRight(length: number,) { + const offset = this.buffer.length; + this.buffer = Buffer.concat([this.buffer, Buffer.alloc(length)]); + await this.readHandle.readComplete(this.buffer, offset, length, this.position + offset); + } + + async moveRight(shift: number) { + if (shift) { + this.buffer.copy(this.buffer, 0, shift); + } + this.position += shift; + await this.readHandle.readComplete(this.buffer, this.buffer.length - shift, shift, this.position + this.buffer.length - shift); + } +} + +function createVerifier(crc: number, size: number) { + const verify = new Verifier(crc, size); + return new Transform({ + transform: (data: any, unused: BufferEncoding, passThru: TransformCallback) => { + let err; + try { + verify.data(data); + } catch (e: any) { + err = e; + } + passThru(err, data); + } + }); +} + +function createCrcTable() { + const crcTable = []; + const b = Buffer.alloc(4); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 8; --k >= 0;) { + if ((c & 1) !== 0) { + c = 0xedb88320 ^ (c >>> 1); + } else { + c = c >>> 1; + } + } + if (c < 0) { + b.writeInt32LE(c, 0); + c = b.readUInt32LE(0); + } + crcTable[n] = c; + } + return crcTable; +} + +const crcTable = createCrcTable(); + +class Verifier { + state = { + crc: ~0, + size: 0, + }; + + constructor(public crc: number, public size: number) { + } + + data(data: Buffer) { + let crc = this.state.crc; + let off = 0; + let len = data.length; + while (--len >= 0) { + crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8); + } + this.state.crc = crc; + this.state.size += data.length; + if (this.state.size >= this.size) { + const buf = Buffer.alloc(4); + buf.writeInt32LE(~this.state.crc & 0xffffffff, 0); + crc = buf.readUInt32LE(0); + if (crc !== this.crc) { + throw new Error(`Invalid CRC Expected: ${this.crc} found:${crc} `); + } + if (this.state.size !== this.size) { + throw new Error('Invalid size'); + } + } + } +} + +function parseZipTime(timebytes: number, datebytes: number) { + const timebits = toBits(timebytes, 16); + const datebits = toBits(datebytes, 16); + + const mt = { + h: parseInt(timebits.slice(0, 5).join(''), 2), + m: parseInt(timebits.slice(5, 11).join(''), 2), + s: parseInt(timebits.slice(11, 16).join(''), 2) * 2, + Y: parseInt(datebits.slice(0, 7).join(''), 2) + 1980, + M: parseInt(datebits.slice(7, 11).join(''), 2), + D: parseInt(datebits.slice(11, 16).join(''), 2), + }; + const dt_str = [mt.Y, mt.M, mt.D].join('-') + ' ' + [mt.h, mt.m, mt.s].join(':') + ' GMT+0'; + return new Date(dt_str); +} + +function toBits(dec: number, size: number) { + let b = (dec >>> 0).toString(2); + while (b.length < size) { + b = '0' + b; + } + return b.split(''); +} + +function readUInt64LE(buffer: Buffer, offset: number) { + return buffer.readUInt32LE(offset + 4) * 0x0000000100000000 + buffer.readUInt32LE(offset); +} diff --git a/ce/ce/artifacts/SetOfDemands.ts b/ce/ce/artifacts/SetOfDemands.ts new file mode 100644 index 0000000000..58d3ac41e3 --- /dev/null +++ b/ce/ce/artifacts/SetOfDemands.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { MetadataFile } from '../amf/metadata-file'; +import { Demands } from '../interfaces/metadata/demands'; +import { VersionReference } from '../interfaces/metadata/version-reference'; +import { parseQuery } from '../mediaquery/media-query'; +import { Session } from '../session'; +import { MultipleInstallsMatched } from '../util/exceptions'; +import { Dictionary, linq } from '../util/linq'; +import { Activation } from './activation'; + + +export class SetOfDemands { + _demands = new Map(); + + constructor(metadata: MetadataFile, session: Session) { + this._demands.set('', metadata); + + for (const [query, demands] of metadata.conditionalDemands) { + if (parseQuery(query).match(session.context)) { + session.channels.debug(`Matching demand query: '${query}'`); + this._demands.set(query, demands); + } + } + } + + setActivation(activation: Activation) { + for (const [, demandBlock] of this._demands.entries()) { + demandBlock.setActivation(activation); + } + } + + /** Async Initializer */ + async init(session: Session) { + for (const [query, demands] of this._demands) { + await demands.init(session); + } + } + + get installer() { + const install = linq.entries(this._demands).where(([query, demand]) => demand.install.length > 0).toArray(); + + if (install.length > 1) { + // bad. There should only ever be one install block. + throw new MultipleInstallsMatched(install.map(each => each[0])); + } + + return install[0]?.[1].install || []; + } + + get errors() { + return linq.values(this._demands).selectNonNullable(d => d.error).toArray(); + } + get warnings() { + return linq.values(this._demands).selectNonNullable(d => d.warning).toArray(); + } + get messages() { + return linq.values(this._demands).selectNonNullable(d => d.message).toArray(); + } + get settings() { + return linq.values(this._demands).selectNonNullable(d => d.settings).toArray(); + } + get seeAlso() { + return linq.values(this._demands).selectNonNullable(d => d.seeAlso).toArray(); + } + + get requires() { + const d = this._demands; + const rq1 = linq.values(d).selectNonNullable(d => d.requires).toArray(); + const result = new Dictionary(); + for (const dict of rq1) { + for (const [query, demands] of dict) { + result[query] = demands; + } + } + const rq = [...d.values()].map(each => each.requires).filter(each => each); + + for (const dict of rq) { + for (const [query, demands] of dict) { + result[query] = demands; + } + } + return result; + } +} diff --git a/ce/ce/artifacts/activation.ts b/ce/ce/artifacts/activation.ts new file mode 100644 index 0000000000..acd4611a32 --- /dev/null +++ b/ce/ce/artifacts/activation.ts @@ -0,0 +1,166 @@ +/* eslint-disable keyword-spacing */ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { delimiter } from 'path'; +import { Session } from '../session'; +import { linq } from '../util/linq'; +import { Uri } from '../util/uri'; +import { toXml } from '../util/xml'; +import { Artifact } from './artifact'; + +export class Activation { + #session: Session; + constructor(session: Session) { + this.#session = session; + } + + /** gets a flattend object representation of the activation */ + get output() { + return { + defines: Object.fromEntries(this.defines), + locations: Object.fromEntries([... this.locations.entries()].map(([k, v]) => [k, v.fsPath])), + properties: Object.fromEntries([... this.properties.entries()].map(([k, v]) => [k, v.join(',')])), + environment: { ...process.env, ...Object.fromEntries([... this.environment.entries()].map(([k, v]) => [k, v.join(' ')])) }, + tools: Object.fromEntries(this.tools), + paths: Object.fromEntries([...this.paths.entries()].map(([k, v]) => [k, v.map(each => each.fsPath).join(delimiter)])), + aliases: Object.fromEntries(this.aliases) + }; + } + + generateMSBuild(artifacts: Iterable): string { + const msbuildFile = { + Project: { + $xmlns: 'http://schemas.microsoft.com/developer/msbuild/2003', + PropertyGroup: >>[] + } + }; + + if (this.locations.size) { + msbuildFile.Project.PropertyGroup.push({ $Label: 'Locations', ...linq.entries(this.locations).toObject(([key, value]) => [key, value.fsPath]) }); + } + + if (this.properties.size) { + msbuildFile.Project.PropertyGroup.push({ $Label: 'Properties', ...linq.entries(this.properties).toObject(([key, value]) => [key, value.join(';')]) }); + } + + if (this.tools.size) { + msbuildFile.Project.PropertyGroup.push({ $Label: 'Tools', ...linq.entries(this.tools).toObject(each => each) }); + } + + if (this.environment.size) { + msbuildFile.Project.PropertyGroup.push({ $Label: 'Environment', ...linq.entries(this.environment).toObject(each => each) }); + } + + if (this.paths.size) { + msbuildFile.Project.PropertyGroup.push({ $Label: 'Paths', ...linq.entries(this.paths).toObject(([key, value]) => [key, value.map(each => each.fsPath).join(';')]) }); + } + + if (this.defines.size) { + msbuildFile.Project.PropertyGroup.push({ $Label: 'Defines', DEFINES: linq.entries(this.defines).select(([key, value]) => `${key}=${value}`).join(';') }); + } + + if (this.aliases.size) { + msbuildFile.Project.PropertyGroup.push({ $Label: 'Aliases', ...linq.entries(this.environment).toObject(each => each) }); + } + + const propertyGroup = { $Label: 'Artifacts', Artifacts: { Artifact: [] } }; + + for (const artifact of artifacts) { + propertyGroup.Artifacts.Artifact.push({ $id: artifact.metadata.info.id, '#text': artifact.targetLocation.fsPath }); + } + + if (propertyGroup.Artifacts.Artifact.length > 0) { + msbuildFile.Project.PropertyGroup.push(propertyGroup); + } + + return toXml(msbuildFile); + } + + /** a collection of #define declarations that would assumably be applied to all compiler calls. */ + defines = new Map(); + + /** a collection of tool definitions from artifacts (think shell 'aliases') */ + tools = new Map(); + + /** Aliases are tools that get exposed to the user as shell aliases */ + aliases = new Map(); + + /** a collection of 'published locations' from artifacts. useful for msbuild */ + locations = new Map(); + + /** a collection of environment variables from artifacts that are intended to be combinined into variables that have PATH delimiters */ + paths = new Map>(); + + /** environment variables from artifacts */ + environment = new Map>(); + + /** a collection of arbitrary properties from artifacts. useful for msbuild */ + properties = new Map>(); + + get Paths() { + // return just paths that have contents. + return [... this.paths.entries()].filter(([k, v]) => v.length > 0); + } + + get Variables() { + // tools + environment + const result = new Array<[string, string]>(); + + // combine variables with spaces + for (const [key, values] of this.environment) { + result.push([key, values.join(' ')]); + } + + // add tools to the list + for (const [key, value] of this.tools) { + result.push([key, value]); + } + return result; + } + + get Defines(): Array<[string, string]> { + return linq.entries(this.defines).toArray(); + } + + get Locations(): Array<[string, string]> { + return linq.entries(this.locations).select(([k, v]) => <[string, string]>[k, v.fsPath]).where(([k, v]) => v.length > 0).toArray(); + } + + get Properties(): Array<[string, Array]> { + return linq.entries(this.properties).toArray(); + } + + /** produces an environment block that can be passed to child processes to leverage dependent artifacts during installtion/activation. */ + get environmentBlock(): NodeJS.ProcessEnv { + const result = this.#session.environment; + + // add environment variables + for (const [k, v] of this.Variables) { + result[k] = v; + } + + // update environment paths + for (const [variable, values] of this.Paths) { + if (values.length) { + const s = new Set(values.map(each => each.fsPath)); + const originalVariable = result[variable] || ''; + if (originalVariable) { + for (const p of originalVariable.split(delimiter)) { + if (p) { + s.add(p); + } + } + } + result[variable] = originalVariable; + } + } + + // define tool environment variables + for (const [key, value] of this.tools) { + result[key] = value; + } + + return result; + } +} \ No newline at end of file diff --git a/ce/ce/artifacts/artifact.ts b/ce/ce/artifacts/artifact.ts new file mode 100644 index 0000000000..e493f685ff --- /dev/null +++ b/ce/ce/artifacts/artifact.ts @@ -0,0 +1,358 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { fail } from 'assert'; +import { resolve } from 'path'; +import { MetadataFile } from '../amf/metadata-file'; +import { gitArtifact, gitUniqueIdPrefix, latestVersion } from '../constants'; +import { i } from '../i18n'; +import { InstallEvents } from '../interfaces/events'; +import { Registries } from '../registries/registries'; +import { Session } from '../session'; +import { linq } from '../util/linq'; +import { Uri } from '../util/uri'; +import { Activation } from './activation'; +import { Registry } from './registry'; +import { SetOfDemands } from './SetOfDemands'; + +export type Selections = Map; +export type UID = string; +export type ID = string; +export type VersionRange = string; +export type Selection = [Artifact, ID, VersionRange] + +export class ArtifactMap extends Map{ + get artifacts() { + return [...linq.values(this).select(([artifact, id, range]) => artifact)].sort((a, b) => (b.metadata.info.priority || 0) - (a.metadata.info.priority || 0)); + } +} + +class ArtifactBase { + public registries: Registries; + readonly applicableDemands: SetOfDemands; + + constructor(protected session: Session, public readonly metadata: MetadataFile) { + this.applicableDemands = new SetOfDemands(this.metadata, this.session); + this.registries = new Registries(session); + + // load the registries from the project file + for (const [name, registry] of this.metadata.registries) { + const reg = session.loadRegistry(registry.location.get(0), registry.registryKind || 'artifact'); + if (reg) { + this.registries.add(reg, name); + } + } + } + + /** Async Initializer */ + async init(session: Session) { + await this.applicableDemands.init(session); + return this; + } + + async resolveDependencies(artifacts = new ArtifactMap(), recurse = true) { + // find the dependencies and add them to the set + + let dependency: [Registry, string, Artifact] | undefined; + for (const [id, version] of linq.entries(this.applicableDemands.requires)) { + dependency = undefined; + if (id.indexOf(':') === -1) { + if (this.metadata.registry) { + // let's assume the dependency is in the same registry as the artifact + const [registry, b, artifacts] = (await this.metadata.registry.search(this.registries, { idOrShortName: id, version: version.raw }))[0]; + dependency = [registry, b, artifacts[0]]; + if (!dependency) { + throw new Error(i`Dependency '${id}' version '${version.raw}' does not specify the registry.`); + } + } + } + dependency = dependency || await this.registries.getArtifact(id, version.raw); + if (!dependency) { + throw new Error(i`Unable to resolve dependency ${id}: ${version.raw}`); + } + const artifact = dependency[2]; + if (!artifacts.has(artifact.uniqueId)) { + artifacts.set(artifact.uniqueId, [artifact, id, version.raw || latestVersion]); + + if (recurse) { + // process it's dependencies too. + await artifact.resolveDependencies(artifacts); + } + } + } + + + if (!linq.startsWith(artifacts, gitUniqueIdPrefix)) { + // check if anyone needs git and add it if it isn't there + for (const each of this.applicableDemands.installer) { + if (each.installerKind === 'git') { + const [reg, id, art] = await this.registries.getArtifact(gitArtifact, latestVersion) || []; + if (art) { + artifacts.set(gitArtifact, [art, gitArtifact, latestVersion]); + break; + } + } + } + } + return artifacts; + } +} + +export class Artifact extends ArtifactBase { + isPrimary = false; + + constructor(session: Session, metadata: MetadataFile, public shortName: string = '', public targetLocation: Uri, public readonly registryId: string, public readonly registryUri: Uri) { + super(session, metadata); + } + + get id() { + return this.metadata.info.id; + } + + get reference() { + return `${this.registryId}:${this.id}`; + } + + get version() { + return this.metadata.info.version; + } + + get isInstalled() { + return this.targetLocation.exists('artifact.yaml'); + } + + get uniqueId() { + return `${this.registryUri.toString()}::${this.id}::${this.version}`; + } + + async install(activation: Activation, events: Partial, options: { force?: boolean, allLanguages?: boolean, language?: string }): Promise { + let installing = false; + try { + // is it installed? + const applicableDemands = this.applicableDemands; + applicableDemands.setActivation(activation); + + let isFailing = false; + for (const error of applicableDemands.errors) { + this.session.channels.error(error); + isFailing = true; + } + + if (isFailing) { + throw Error('errors present'); + } + + // warnings + for (const warning of applicableDemands.warnings) { + this.session.channels.warning(warning); + } + + // messages + for (const message of applicableDemands.messages) { + this.session.channels.message(message); + } + + if (await this.isInstalled && !options.force) { + await this.loadActivationSettings(activation); + return false; + } + installing = true; + + if (options.force) { + try { + await this.uninstall(); + } catch { + // if a file is locked, it may not get removed. We'll deal with this later. + } + } + + // ok, let's install this. + for (const installInfo of applicableDemands.installer) { + if (installInfo.lang && !options.allLanguages && options.language && options.language.toLowerCase() !== installInfo.lang.toLowerCase()) { + continue; + } + + const installer = this.session.artifactInstaller(installInfo); + if (!installer) { + fail(i`Unknown installer type ${installInfo!.installerKind}`); + } + await installer(this.session, activation, this.id, this.targetLocation, installInfo, events, options); + } + + // after we unpack it, write out the installed manifest + await this.writeManifest(); + await this.loadActivationSettings(activation); + return true; + } catch (err) { + if (installing) { + // if we started installing, and then had an error, we need to remove the artifact. + try { + await this.uninstall(); + } catch { + // if a file is locked, it may not get removed. We'll deal with this later. + } + } + throw err; + } + } + + get name() { + return `${this.metadata.info.id.replace(/[^\w]+/g, '.')}-${this.metadata.info.version}`; + } + + async writeManifest() { + await this.targetLocation.createDirectory(); + await this.metadata.save(this.targetLocation.join('artifact.yaml')); + } + + async uninstall() { + await this.targetLocation.delete({ recursive: true, useTrash: false }); + } + + async loadActivationSettings(activation: Activation) { + // construct paths (bin, lib, include, etc.) + // construct tools + // compose variables + // defines + + const l = this.targetLocation.toString().length + 1; + const allPaths = (await this.targetLocation.readDirectory(undefined, { recursive: true })).select(([name, stat]) => name.toString().substr(l)); + + for (const settingBlock of this.applicableDemands.settings) { + // **** defines **** + // eslint-disable-next-line prefer-const + for (let [key, value] of settingBlock.defines) { + if (value === 'true') { + value = '1'; + } + + const v = activation.defines.get(key); + if (v && v !== value) { + // conflict. todo: what do we want to do? + this.session.channels.warning(i`Duplicate define ${key} during activation. New value will replace old `); + } + activation.defines.set(key, value); + } + + // **** paths **** + for (const key of settingBlock.paths.keys) { + if (!key) { + continue; + } + + const pathEnvVariable = key.toUpperCase(); + const p = activation.paths.getOrDefault(pathEnvVariable, []); + const l = settingBlock.paths.get(key); + const uris = new Set(); + + for (const location of l ?? []) { + // check that each path is an actual path. + const path = await this.sanitizeAndValidatePath(location); + if (path && !uris.has(path)) { + uris.add(path); + p.push(path); + } + } + } + + // **** tools **** + for (const key of settingBlock.tools.keys) { + const envVariable = key.toUpperCase(); + + if (activation.tools.has(envVariable)) { + this.session.channels.error(i`Duplicate tool declared ${key} during activation. Probably not a good thing?`); + } + + const p = settingBlock.tools.get(key) || ''; + const uri = await this.sanitizeAndValidatePath(p); + if (uri) { + activation.tools.set(envVariable, uri.fsPath); + } else { + if (p) { + activation.tools.set(envVariable, p); + // this.session.channels.warning(i`Invalid tool path '${p}'`); + } + } + } + + // **** variables **** + for (const [key, value] of settingBlock.variables) { + const envKey = activation.environment.getOrDefault(key, []); + envKey.push(...value); + } + + // **** properties **** + for (const [key, value] of settingBlock.properties) { + const envKey = activation.properties.getOrDefault(key, []); + envKey.push(...value); + } + + // **** locations **** + for (const locationName of settingBlock.locations.keys) { + if (activation.locations.has(locationName)) { + this.session.channels.error(i`Duplicate location declared ${locationName} during activation. Probably not a good thing?`); + } + + const p = settingBlock.locations.get(locationName) || ''; + const uri = await this.sanitizeAndValidatePath(p); + if (uri) { + activation.locations.set(locationName, uri); + } + } + } + } + + async sanitizeAndValidatePath(path: string) { + if (!path.startsWith('.')) { + try { + const loc = this.session.fileSystem.file(resolve(path)); + if (await loc.exists()) { + return loc; + } + } catch { + // no worries, treat it like a relative path. + } + } + const loc = this.targetLocation.join(sanitizePath(path)); + if (await loc.exists()) { + return loc; + } + return undefined; + } +} + +export function sanitizePath(path: string) { + return path. + replace(/[\\/]+/g, '/'). // forward slahses please + replace(/[?<>:|"]/g, ''). // remove illegal characters. + // eslint-disable-next-line no-control-regex + replace(/[\x00-\x1f\x80-\x9f]/g, ''). // remove unicode control codes + replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, ''). // no reserved names + replace(/^[/.]*\//, ''). // dots and slashes off the front. + replace(/[/.]+$/, ''). // dots and slashes off the back. + replace(/\/\.+\//g, '/'). // no parts made just of dots. + replace(/\/+/g, '/'); // duplicate slashes. +} + +export function sanitizeUri(u: string) { + return u. + replace(/[\\/]+/g, '/'). // forward slahses please + replace(/[?<>|"]/g, ''). // remove illegal characters. + // eslint-disable-next-line no-control-regex + replace(/[\x00-\x1f\x80-\x9f]/g, ''). // remove unicode control codes + replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, ''). // no reserved names + replace(/^[/.]*\//, ''). // dots and slashes off the front. + replace(/[/.]+$/, ''). // dots and slashes off the back. + replace(/\/\.+\//g, '/'). // no parts made just of dots. + replace(/\/+/g, '/'); // duplicate slashes. +} + +export class ProjectManifest extends ArtifactBase { + +} + +export class InstalledArtifact extends Artifact { + constructor(session: Session, metadata: MetadataFile) { + super(session, metadata, '', Uri.invalid, 'OnDisk?', Uri.invalid); /* fixme ? */ + } +} \ No newline at end of file diff --git a/ce/ce/artifacts/registry.ts b/ce/ce/artifacts/registry.ts new file mode 100644 index 0000000000..bb7d0c5f74 --- /dev/null +++ b/ce/ce/artifacts/registry.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Registries } from '../registries/registries'; +import { Uri } from '../util/uri'; +import { Artifact } from './artifact'; + +export interface SearchCriteria { + idOrShortName?: string; + version?: string + keyword?: string; +} + +export interface Registry { + readonly count: number; + readonly location: Uri; + readonly loaded: boolean; + + search(parent: Registries, criteria?: SearchCriteria): Promise]>>; + + load(force?: boolean): Promise; + save(): Promise; + update(): Promise; + regenerate(): Promise; +} diff --git a/ce/ce/cli/argument.ts b/ce/ce/cli/argument.ts new file mode 100644 index 0000000000..970022fc97 --- /dev/null +++ b/ce/ce/cli/argument.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Command } from './command'; +import { Help } from './command-line'; + +export abstract class Argument implements Help { + readonly abstract argument: string; + readonly title = ''; + readonly abstract help: Array; + + constructor(protected command: Command) { + command.arguments.push(this); + } +} diff --git a/ce/ce/cli/artifacts.ts b/ce/ce/cli/artifacts.ts new file mode 100644 index 0000000000..08782b7a62 --- /dev/null +++ b/ce/ce/cli/artifacts.ts @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { MultiBar, SingleBar } from 'cli-progress'; +import { Activation } from '../artifacts/activation'; +import { Artifact, ArtifactMap } from '../artifacts/artifact'; +import { i } from '../i18n'; +import { trackAcquire } from '../insights'; +import { Registries } from '../registries/registries'; +import { Session } from '../session'; +import { artifactIdentity, artifactReference } from './format'; +import { Table } from './markdown-table'; +import { debug, error, log } from './styling'; + +export async function showArtifacts(artifacts: Iterable, options?: { force?: boolean }) { + let failing = false; + const table = new Table(i`Artifact`, i`Version`, i`Status`, i`Dependency`, i`Summary`); + for (const artifact of artifacts) { + const name = artifactIdentity(artifact.registryId, artifact.id, artifact.shortName); + if (!artifact.metadata.isValid) { + failing = true; + for (const err of artifact.metadata.validationErrors) { + error(err); + } + } + table.push(name, artifact.version, options?.force || await artifact.isInstalled ? 'installed' : 'will install', artifact.isPrimary ? ' ' : '*', artifact.metadata.info.summary || ''); + } + log(table.toString()); + + return !failing; +} + +export type Selections = Map; + +export async function selectArtifacts(selections: Selections, registries: Registries): Promise { + const artifacts = new ArtifactMap(); + + for (const [identity, version] of selections) { + const [registry, id, artifact] = await registries.getArtifact(identity, version) || []; + + if (!artifact) { + error(`Unable to resolve artifact: ${artifactReference('', identity, version)}`); + return false; + } + + artifacts.set(artifact.uniqueId, [artifact, identity, version]); + artifact.isPrimary = true; + await artifact.resolveDependencies(artifacts); + } + return artifacts; +} + +export async function installArtifacts(session: Session, artifacts: Iterable, options?: { force?: boolean, allLanguages?: boolean, language?: string }): Promise<[boolean, Map, Activation]> { + // resolve the full set of artifacts to install. + const installed = new Map(); + const activation = new Activation(session); + + const bar = new MultiBar({ + clearOnComplete: true, hideCursor: true, format: '{name} {bar}\u25A0 {percentage}% {action} {current}', + barCompleteChar: '\u25A0', + barIncompleteChar: ' ', + etaBuffer: 40 + }); + let dl: SingleBar | undefined; + let p: SingleBar | undefined; + let spinnerValue = 0; + + for (const artifact of artifacts) { + const id = artifact.id; + const registryName = artifact.registryId; + + try { + const actuallyInstalled = await artifact.install(activation, { + verifying: (name, percent) => { + if (percent >= 100) { + p?.update(percent); + p = undefined; + return; + } + if (percent) { + if (!p) { + p = bar.create(100, 0, { action: i`verifying`, name: artifactIdentity(registryName, id), current: name }); + } + p?.update(percent); + } + }, + download: (name, percent) => { + if (percent >= 100) { + if (dl) { + dl.update(percent); + } + dl = undefined; + return; + } + if (percent) { + if (!dl) { + dl = bar.create(100, 0, { action: i`downloading`, name: artifactIdentity(registryName, id), current: name }); + } + dl.update(percent); + } + }, + fileProgress: (entry) => { + p?.update({ action: i`unpacking`, name: artifactIdentity(registryName, id), current: entry.extractPath }); + }, + progress: (percent: number) => { + if (percent >= 100) { + if (p) { + p.update(percent, { action: i`unpacked`, name: artifactIdentity(registryName, id), current: '' }); + } + p = undefined; + return; + } + if (percent) { + if (!p) { + p = bar.create(100, 0, { action: i`unpacking`, name: artifactIdentity(registryName, id), current: '' }); + } + p.update(percent); + } + }, + heartbeat: (text: string) => { + if (!p) { + p = bar.create(3, 0, { action: i`working`, name: artifactIdentity(registryName, id), current: '' }); + } + p?.update((spinnerValue++) % 4, { action: i`working`, name: artifactIdentity(registryName, id), current: text }); + } + }, options || {}); + // remember what was actually installed + installed.set(artifact, actuallyInstalled); + if (actuallyInstalled) { + trackAcquire(artifact.id, artifact.version); + } + } catch (e: any) { + bar.stop(); + debug(e); + debug(e.stack); + error(i`Error installing ${artifactIdentity(registryName, id)} - ${e} `); + return [false, installed, activation]; + } + + bar.stop(); + } + return [true, installed, activation]; +} + +export async function activateArtifacts(session: Session, artifacts: Iterable) { + const activation = new Activation(session); + for (const artifact of artifacts) { + if (await artifact.isInstalled) { + await artifact.loadActivationSettings(activation); + } + } + return activation; +} diff --git a/ce/ce/cli/command-line.ts b/ce/ce/cli/command-line.ts new file mode 100644 index 0000000000..48675af6b3 --- /dev/null +++ b/ce/ce/cli/command-line.ts @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { tmpdir } from 'os'; +import { join, resolve } from 'path'; +import { i } from '../i18n'; +import { intersect } from '../util/intersect'; +import { Command } from './command'; +import { cmdSwitch } from './format'; + +export type switches = { + [key: string]: Array; +} + +export interface Help { + readonly help: Array; + readonly title: string; +} + +class Ctx { + constructor(cmdline: CommandLine) { + this.os = + cmdline.isSet('windows') ? 'win32' : + cmdline.isSet('osx') ? 'darwin' : + cmdline.isSet('linux') ? 'linux' : + cmdline.isSet('freebsd') ? 'freebsd' : + process.platform; + this.arch = cmdline.isSet('x64') ? 'x64' : + cmdline.isSet('x86') ? 'x32' : + cmdline.isSet('arm') ? 'arm' : + cmdline.isSet('arm64') ? 'arm64' : + process.arch; + } + + readonly os: string; + readonly arch: string; + + get windows(): boolean { + return this.os === 'win32'; + } + + get linux(): boolean { + return this.os === 'linux'; + } + + get freebsd(): boolean { + return this.os === 'freebsd'; + } + + get osx(): boolean { + return this.os === 'darwin'; + } + + get x64(): boolean { + return this.arch === 'x64'; + } + + get x86(): boolean { + return this.arch === 'x32'; + } + + get arm(): boolean { + return this.arch === 'arm'; + } + + get arm64(): boolean { + return this.arch === 'arm64'; + } +} + +export function resolvePath(v: string | undefined) { + return v?.startsWith('.') ? resolve(v) : v; +} + +export class CommandLine { + readonly commands = new Array(); + readonly inputs = new Array(); + readonly switches: switches = {}; + readonly context: Ctx & switches; + + #home?: string; + get homeFolder() { + // home folder is determined by + // command line (--vcpkg_root, --vcpkg-root ) + // environment (VCPKG_ROOT) + // default 1 $HOME/.vcpkg + // default 2 /.vcpkg + + // note, this does not create the folder, that would happen when the session is initialized. + + return this.#home || (this.#home = resolvePath( + this.switches['vcpkg-root']?.[0] || + this.switches['vcpkg_root']?.[0] || + process.env['VCPKG_ROOT'] || + join(process.env['HOME'] || process.env['USERPROFILE'] || tmpdir(), '.vcpkg'))); + } + + get force() { + return !!this.switches['force']; + } + + get debug() { + return !!this.switches['debug']; + } + + get fromVCPKG() { + return !!this.switches['from-vcpkg']; + } + + get language() { + const l = this.switches['language'] || []; + strict.ok((l?.length || 0) < 2, i`Expected a single value for ${cmdSwitch('language')} - found multiple`); + return l[0] || Intl.DateTimeFormat().resolvedOptions().locale; + } + + get allLanguages(): boolean { + const l = this.switches['all-languages'] || []; + strict.ok((l?.length || 0) < 2, i`Expected a single value for ${cmdSwitch('all-languages')} - found multiple`); + return !!l[0]; + } + + isSet(sw: string) { + const s = this.switches[sw]; + if (s && s.last !== 'false') { + return true; + } + return false; + } + + claim(sw: string) { + const v = this.switches[sw]; + delete this.switches[sw]; + return v; + } + + addCommand(command: Command) { + this.commands.push(command); + } + + /** parses the command line and returns the command that has been requested */ + get command() { + return this.commands.find(cmd => cmd.command === this.inputs[0] || !!cmd.aliases.find(alias => alias === this.inputs[0])); + } + + constructor(args: Array) { + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + // eslint-disable-next-line prefer-const + let [, name, sep, value] = /^--([^=:]+)([=:])?(.+)?$/g.exec(arg) || []; + if (name) { + if (!value) { + if (i + 1 < args.length && !args[i + 1].startsWith('--')) { + // if you say --foo bar then bar is the value + value = args[++i]; + } + } + this.switches[name] = this.switches[name] === undefined ? [] : this.switches[name]; + this.switches[name].push(value); + continue; + } + this.inputs.push(arg); + } + + this.context = intersect(new Ctx(this), this.switches); + } +} diff --git a/ce/ce/cli/command.ts b/ce/ce/cli/command.ts new file mode 100644 index 0000000000..80b01505f4 --- /dev/null +++ b/ce/ce/cli/command.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../i18n'; +import { Argument } from './argument'; +import { CommandLine, Help } from './command-line'; +import { blank, cli } from './constants'; +import { cmdSwitch, command, heading, optional } from './format'; +import { Switch } from './switch'; +import { Debug } from './switches/debug'; +import { Force } from './switches/force'; + +/** @internal */ + +export abstract class Command implements Help { + readonly abstract command: string; + readonly abstract argumentsHelp: Array; + + readonly switches = new Array(); + readonly arguments = new Array(); + + readonly abstract seeAlso: Array; + readonly abstract aliases: Array; + + abstract get summary(): string; + abstract get description(): Array; + + readonly force = new Force(this); + readonly debug = new Debug(this); + + get synopsis(): Array { + return [ + heading(i`Synopsis`, 2), + ` ${command(`${cli} ${this.command} ${this.arguments.map(each => `<${each.argument}>`).join(' ')}`)}${this.switches.flatMap(each => optional(`[--${each.switch}]`)).join(' ')}`, + ]; + } + + get title() { + return `${cli} ${this.command}`; + } + + constructor(public commandLine: CommandLine) { + commandLine.addCommand(this); + } + + get inputs() { + return this.commandLine.inputs.slice(1); + } + + get help() { + return [ + heading(this.title), + blank, + this.summary, + blank, + ...this.synopsis, + blank, + heading(i`Description`, 2), + blank, + ...this.description, + ...this.argumentsHelp, + ...(this.switches.length ? [ + blank, + heading(i`Switches`, 2), + blank, + ...this.switches.flatMap(each => ` ${cmdSwitch(each.switch)}: ${each.help.join(' ')}`) + ] : []), + ...(this.seeAlso.length ? [ + heading(i`See Also`, 2), + ...this.seeAlso.flatMap(each => each.title) + ] : []), + ]; + } + + async run() { + // do something + return true; + } + +} diff --git a/ce/ce/cli/commands/acquire.ts b/ce/ce/cli/commands/acquire.ts new file mode 100644 index 0000000000..b0fdbfb30c --- /dev/null +++ b/ce/ce/cli/commands/acquire.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { countWhere } from '../../util/linq'; +import { installArtifacts, selectArtifacts, showArtifacts } from '../artifacts'; +import { Command } from '../command'; +import { blank } from '../constants'; +import { cmdSwitch } from '../format'; +import { debug, error, log, warning } from '../styling'; +import { Registry } from '../switches/registry'; +import { Version } from '../switches/version'; +import { WhatIf } from '../switches/whatIf'; + +export class AcquireCommand extends Command { + readonly command = 'acquire'; + readonly aliases = ['install']; + seeAlso = []; + argumentsHelp = []; + version = new Version(this); + whatIf = new WhatIf(this); + registrySwitch = new Registry(this); + + get summary() { + return i`Acquire artifacts in the registry`; + } + + get description() { + return [ + i`This allows the consumer to acquire (download and unpack) artifacts. Artifacts must be activated to be used`, + ]; + } + + override async run() { + if (this.inputs.length === 0) { + error(i`No artifacts specified`); + return false; + } + + const registries = await this.registrySwitch.loadRegistries(session); + + const versions = this.version.values; + if (versions.length && this.inputs.length !== versions.length) { + error(i`Multiple packages specified, but not an equal number of ${cmdSwitch('version')} switches.`); + return false; + } + + const artifacts = await selectArtifacts(new Map(this.inputs.map((v, i) => [v, versions[i] || '*'])), registries); + + if (!artifacts) { + debug('No artifacts selected - stopping'); + return false; + } + + if (!await showArtifacts(artifacts.artifacts, this.commandLine)) { + warning(i`No artifacts are acquired`); + return false; + } + + const numberOfArtifacts = await countWhere(artifacts.artifacts, async (artifact) => !(!this.commandLine.force && await artifact.isInstalled)); + + if (!numberOfArtifacts) { + log(blank); + log(i`All artifacts are already installed`); + return true; + } + + debug(`Installing ${numberOfArtifacts} artifacts`); + + const [success] = await installArtifacts(session, artifacts.artifacts, { force: this.commandLine.force, language: this.commandLine.language, allLanguages: this.commandLine.allLanguages }); + + if (success) { + log(blank); + log(i`${numberOfArtifacts} artifacts installed successfuly`); + return true; + } + + log(blank); + log(i`Installation failed -- stopping`); + + return false; + } +} diff --git a/ce/ce/cli/commands/activate.ts b/ce/ce/cli/commands/activate.ts new file mode 100644 index 0000000000..df2d8f7097 --- /dev/null +++ b/ce/ce/cli/commands/activate.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Command } from '../command'; +import { projectFile } from '../format'; +import { activateProject } from '../project'; +import { debug, error } from '../styling'; +import { MSBuildProps } from '../switches/msbuild-props'; +import { Project } from '../switches/project'; +import { WhatIf } from '../switches/whatIf'; + +export class ActivateCommand extends Command { + readonly command = 'activate'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + whatIf = new WhatIf(this); + project: Project = new Project(this); + msbuildProps: MSBuildProps = new MSBuildProps(this); + + get summary() { + return i`Activates the tools required for a project`; + } + + get description() { + return [ + i`This allows the consumer to Activate the tools required for a project. If the tools are not already installed, this will force them to be downloaded and installed before activation.`, + ]; + } + + override async run() { + const projectManifest = await this.project.manifest; + + if (!projectManifest) { + error(i`Unable to find project in folder (or parent folders) for ${session.currentDirectory.fsPath}`); + return false; + } + + debug(i`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`); + await session.deactivate(); + + return await activateProject(projectManifest, { + force: this.commandLine.force, + allLanguages: this.commandLine.allLanguages, + language: this.commandLine.language, + msbuildProps: await this.msbuildProps.value + }); + } +} diff --git a/ce/ce/cli/commands/add.ts b/ce/ce/cli/commands/add.ts new file mode 100644 index 0000000000..986c12d706 --- /dev/null +++ b/ce/ce/cli/commands/add.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { selectArtifacts } from '../artifacts'; +import { Command } from '../command'; +import { cmdSwitch, projectFile } from '../format'; +import { activateProject } from '../project'; +import { debug, error } from '../styling'; +import { Project } from '../switches/project'; +import { Registry } from '../switches/registry'; +import { Version } from '../switches/version'; +import { WhatIf } from '../switches/whatIf'; + +export class AddCommand extends Command { + readonly command = 'add'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + + version = new Version(this); + project: Project = new Project(this); + whatIf = new WhatIf(this); + registrySwitch = new Registry(this); + + + get summary() { + return i`Adds an artifact to the project`; + } + + get description() { + return [ + i`This allows the consumer to add an artifact to the project. This will activate the project as well.`, + ]; + } + + override async run() { + const projectManifest = await this.project.manifest; + + if (!projectManifest) { + error(i`Unable to find project in folder (or parent folders) for ${session.currentDirectory.fsPath}`); + return false; + } + + // pull in any registries that are on the command line + await this.registrySwitch.loadRegistries(session); + + if (this.inputs.length === 0) { + error(i`No artifacts specified`); + return false; + } + + const versions = this.version.values; + if (versions.length && this.inputs.length !== versions.length) { + error(i`Multiple artifacts specified, but not an equal number of ${cmdSwitch('version')} switches`); + return false; + } + + const selections = new Map(this.inputs.map((v, i) => [v, versions[i] || '*'])); + const selectedArtifacts = await selectArtifacts(selections, projectManifest.registries); + + if (!selectedArtifacts) { + return false; + } + + for (const [artifact, id, requested] of selectedArtifacts.values()) { + // make sure the registry is in the project + const registry = projectManifest.registries.getRegistry(artifact.registryUri); + if (!registry) { + const r = projectManifest.metadata.registries.add(artifact.registryId, artifact.registryUri, 'artifact'); + + } + + + // add the artifact to the project + const fulfilled = artifact.version.toString(); + const v = requested !== fulfilled ? `${requested} ${fulfilled}` : fulfilled; + projectManifest.metadata.requires.set(artifact.reference, v); + } + + // write the file out. + await projectManifest.metadata.save(); + + debug(i`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`); + await session.deactivate(); + + return await activateProject(projectManifest, this.commandLine); + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/apply-vsman.ts b/ce/ce/cli/commands/apply-vsman.ts new file mode 100644 index 0000000000..d544d208e2 --- /dev/null +++ b/ce/ce/cli/commands/apply-vsman.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { MetadataFile } from '../../amf/metadata-file'; +import { acquireArtifactFile } from '../../fs/acquire'; +import { FileType } from '../../fs/filesystem'; +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Session } from '../../session'; +import { Uri } from '../../util/uri'; +import { templateAmfApplyVsManifestInformation } from '../../willow/template-amf'; +import { parseVsManFromChannel, VsManDatabase } from '../../willow/willow'; +import { Command } from '../command'; +import { log } from '../styling'; +import { Switch } from '../switch'; + +class ChannelUri extends Switch { + readonly switch = 'channel'; + + get help() { + return [ + i`The URI to the Visual Studio channel to apply.` + ]; + } +} + +class RepoRoot extends Switch { + readonly switch = 'repo'; + + get help() { + return [ + i`The directory path to the root of the repo into which artifact metadata is to be generated.` + ]; + } +} + +export class ApplyVsManCommand extends Command { + readonly command = 'z-apply-vsman'; + readonly seeAlso = []; + readonly argumentsHelp = []; + readonly aliases = []; + + readonly channelUri = new ChannelUri(this); + readonly repoRoot = new RepoRoot(this); + + get summary() { + return i`Apply Visual Studio Channel (.vsman) information to a prototypical artifact metadata.`; + } + + get description() { + return [ + i`This is used to mint artifacts metadata exactly corresponding to a release state in a Visual Studio channel.`, + ]; + } + + /** + * Process an input file. + */ + static async processFile(session: Session, inputUri: Uri, repoRoot: Uri, vsManLookup: VsManDatabase) { + const inputPath = inputUri.fsPath; + session.channels.debug(i`Processing ${inputPath}...`); + const inputContent = await inputUri.readUTF8(); + const outputContent = templateAmfApplyVsManifestInformation(session, inputPath, inputContent, vsManLookup); + if (!outputContent) { + session.channels.warning(i`Skipped processing ${inputPath}`); + return 0; + } + + const outputAmf = await MetadataFile.parseConfiguration(inputPath, outputContent, session); + if (!outputAmf.isValid) { + const errors = outputAmf.validationErrors.join('\n'); + session.channels.warning(i`After transformation, ${inputPath} did not result in a valid AMF; skipping:\n${outputContent}\n${errors}`); + return 0; + } + + const outputId = outputAmf.info.id; + const outputIdLast = outputId.slice(outputId.lastIndexOf('/')); + const outputVersion = outputAmf.info.version; + const outputRelativePath = `${outputId}/${outputIdLast}-${outputVersion}.yaml`; + const outputFullPath = repoRoot.join(outputRelativePath); + let doWrite = true; + try { + const outputExistingContent = await outputFullPath.readUTF8(); + if (outputExistingContent === outputContent) { + doWrite = false; + } else { + session.channels.warning(i`After transformation, ${inputPath} results in ${outputFullPath.toString()} which already exists; overwriting.`); + } + } catch { + // nothing to do + } + + if (doWrite) { + await outputFullPath.writeUTF8(outputContent); + } + + session.channels.debug(i`-> ${outputFullPath.toString()}`); + return 1; + } + + /** + * Process an input file or directory, recursively. + */ + static async processInput(session: Session, inputDirectoryEntry: [Uri, FileType], repoRoot: Uri, vsManLookup: VsManDatabase): Promise { + if ((inputDirectoryEntry[1] & FileType.Directory) !== 0) { + let total = 0; + for (const child of await inputDirectoryEntry[0].readDirectory()) { + total += await ApplyVsManCommand.processInput(session, child, repoRoot, vsManLookup); + } + + return total; + } else if ((inputDirectoryEntry[1] & FileType.File) !== 0) { + return await ApplyVsManCommand.processFile(session, inputDirectoryEntry[0], repoRoot, vsManLookup); + } + + return 0; + } + + override async run() { + const channelUriStr = this.channelUri.requiredValue; + const repoRoot = session.fileSystem.file(this.repoRoot.requiredValue); + log(i`Downloading channel manifest from ${channelUriStr}`); + const channelUriUri = session.parseUri(channelUriStr); + const channelFile = await acquireArtifactFile(session, [channelUriUri], 'channel.chman', {}); + const vsManPayload = parseVsManFromChannel(await channelFile.readUTF8()); + log(i`Downloading Visual Studio manifest version ${vsManPayload.version} (${vsManPayload.url})`); + const vsManUri = await acquireArtifactFile(session, [session.parseUri(vsManPayload.url)], vsManPayload.fileName, {}); + const vsManLookup = new VsManDatabase(await vsManUri.readUTF8()); + let totalProcessed = 0; + for (const inputPath of this.inputs) { + const inputUri = session.fileSystem.file(inputPath); + const inputStat = await inputUri.stat(); + totalProcessed += await ApplyVsManCommand.processInput(session, [inputUri, inputStat.type], repoRoot, vsManLookup); + } + + session.channels.message(i`Processed ${totalProcessed} templates.`); + return true; + } +} diff --git a/ce/ce/cli/commands/cache.ts b/ce/ce/cli/commands/cache.ts new file mode 100644 index 0000000000..980d937293 --- /dev/null +++ b/ce/ce/cli/commands/cache.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { basename } from 'path'; +import { FileType } from '../../fs/filesystem'; +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Uri } from '../../util/uri'; +import { Command } from '../command'; +import { Table } from '../markdown-table'; +import { log } from '../styling'; +import { Clear } from '../switches/clear'; +import { WhatIf } from '../switches/whatIf'; + +export class CacheCommand extends Command { + readonly command = 'cache'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + clear = new Clear(this); + whatIf = new WhatIf(this); + + get summary() { + return i`Manages the download cache`; + } + + get description() { + return [ + i`Manages the download cache.`, + ]; + } + + override async run() { + if (this.clear.active) { + await session.cache.delete({ recursive: true }); + await session.cache.createDirectory(); + log(i`Cache folder cleared (${session.cache.fsPath}) `); + return true; + } + let files: Array<[Uri, FileType]> = []; + try { + files = await session.cache.readDirectory(); + } catch { + // shh + } + + if (!files.length) { + log('The download cache is empty'); + return true; + } + + const table = new Table('File', 'Size', 'Date'); + for (const [file, type] of files) { + const stat = await file.stat(); + table.push(basename(file.fsPath), stat.size.toString(), new Date(stat.mtime).toString()); + } + log(table.toString()); + log(); + + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/clean.ts b/ce/ce/cli/commands/clean.ts new file mode 100644 index 0000000000..b8360121af --- /dev/null +++ b/ce/ce/cli/commands/clean.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Command } from '../command'; +import { debug, log } from '../styling'; +import { Switch } from '../switch'; +import { WhatIf } from '../switches/whatIf'; + +export class All extends Switch { + switch = 'all'; + get help() { + return [ + i`cleans out everything (cache, installed artifacts)` + ]; + } +} + +export class Cache extends Switch { + switch = 'cache'; + get help() { + return [ + i`cleans out the cache` + ]; + } +} + +export class Artifacts extends Switch { + switch = 'artifacts'; + get help() { + return [ + i`removes all the artifacts that are installed` + ]; + } +} + +export class CleanCommand extends Command { + readonly command = 'clean'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + all = new All(this); + artifacts = new Artifacts(this); + cache = new Cache(this); + whatIf = new WhatIf(this); + + get summary() { + return i`cleans up`; + } + + get description() { + return [ + i`Allows the user to clean out the cache, installed artifacts, etc.`, + ]; + } + + override async run() { + + if (this.all.active || this.artifacts.active) { + // if we're removing artifacts + debug(i`Deactivating project`); + await session.deactivate(); + + await session.installFolder.delete({ recursive: true }); + await session.installFolder.createDirectory(); + log(i`Installed Artifact folder cleared (${session.installFolder.fsPath}) `); + } + + if (this.all.active || this.cache.active) { + await session.cache.delete({ recursive: true }); + await session.cache.createDirectory(); + log(i`Cache folder cleared (${session.cache.fsPath}) `); + } + + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/deactivate.ts b/ce/ce/cli/commands/deactivate.ts new file mode 100644 index 0000000000..d6fc8e94c5 --- /dev/null +++ b/ce/ce/cli/commands/deactivate.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Command } from '../command'; +import { projectFile } from '../format'; +import { log } from '../styling'; +import { Project } from '../switches/project'; +import { WhatIf } from '../switches/whatIf'; + +export class DeactivateCommand extends Command { + readonly command = 'deactivate'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + project = new Project(this); + whatIf = new WhatIf(this); + + get summary() { + return i`Deactivates the current session`; + } + + get description() { + return [ + i`This allows the consumer to remove environment settings for the currently active session.`, + ]; + } + + override async run() { + const project = await this.project.value; + if (!project) { + return false; + } + + log(i`Deactivating project ${projectFile(project)}`); + await session.deactivate(); + + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/delete.ts b/ce/ce/cli/commands/delete.ts new file mode 100644 index 0000000000..68c23e2cfb --- /dev/null +++ b/ce/ce/cli/commands/delete.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Command } from '../command'; +import { Version } from '../switches/version'; +import { WhatIf } from '../switches/whatIf'; + +export class DeleteCommand extends Command { + readonly command = 'delete'; + readonly aliases = ['uninstall']; + seeAlso = []; + argumentsHelp = []; + version = new Version(this); + whatIf = new WhatIf(this); + + get summary() { + return i`Deletes an artifact from the artifact folder`; + } + + get description() { + return [ + i`This allows the consumer to remove an artifact from disk.`, + ]; + } + + override async run() { + const artifacts = await session.getInstalledArtifacts(); + for (const input of this.inputs) { + for (const { artifact, id, folder } of artifacts) { + if (input === id) { + if (await folder.exists()) { + session.channels.message(i`Deleting artifact ${id} from ${folder.fsPath}`); + await artifact.uninstall(); + } + } + } + } + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/find.ts b/ce/ce/cli/commands/find.ts new file mode 100644 index 0000000000..5f1b42f73e --- /dev/null +++ b/ce/ce/cli/commands/find.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Registries } from '../../registries/registries'; +import { Command } from '../command'; +import { artifactIdentity } from '../format'; +import { Table } from '../markdown-table'; +import { debug, log } from '../styling'; +import { Project } from '../switches/project'; +import { Registry } from '../switches/registry'; +import { Version } from '../switches/version'; + +export class FindCommand extends Command { + readonly command = 'find'; + readonly aliases = ['search']; + seeAlso = []; + argumentsHelp = []; + + version = new Version(this); + registrySwitch = new Registry(this); + project = new Project(this); + + get summary() { + return i`Find artifacts in the registry`; + } + + get description() { + return [ + i`This allows the user to find artifacts based on some criteria.`, + ]; + } + + override async run() { + // load registries (from the current project too if available) + let registries: Registries = await this.registrySwitch.loadRegistries(session); + registries = (await this.project.manifest)?.registries ?? registries; + + debug(`using registries: ${[...registries].map(([registry, registryNames]) => registryNames[0]).join(', ')}`); + const table = new Table('Artifact', 'Version', 'Summary'); + + for (const each of this.inputs) { + for (const [registry, id, artifacts] of await registries.search({ idOrShortName: each, version: this.version.value })) { + const latest = artifacts[0]; + if (!latest.metadata.info.dependencyOnly) { + const name = artifactIdentity(latest.registryId, id, latest.shortName); + table.push(name, latest.metadata.info.version, latest.metadata.info.summary || ''); + } + } + } + + log(table.toString()); + log(); + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/help.ts b/ce/ce/cli/commands/help.ts new file mode 100644 index 0000000000..3380a8a13e --- /dev/null +++ b/ce/ce/cli/commands/help.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { Argument } from '../argument'; +import { Command } from '../command'; +import { blank, cli } from '../constants'; +import { command as formatCommand, heading, hint } from '../format'; +import { error, indent, log } from '../styling'; + +class CommandName extends Argument { + argument = 'command'; + + get help() { + return [ + i`the name of the command for which you want help` + ]; + } +} + +/**@internal */ +export class HelpCommand extends Command { + readonly command = 'help'; + readonly aliases = []; + seeAlso = []; + commandName: CommandName = new CommandName(this); + + get argumentsHelp() { + return [indent(i` <${this.commandName.argument}> : ${this.commandName.help.join(' ')}`)]; + } + + get summary() { + return i`get help on ${cli} or one of the commands`; + } + + get description() { + return [ + i`Gets detailed help on ${cli}, or one of the commands`, + blank, + i`Arguments:` + ]; + } + + override async run() { + const cmd = ['-h', '-help', '-?', '/?'].find(each => (this.commandLine.inputs.indexOf(each) > -1)) ? this.commandLine.inputs[0] : this.commandLine.inputs[1]; + // did they ask for help on a command? + + if (cmd) { + const target = this.commandLine.commands.find(each => each.command === cmd); + if (target) { + log(target.help.join('\n')); + log(blank); + return true; + } + + // I don't know the command + error(i`Unrecognized command '${cmd}'`); + log(hint(i`Use ${formatCommand(`${cli} ${this.command}`)} to get the list of available commands`)); + return false; + } + + // general help. return the general help info + + log(heading(i`Usage`, 2)); + log(blank); + log(indent(i`${cli} COMMAND [--switches]`)); + log(blank); + + log(heading(i`Available ${cli} commands:`, 2)); + log(blank); + const max = Math.max(...this.commandLine.commands.map(each => each.command.length)); + for (const command of this.commandLine.commands) { + if (command.command.startsWith('z-')) { + // don't show internal commands + continue; + } + log(indent(i`${formatCommand(command.command.padEnd(max))} : ${command.summary}`)); + } + + log(blank); + + return true; + } +} diff --git a/ce/ce/cli/commands/list.ts b/ce/ce/cli/commands/list.ts new file mode 100644 index 0000000000..0838b06dd6 --- /dev/null +++ b/ce/ce/cli/commands/list.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Command } from '../command'; +import { artifactIdentity } from '../format'; +import { Table } from '../markdown-table'; +import { log } from '../styling'; +import { Installed } from '../switches/installed'; + +export class ListCommand extends Command { + readonly command = 'list'; + readonly aliases = ['show']; + seeAlso = []; + argumentsHelp = []; + installed = new Installed(this); + + get summary() { + return i`Lists the artifacts`; + } + + get description() { + return [ + i`This allows the consumer to list artifacts.`, + ]; + } + + override async run() { + if (this.installed.active) { + const artifacts = await session.getInstalledArtifacts(); + const table = new Table('Artifact', 'Version', 'Summary'); + + for (const { artifact, id, folder } of artifacts) { + + const name = artifactIdentity('', id); //todo: fixme + table.push(name, artifact.version, artifact.metadata.info.summary || ''); + } + log(table.toString()); + log(); + } + else { + log('use --installed for now'); + } + + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/new.ts b/ce/ce/cli/commands/new.ts new file mode 100644 index 0000000000..21c6857e25 --- /dev/null +++ b/ce/ce/cli/commands/new.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { MetadataFile } from '../../amf/metadata-file'; +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Command } from '../command'; +import { project } from '../constants'; +import { log } from '../styling'; +import { WhatIf } from '../switches/whatIf'; + +export class NewCommand extends Command { + readonly command = 'new'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + whatIf = new WhatIf(this); + + get summary() { + return i`Creates a new project file`; + } + + get description() { + return [ + i`This allows the consumer create a new project file ('${project}').`, + ]; + } + + override async run() { + if (await session.currentDirectory.exists(project)) { + log(i`The folder at ${session.currentDirectory.fsPath} already contains a project file '${project}'`); + return false; + } + const prjFile = session.currentDirectory.join(project); + + await (await MetadataFile.parseConfiguration(prjFile.toString(), '# Environment configuration\n', session)).save(session.currentDirectory.join(project)); + + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/regenerate-index.ts b/ce/ce/cli/commands/regenerate-index.ts new file mode 100644 index 0000000000..3afeb8399c --- /dev/null +++ b/ce/ce/cli/commands/regenerate-index.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Registry } from '../../artifacts/registry'; +import { registryIndexFile } from '../../constants'; +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Registries } from '../../registries/registries'; +import { Uri } from '../../util/uri'; +import { Command } from '../command'; +import { cli } from '../constants'; +import { error, log, writeException } from '../styling'; +import { Project } from '../switches/project'; +import { Registry as RegSwitch } from '../switches/registry'; +import { WhatIf } from '../switches/whatIf'; + +export class RegenerateCommand extends Command { + readonly command = 'regenerate'; + project = new Project(this); + readonly aliases = ['regen']; + readonly regSwitch = new RegSwitch(this, { required: true }); + seeAlso = []; + argumentsHelp = []; + + whatIf = new WhatIf(this); + get summary() { + return i`regenerate the index for a registry`; + } + + get description() { + return [ + i`This allows the user to regenerate the ${registryIndexFile} files for a ${cli} registry.`, + ]; + } + + override async run() { + let registries: Registries = await this.regSwitch.loadRegistries(session); + registries = (await this.project.manifest)?.registries ?? registries; + + for (const registryNameOrLocation of this.inputs) { + let registry: Registry | undefined; + try { + if (registries.has(registryNameOrLocation)) { + // check for named registries first. + registry = registries.getRegistry(registryNameOrLocation); + await registry?.load(); + } else { + // see if the name is a location + const location = await session.parseLocation(registryNameOrLocation); + registry = location ? + session.loadRegistry(location, 'artifact') : // a folder + registries.getRegistry(registryNameOrLocation); // a registry name or other location. + } + if (registry) { + if (Uri.isInvalid(registry.location)) { + error(i`Registry: '${registryNameOrLocation}' does not have an index to regenerate.`); + return false; + } + log(i`Regenerating index for ${registry.location.formatted}`); + await registry.regenerate(); + if (registry.count) { + await registry.save(); + log(i`Regeneration complete. Index contains ${registry.count} metadata files`); + continue; + } + // looks like the registry contained no items + error(i`Registry: '${registry.location.formatted}' contains no artifacts.`); + continue; + } + + error(i`Unrecognized registry: ${registryNameOrLocation}`); + return false; + + } catch (e) { + log(i`Regeneration failed for ${registryNameOrLocation.toString()}`); + writeException(e); + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/remove.ts b/ce/ce/cli/commands/remove.ts new file mode 100644 index 0000000000..328f41b2c9 --- /dev/null +++ b/ce/ce/cli/commands/remove.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Command } from '../command'; +import { projectFile } from '../format'; +import { activateProject } from '../project'; +import { debug, error, log } from '../styling'; +import { Project } from '../switches/project'; +import { WhatIf } from '../switches/whatIf'; + +export class RemoveCommand extends Command { + readonly command = 'remove'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + whatIf = new WhatIf(this); + project: Project = new Project(this); + + get summary() { + return i`Removes an artifact from a project`; + } + + get description() { + return [ + i`This allows the consumer to remove an artifact from the project. Forces reactivation in this window.`, + ]; + } + + override async run() { + const projectManifest = await this.project.manifest; + + if (!projectManifest) { + error(i`Unable to find project in folder (or parent folders) for ${session.currentDirectory.fsPath}`); + return false; + } + + if (this.inputs.length === 0) { + error(i`No artifacts specified`); + return false; + } + + + const req = projectManifest.metadata.requires.keys; + for (const input of this.inputs) { + if (req.indexOf(input) !== -1) { + projectManifest.metadata.requires.delete(input); + log(i`Removing ${input} from project manifest`); + } else { + error(i`unable to find artifact ${input} in the project manifest`); + return false; + } + } + + // write the file out. + await projectManifest.metadata.save(); + + debug(`Deactivating project ${projectFile(projectManifest.metadata.context.file)}`); + await session.deactivate(); + + return await activateProject(projectManifest, this.commandLine); + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/update.ts b/ce/ce/cli/commands/update.ts new file mode 100644 index 0000000000..8ce7f4ba34 --- /dev/null +++ b/ce/ce/cli/commands/update.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +import { Registry } from '../../artifacts/registry'; +import { i } from '../../i18n'; +import { session } from '../../main'; +import { RemoteFileUnavailable } from '../../util/exceptions'; +import { Command } from '../command'; +import { CommandLine } from '../command-line'; +import { count } from '../format'; +import { error, log, writeException } from '../styling'; +import { Registry as RegSwitch } from '../switches/registry'; +import { WhatIf } from '../switches/whatIf'; + +export class UpdateCommand extends Command { + readonly command = 'update'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + whatIf = new WhatIf(this); + registrySwitch = new RegSwitch(this); + + get summary() { + return i`update the registry from the remote`; + } + + get description() { + return [ + i`This downloads the latest contents of the registry from the remote service.`, + ]; + } + + override async run() { + const registries = await this.registrySwitch.loadRegistries(session); + + // process named registries + for (let registryName of this.inputs) { + if (registryName.indexOf(':') !== -1) { + registryName = session.parseUri(registryName).toString(); + } + const registry = registries.getRegistryWithNameOrLocation(registryName); + if (registry) { + try { + log(i`Downloading registry data`); + await registry.update(); + await registry.load(); + log(i`Updated ${registryName}. registry contains ${count(registry.count)} metadata files`); + } catch (e) { + if (e instanceof RemoteFileUnavailable) { + log(i`Unable to download registry snapshot`); + return false; + } + writeException(e); + return false; + } + } else { + error(i`Unable to find registry ${registryName}`); + } + } + + return true; + } + + static async update(registry: Registry) { + log(i`Artifact registry data is not loaded`); + log(i`Attempting to update artifact registry`); + const update = new UpdateCommand(new CommandLine([])); + + let success = true; + try { + success = await update.run(); + } catch (e) { + writeException(e); + success = false; + } + if (!success) { + error(i`Unable to load registry index`); + return false; + } + try { + await registry.load(); + } catch (e) { + writeException(e); + // it just doesn't want to load. + error(i`Unable to load registry index`); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/use.ts b/ce/ce/cli/commands/use.ts new file mode 100644 index 0000000000..4b8bdad03b --- /dev/null +++ b/ce/ce/cli/commands/use.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Registries } from '../../registries/registries'; +import { installArtifacts, selectArtifacts, showArtifacts } from '../artifacts'; +import { Command } from '../command'; +import { cmdSwitch } from '../format'; +import { error, log, warning } from '../styling'; +import { MSBuildProps } from '../switches/msbuild-props'; +import { Project } from '../switches/project'; +import { Registry } from '../switches/registry'; +import { Version } from '../switches/version'; +import { WhatIf } from '../switches/whatIf'; + +export class UseCommand extends Command { + readonly command = 'use'; + readonly aliases = []; + seeAlso = []; + argumentsHelp = []; + version = new Version(this); + whatIf = new WhatIf(this); + registrySwitch = new Registry(this); + project = new Project(this); + msbuildProps = new MSBuildProps(this); + + get summary() { + return i`Instantly activates an artifact outside of the project`; + } + + get description() { + return [ + i`This will instantly activate an artifact .`, + ]; + } + + override async run() { + if (this.inputs.length === 0) { + error(i`No artifacts specified`); + return false; + } + + // load registries (from the current project too if available) + let registries: Registries = await this.registrySwitch.loadRegistries(session); + registries = (await this.project.manifest)?.registries ?? registries; + + const versions = this.version.values; + if (versions.length && this.inputs.length !== versions.length) { + error(i`Multiple packages specified, but not an equal number of ${cmdSwitch('version')} switches`); + return false; + } + + const selections = new Map(this.inputs.map((v, i) => [v, versions[i] || '*'])); + const artifacts = await selectArtifacts(selections, registries); + + if (!artifacts) { + return false; + } + + if (!await showArtifacts(artifacts.artifacts, this.commandLine)) { + warning(i`No artifacts are being acquired`); + return false; + } + + const [success, artifactStatus, activation] = await installArtifacts(session, artifacts.artifacts, { force: this.commandLine.force, language: this.commandLine.language, allLanguages: this.commandLine.allLanguages }); + if (success) { + log(i`Activating individual artifacts`); + await session.setActivationInPostscript(activation, false); + const propsFile = this.msbuildProps.value; + if (propsFile) { + await propsFile.writeUTF8(activation.generateMSBuild(artifactStatus.keys())); + } + } else { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/ce/ce/cli/commands/version.ts b/ce/ce/cli/commands/version.ts new file mode 100644 index 0000000000..0ad631d797 --- /dev/null +++ b/ce/ce/cli/commands/version.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { parse } from 'semver'; +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Version } from '../../version'; +import { Command } from '../command'; +import { cli, product } from '../constants'; +import { debug, error, log } from '../styling'; +import { Switch } from '../switch'; + +class Check extends Switch { + switch = 'check'; + get help() { + return [ + i`check to see if a newer version of ${cli} is available` + ]; + } +} + +class Update extends Switch { + switch = 'update'; + get help() { + return [ + i`will update the current installation of ${cli} if a newer version is available` + ]; + } +} + +export class VersionCommand extends Command { + readonly command = 'version'; + readonly aliases = ['ver']; + seeAlso = []; + argumentsHelp = []; + check = new Check(this); + update = new Update(this); + + versionUrl = session.parseUri('https://aka.ms/vcpkg-ce.version'); + + get summary() { + return i`manage the version of ${cli}`; + } + + get description() { + return [ + i`This allows the user to get the current verison information for ${cli}`, + i`as well as checking if a new version is available, and can upgrade the current installation to the latest version.`, + ]; + } + + private async getRemoteVersion() { + const version = await this.versionUrl.readUTF8(); + const semver = parse(version.trim()); + strict.ok(semver, i`Unable to parse version ${version}`); + + return semver; + } + + override async run() { + if (this.update.active) { + // check for a new version, and update if necessary + debug(i`checking to see if there is a new version of the ${cli}, and updating if there is`); + try { + const semver = await this.getRemoteVersion(); + + if (semver.compare(Version) > 0) { + // we can update the tool. + debug('An update is available, we can install it. '); + debug('(we can not do it yet, waiting for download support'); + } + + } catch (err) { + error('Failed to get latest version number'); + return false; + } + return true; + } + + if (this.check.active) { + // check for a new version + debug(i`checking to see if there is a new version of the ${cli}`); + try { + const semver = await this.getRemoteVersion(); + + if (semver.compare(Version) > 0) { + log(i`There is a new version (${semver.version}) of ${cli} available`); + } + return true; + } catch (err) { + if (err instanceof Error) { + error(i`Failed to get latest version number. (${err.message})`); + log(err.stack || ''); + } + } + return false; + } + + // dump version information + log(i`${product} version information\n`); + log(i` version: ${Version} `); + + + // Make the NOTICE and LICENSE files discoverable. NOTICE is generated during the official build. + log(i`Usage of vcpkg-ce is subject to license terms available at ${session.homeFolder.join('LICENSE.txt').fsPath}`); + log(i`Third-party license information is available at ${session.homeFolder.join('NOTICE.txt').fsPath}`); + return true; + } + +} diff --git a/ce/ce/cli/constants.ts b/ce/ce/cli/constants.ts new file mode 100644 index 0000000000..6e15e31800 --- /dev/null +++ b/ce/ce/cli/constants.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const cli = 'ce'; +export const product = 'vcpkg-ce'; +export const project = 'vcpkg-configuration.json'; +export const blank = '\n'; diff --git a/ce/ce/cli/format.ts b/ce/ce/cli/format.ts new file mode 100644 index 0000000000..720914b5a6 --- /dev/null +++ b/ce/ce/cli/format.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { bold, cyan, gray, green, greenBright, grey, underline, whiteBright, yellowBright } from 'chalk'; +import { Uri } from '../util/uri'; + +export function projectFile(uri: Uri): string { + return cyan(uri.fsPath); +} + +export function artifactIdentity(registryName: string, identity: string, alias?: string) { + if (alias) { + return `${registryName}:${identity.substr(0, identity.length - alias.length)}${yellowBright(alias)}`; + } + return yellowBright(identity); +} + +export function artifactReference(registryName: string, identity: string, version: string) { + return `${artifactIdentity(registryName, identity)}-v${gray(version)}`; +} + +export function heading(text: string, level = 1) { + switch (level) { + case 1: + return `${underline.bold(text)}\n`; + case 2: + return `${greenBright(text)}\n`; + case 3: + return `${green(text)}\n`; + } + return `${bold(text)}\n`; +} + +export function optional(text: string) { + return gray(text); +} +export function cmdSwitch(text: string) { + return optional(`--${text}`); +} + +export function command(text: string) { + return whiteBright.bold(text); +} + +export function hint(text: string) { + return green.dim(text); +} + +export function count(num: number) { + return grey(`${num}`); +} + +export function position(text: string) { + return grey(`${text}`); +} \ No newline at end of file diff --git a/ce/ce/cli/markdown-table.ts b/ce/ce/cli/markdown-table.ts new file mode 100644 index 0000000000..5359dec6bd --- /dev/null +++ b/ce/ce/cli/markdown-table.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; + +export class Table { + private readonly rows = new Array(); + private numberOfColumns = 0; + constructor(...columnNames: Array) { + this.numberOfColumns = columnNames.length; + this.rows.push(`|${columnNames.join('|')}|`); + this.rows.push(`${'|--'.repeat(this.numberOfColumns)}|`); + } + push(...values: Array) { + strict.equal(values.length, this.numberOfColumns, 'unexpected number of arguments in table row'); + this.rows.push(`|${values.join('|')}|`); + } + toString() { + return this.rows.join('\n'); + } +} \ No newline at end of file diff --git a/ce/ce/cli/project.ts b/ce/ce/cli/project.ts new file mode 100644 index 0000000000..0ad6b5bdfd --- /dev/null +++ b/ce/ce/cli/project.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ArtifactMap, ProjectManifest } from '../artifacts/artifact'; +import { i } from '../i18n'; +import { trackActivation } from '../insights'; +import { session } from '../main'; +import { Uri } from '../util/uri'; +import { installArtifacts, showArtifacts } from './artifacts'; +import { blank } from './constants'; +import { projectFile } from './format'; +import { error, log } from './styling'; + +class ActivationOptions { + force?: boolean; + allLanguages?: boolean; + language?: string; + msbuildProps?: Uri; +} + +export async function openProject(location: Uri): Promise { + // load the project + return new ProjectManifest(session, await session.openManifest(location)); +} + +export async function activate(artifacts: ArtifactMap, options?: ActivationOptions) { + // install the items in the project + const [success, artifactStatus, activation] = await installArtifacts(session, artifacts.artifacts, options); + + if (success) { + // create an MSBuild props file if indicated by the user + const propsFile = options?.msbuildProps; + if (propsFile) { + await propsFile.writeUTF8(activation.generateMSBuild(artifactStatus.keys())); + } + + // activate all the tools in the project + await session.setActivationInPostscript(activation); + } + + return success; +} + +export async function activateProject(project: ProjectManifest, options?: ActivationOptions) { + // track what got installed + const artifacts = await project.resolveDependencies(); + + // print the status of what is going to be activated. + if (!await showArtifacts(artifacts.artifacts, options)) { + error(i`Unable to activate project`); + return false; + } + + if (await activate(artifacts, options)) { + trackActivation(); + log(blank); + log(i`Project ${projectFile(project.metadata.context.folder)} activated`); + return true; + } + + log(blank); + log(i`Failed to activate project ${projectFile(project.metadata.context.folder)}`); + + return false; +} diff --git a/ce/ce/cli/styling.ts b/ce/ce/cli/styling.ts new file mode 100644 index 0000000000..8b9676b44f --- /dev/null +++ b/ce/ce/cli/styling.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { blue, cyan, gray, green, red, white, yellow } from 'chalk'; +import * as renderer from 'marked-terminal'; +import { argv } from 'process'; +import { Session } from '../session'; +import { CommandLine } from './command-line'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const marked = require('marked'); + +function formatTime(t: number) { + return ( + t < 3600000 ? [Math.floor(t / 60000) % 60, Math.floor(t / 1000) % 60, t % 1000] : + t < 86400000 ? [Math.floor(t / 3600000) % 24, Math.floor(t / 60000) % 60, Math.floor(t / 1000) % 60, t % 1000] : + [Math.floor(t / 86400000), Math.floor(t / 3600000) % 24, Math.floor(t / 60000) % 60, Math.floor(t / 1000) % 60, t % 1000]).map(each => each.toString().padStart(2, '0')).join(':').replace(/(.*):(\d)/, '$1.$2'); +} + +// setup markdown renderer +marked.setOptions({ + renderer: new renderer({ + tab: 2, + emoji: true, + showSectionPrefix: false, + firstHeading: green.underline.bold, + heading: green.underline, + codespan: white.bold, + link: blue.bold, + href: blue.bold.underline, + code: gray, + tableOptions: { + chars: { + 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '' + , 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '' + , 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '' + , 'right': '', 'right-mid': '', 'middle': '' + } + } + }), + gfm: true, +}); + +export function indent(text: string): string +export function indent(text: Array): Array +export function indent(text: string | Array): string | Array { + if (Array.isArray(text)) { + return text.map(each => indent(each)); + } + return ` ${text}`; +} + +function md(text = '', session?: Session): string { + if (text) { + text = marked.marked(`${text}`.replace(/\\\./g, '\\\\.')); // work around md messing up paths with .\ in them. + + // rewrite file:// urls to be locl filesystem urls. + return (!!text && !!session) ? text.replace(/(file:\/\/\S*)/g, (s, a) => yellow.dim(session.parseUri(a).fsPath)) : text; + } + return ''; +} + +const stdout = console['log']; + +export let log: (message?: any, ...optionalParams: Array) => void = stdout; +export let error: (message?: any, ...optionalParams: Array) => void = console.error; +export let warning: (message?: any, ...optionalParams: Array) => void = console.error; +export let debug: (message?: any, ...optionalParams: Array) => void = (text) => { + if (argv.any(arg => arg === '--debug')) { + stdout(`${cyan.bold('debug: ')}${text}`); + } +}; + +export function writeException(e: any) { + if (e instanceof Error) { + debug(e.message); + debug(e.stack); + return; + } + debug(e && e.toString ? e.toString() : e); +} + +export function initStyling(commandline: CommandLine, session: Session) { + log = (text) => stdout((md(text, session).trim())); + error = (text) => stdout(`${red.bold('ERROR: ')}${md(text, session).trim()}`); + warning = (text) => stdout(`${yellow.bold('WARNING: ')}${md(text, session).trim()}`); + debug = (text) => { if (commandline.debug) { stdout(`${cyan.bold('DEBUG: ')}${md(text, session).trim()}`); } }; + + session.channels.on('message', (text: string, context: any, msec: number) => { + log(text); + }); + + session.channels.on('error', (text: string, context: any, msec: number) => { + error(text); + }); + + session.channels.on('debug', (text: string, context: any, msec: number) => { + debug(`${cyan.bold(`[${formatTime(msec)}]`)} ${md(text, session)}`); + }); + + session.channels.on('warning', (text: string, context: any, msec: number) => { + warning(text); + }); +} diff --git a/ce/ce/cli/switch.ts b/ce/ce/cli/switch.ts new file mode 100644 index 0000000000..d0370b9e95 --- /dev/null +++ b/ce/ce/cli/switch.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { i } from '../i18n'; +import { Command } from './command'; +import { Help } from './command-line'; +import { cmdSwitch } from './format'; + + +export abstract class Switch implements Help { + readonly abstract switch: string; + readonly title = ''; + readonly abstract help: Array; + readonly required: boolean; + readonly multipleAllowed: boolean; + + constructor(protected command: Command, options?: { multipleAllowed?: boolean, required?: boolean }) { + command.switches.push(this); + this.multipleAllowed = options?.multipleAllowed || false; + this.required = options?.required || false; + } + + get valid() { + return this.required || this.active; + } + + #values?: Array; + get values() { + return this.#values || (this.#values = this.command.commandLine.claim(this.switch) || []); + } + + get value(): any | undefined { + const v = this.values; + strict.ok(v.length < 2, i`Expected a single value for ${cmdSwitch(this.switch)} - found multiple`); + return v[0]; + } + + get requiredValue(): string { + const v = this.values; + strict.ok(v.length == 1 && v[0], i`Expected a single value for '--${this.switch}'.`); + return v[0]; + } + + get active(): boolean { + const v = this.values; + return !!v && v.length > 0 && v[0] !== 'false'; + } +} diff --git a/ce/ce/cli/switches/clear.ts b/ce/ce/cli/switches/clear.ts new file mode 100644 index 0000000000..3bb39ad0c7 --- /dev/null +++ b/ce/ce/cli/switches/clear.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { Switch } from '../switch'; + +export class Clear extends Switch { + switch = 'clear'; + get help() { + return [ + i`removes all files in the local cache` + ]; + } +} diff --git a/ce/ce/cli/switches/debug.ts b/ce/ce/cli/switches/debug.ts new file mode 100644 index 0000000000..81b685af56 --- /dev/null +++ b/ce/ce/cli/switches/debug.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { cli } from '../constants'; +import { Switch } from '../switch'; + +export class Debug extends Switch { + switch = 'debug'; + get help() { + return [ + i`enables debug mode, displays internal messsages about how ${cli} works` + ]; + } +} diff --git a/ce/ce/cli/switches/force.ts b/ce/ce/cli/switches/force.ts new file mode 100644 index 0000000000..4c22d2cccb --- /dev/null +++ b/ce/ce/cli/switches/force.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { Switch } from '../switch'; + +export class Force extends Switch { + switch = 'force'; + get help() { + return [ + i`proceeds with the (potentially dangerous) action without confirmation` + ]; + } +} diff --git a/ce/ce/cli/switches/installed.ts b/ce/ce/cli/switches/installed.ts new file mode 100644 index 0000000000..aa56fe8763 --- /dev/null +++ b/ce/ce/cli/switches/installed.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { Switch } from '../switch'; + +export class Installed extends Switch { + switch = 'installed'; + get help() { + return [ + i`shows the _installed_ artifacts` + ]; + } +} diff --git a/ce/ce/cli/switches/msbuild-props.ts b/ce/ce/cli/switches/msbuild-props.ts new file mode 100644 index 0000000000..125edc8c61 --- /dev/null +++ b/ce/ce/cli/switches/msbuild-props.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Uri } from '../../util/uri'; +import { resolvePath } from '../command-line'; +import { Switch } from '../switch'; + +export class MSBuildProps extends Switch { + switch = 'msbuild-props'; + override multipleAllowed = false; + get help() { + return [ + i`Full path to the file in which MSBuild properties will be written.` + ]; + } + + override get value(): Uri | undefined { + const v = resolvePath(super.value); + return v ? session.fileSystem.file(v) : undefined; + } +} diff --git a/ce/ce/cli/switches/project.ts b/ce/ce/cli/switches/project.ts new file mode 100644 index 0000000000..3afca8b20c --- /dev/null +++ b/ce/ce/cli/switches/project.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ProjectManifest } from '../../artifacts/artifact'; +import { FileType } from '../../fs/filesystem'; +import { i } from '../../i18n'; +import { session } from '../../main'; +import { Uri } from '../../util/uri'; +import { resolvePath } from '../command-line'; +import { projectFile } from '../format'; +import { debug, error } from '../styling'; +import { Switch } from '../switch'; + +export class Project extends Switch { + switch = 'project'; + get help() { + return [ + i`override the path to the project folder` + ]; + } + + async getProjectFolder() { + const v = resolvePath(super.value); + if (v) { + const uri = session.fileSystem.file(v); + const stat = await uri.stat(); + + if (stat.type & FileType.File) { + return uri; + } + if (stat.type & FileType.Directory) { + const project = await session.findProjectProfile(uri, false); + if (project) { + return project; + } + } + error(i`Unable to find project environment ${projectFile(uri)}`); + return undefined; + } + return session.findProjectProfile(); + } + + override get value(): Promise { + return this.getProjectFolder(); + } + + get manifest(): Promise { + return this.value.then(async (project) => { + if (!project) { + debug('No project manifest'); + return undefined; + } + + debug(`Loading project manifest ${project} `); + return new ProjectManifest(session, await session.openManifest(project)); + }); + } +} \ No newline at end of file diff --git a/ce/ce/cli/switches/registry.ts b/ce/ce/cli/switches/registry.ts new file mode 100644 index 0000000000..4f26c667a9 --- /dev/null +++ b/ce/ce/cli/switches/registry.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { sanitizeUri } from '../../artifacts/artifact'; +import { i } from '../../i18n'; +import { Session } from '../../session'; +import { UpdateCommand } from '../commands/update'; +import { Switch } from '../switch'; + +export class Registry extends Switch { + switch = 'registry'; + get help() { + return [ + i`override the path to the registry` + ]; + } + + async loadRegistries(session: Session, more: Array = []) { + for (const registry of new Set([...this.values, ...more].map(each => sanitizeUri(each)))) { + if (registry) { + const uri = session.parseUri(registry); + if (await session.isLocalRegistry(uri) || await session.isRemoteRegistry(uri)) { + + const r = session.loadRegistry(uri, 'artifact'); + if (r) { + try { + await r.load(); + } catch (e) { + // try to update the repo + if (!await UpdateCommand.update(r)) { + session.channels.error(i`failed to load registry ${uri.toString()}`); + continue; + } + } + // registry is loaded + // it should be added to the aggregator + session.defaultRegistry.add(r, registry); + } + continue; + } + session.channels.error(i`Invalid registry ${registry}`); + } + } + + return session.defaultRegistry; + } + +} \ No newline at end of file diff --git a/ce/ce/cli/switches/verbose.ts b/ce/ce/cli/switches/verbose.ts new file mode 100644 index 0000000000..7eed7f7e05 --- /dev/null +++ b/ce/ce/cli/switches/verbose.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { Switch } from '../switch'; + +export class Verbose extends Switch { + switch = 'verbose'; + get help() { + return [ + i`enables verbose mode, displays verbose messsages about the process` + ]; + } +} diff --git a/ce/ce/cli/switches/version.ts b/ce/ce/cli/switches/version.ts new file mode 100644 index 0000000000..e697b91a7c --- /dev/null +++ b/ce/ce/cli/switches/version.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { Switch } from '../switch'; + +export class Version extends Switch { + switch = 'version'; + get help() { + return [ + i`a version or version range to match` + ]; + } +} \ No newline at end of file diff --git a/ce/ce/cli/switches/whatIf.ts b/ce/ce/cli/switches/whatIf.ts new file mode 100644 index 0000000000..4ffb9691bd --- /dev/null +++ b/ce/ce/cli/switches/whatIf.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../../i18n'; +import { Switch } from '../switch'; + +export class WhatIf extends Switch { + switch = 'what-if'; + get help() { + return [ + i`does not actually perform the action, shows only what would be done` + ]; + } +} diff --git a/ce/ce/constants.ts b/ce/ce/constants.ts new file mode 100644 index 0000000000..b9b6511858 --- /dev/null +++ b/ce/ce/constants.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const project = 'environment.yaml'; +export const undo = 'Z_VCPKG_UNDO'; +export const postscriptVarible = 'Z_VCPKG_POSTSCRIPT'; +export const blank = '\n'; +export const gitUniqueIdPrefix = 'https://aka.ms/vcpkg-ce-default::tools/git::'; +export const gitArtifact = 'microsoft:tools/git'; +export const latestVersion = '*'; +export const vcpkgDownloadFolder = 'VCPKG_DOWNLOADS'; +export const globalConfigurationFile = 'vcpkg-configuration.global.json'; +export const profileNames = ['vcpkg-configuration.json', 'vcpkg-configuration.yaml', 'environment.yaml', 'environment.yml', 'environment.json']; +export const registryIndexFile = 'index.yaml'; + +export const defaultConfig = + `{ + "registries": [ + { + "kind": "artifact", + "name": "microsoft", + "location": "https://aka.ms/vcpkg-ce-default" + } + ] +} +`; diff --git a/ce/ce/exports.ts b/ce/ce/exports.ts new file mode 100644 index 0000000000..7fc8613767 --- /dev/null +++ b/ce/ce/exports.ts @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ManyMap } from './util/linq'; +import { Queue } from './util/promise'; + +/** This adds the expected declarations to the Array type. */ +declare global { + interface Array { + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param callbackfn A function that accepts up to three arguments. The filter method calls the callbackfn function one time for each element in the array. + */ + where(callbackfn: (value: T, index: number, array: Array) => value is S): Array; + /** + * Returns the elements of an array that meet the condition specified in a callback function. + * @param callbackfn A function that accepts up to three arguments. The filter method calls the callbackfn function one time for each element in the array. + */ + where(callbackfn: (value: T, index: number, array: Array) => unknown): Array; + + /** + * Calls a defined callback function on each element of an array, and returns an array that contains the results. + */ + select(callbackfn: (value: T, index: number, array: Array) => U): Array; + + /** + * Determines whether the specified callback function returns true for any element of an array. + * @param callbackfn A function that accepts up to three arguments. The some method calls + * the callbackfn function for each element in the array until the callbackfn returns a value + * which is coercible to the Boolean value true, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + any(callbackfn: (value: T, index: number, array: Array) => unknown, thisArg?: any): boolean; + /** + * Determines whether all the members of an array satisfy the specified test. + * @param callbackfn A function that accepts up to three arguments. The every method calls + * the callbackfn function for each element in the array until the callbackfn returns a value + * which is coercible to the Boolean value false, or until the end of the array. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. + * If thisArg is omitted, undefined is used as the this value. + */ + all(callbackfn: (value: T, index: number, array: Array) => unknown, thisArg?: any): boolean; + + /** + * Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. + * @param start The zero-based location in the array from which to start removing elements. + * @param deleteCount The number of elements to remove. + * @param items Elements to insert into the array in place of the deleted elements. + */ + insert(start: number, ...items: Array): Array; + + /** + * Removes elements from an array returning the deleted elements. + * @param start The zero-based location in the array from which to start removing elements. + * @param deleteCount The number of elements to remove. + */ + remove(start: number, deleteCount?: number): Array; + + /** + * Iterates on a collection to create a Queue that will throttle + * the async operation 'fn' to a reasonable degree of parallelism. + * @param fn the async Fn to call on each + */ + forEachAsync(fn: (v: T) => Promise): Queue; + + selectMany(callbackfn: (value: T, index: number, array: Array) => U): Array ? InnerArr : U>; + groupByMap(keySelector: (each: T) => TKey, selector: (each: T) => TValue): Map>; + groupBy(keySelector: (each: T) => string, selector: (each: T) => TValue): { [s: string]: Array }; + count(predicate: (each: T) => Promise): Promise, + count(predicate: (each: T) => boolean): number, + readonly last: T | undefined; + readonly first: T | undefined; + } +} + +declare global { + interface Map { + getOrDefault(key: K, defaultValue: V | (() => V)): V; + } +} + +if (!Map.prototype.getOrDefault) { + Object.defineProperties(Map.prototype, { + getOrDefault: { + value: function (key: any, defaultValue: any) { + let v = this.get(key); + if (!v) { + this.set(key, v = typeof defaultValue === 'function' ? defaultValue() : defaultValue); + } + return v; + } + } + }); +} + +if (!Array.prototype.insert) { + /** + * adding some linq-like functionality to the Array type + */ + Object.defineProperties(Array.prototype, { + where: { value: Array.prototype.filter }, + select: { value: Array.prototype.map }, + any: { value: Array.prototype.some }, + all: { value: Array.prototype.every }, + insert: { value: function (position: number, items: Array) { return (>this).splice(position, 0, ...items); } }, + selectMany: { value: Array.prototype.flatMap }, + count: { + value: function (predicate: (e: any) => boolean | Promise) { + let v = 0; + const all = []; + for (const each of this) { + const test = predicate(each); + if (test.then) { + all.push(test.then((antecedent: any) => { + if (antecedent) { + v++; + } + })); + continue; + } + if (test) { + v++; + } + } + if (all.length) { + return Promise.all(all).then(() => v); + } + return v; + } + }, + groupByMap: { + value: function (keySelector: (each: any) => any, selector: (each: any) => any) { + const result = new ManyMap(); + for (const each of this) { + result.push(keySelector(each), selector(each)); + } + return result; + } + }, + groupBy: { + value: function (keySelector: (each: any) => any, selector: (each: any) => any) { + const result = {}; + for (const each of this) { + const key = keySelector(each); + (result[key] = result[key] || new Array()).push(selector(each)); + } + return result; + } + }, + last: { + get() { + return this[this.length - 1]; + } + }, + first: { + get() { + return this[0]; + } + }, + forEachAsync: { + value: function (fn: (i: any) => Promise) { + return new Queue().enqueueMany(this, fn); + } + } + }); +} diff --git a/ce/ce/fs/acquire.ts b/ce/ce/fs/acquire.ts new file mode 100644 index 0000000000..d930ac524c --- /dev/null +++ b/ce/ce/fs/acquire.ts @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { pipeline as origPipeline } from 'stream'; +import { promisify } from 'util'; +import { i } from '../i18n'; +import { AcquireEvents } from '../interfaces/events'; +import { Session } from '../session'; +import { Credentials } from '../util/credentials'; +import { ExtendedEmitter } from '../util/events'; +import { RemoteFileUnavailable } from '../util/exceptions'; +import { Algorithm, Hash } from '../util/hash'; +import { Uri } from '../util/uri'; +import { get, getStream, RemoteFile, resolveRedirect } from './https'; +import { ProgressTrackingStream } from './streams'; + +const pipeline = promisify(origPipeline); + +const size32K = 1 << 15; +const size64K = 1 << 16; + +export interface AcquireOptions extends Hash { + /** force a redownload even if it's in cache */ + force?: boolean; + credentials?: Credentials; +} + +export async function acquireArtifactFile(session: Session, uris: Array, outputFilename: string, events: Partial, options?: AcquireOptions) { + await session.cache.createDirectory(); + const outputFile = session.cache.join(outputFilename); + session.channels.debug(`Acquire file '${outputFilename}' from [${uris.map(each => each.toString()).join(',')}]`); + + if (options?.algorithm && options?.value) { + session.channels.debug(`We have a hash: ${options.algorithm}/${options.value}`); + + // if we have hash data, check to see if the output file is good. + if (await outputFile.isFile()) { + session.channels.debug(`There is an output file already, verifying: ${outputFile.fsPath}`); + + if (await outputFile.hashValid(events, options)) { + session.channels.debug(`Cached file matched hash: ${outputFile.fsPath}`); + return outputFile; + } + } + } + + // is the file present on a local filesystem? + for (const uri of uris) { + if (uri.isLocal) { + // we have a local file + + if (options?.algorithm && options?.value) { + // we have a hash. + // is it valid? + if (await uri.hashValid(events, options)) { + session.channels.debug(`Local file matched hash: ${uri.fsPath}`); + return uri; + } + } else if (await uri.exists()) { + // we don't have a hash, but the file is local, and it exists. + // we have to return it + session.channels.debug(`Using local file (no hash, unable to verify): ${uri.fsPath}`); + return uri; + } + // do we have a filename + } + } + + // we don't have a local file + // https is all that we know at the moment. + const webUris = uris.where(each => each.isHttps); + if (webUris.length === 0) { + // wait, no web uris? + throw new RemoteFileUnavailable(uris); + } + + return https(session, webUris, outputFilename, events, options); +} + +/** */ +async function https(session: Session, uris: Array, outputFilename: string, events: Partial, options?: AcquireOptions) { + const ee = new ExtendedEmitter(); + ee.subscribe(events); + session.channels.debug(`Attempting to download file '${outputFilename}' from [${uris.map(each => each.toString()).join(',')}]`); + + let resumeAtOffset = 0; + await session.cache.createDirectory(); + const outputFile = session.cache.join(outputFilename); + + if (options?.force) { + session.channels.debug(`Acquire '${outputFilename}': force specified, forcing download`); + // is force specified; delete the current file + await outputFile.delete(); + } + + // start this peeking at the target uris. + session.channels.debug(`Acquire '${outputFilename}': checking remote connections`); + const locations = new RemoteFile(uris, { credentials: options?.credentials }); + let url: Uri | undefined; + + // is there a file in the cache + if (await outputFile.exists()) { + session.channels.debug(`Acquire '${outputFilename}': local file exists`); + if (options?.algorithm) { + // does it match a hash that we have? + if (await outputFile.hashValid(events, options)) { + session.channels.debug(`Acquire '${outputFilename}': local file hash matches metdata`); + // yes it does. let's just return done. + return outputFile; + } + } + + // it doesn't match a known hash. + const contentLength = await locations.contentLength; + session.channels.debug(`Acquire '${outputFilename}': remote connection info is back`); + const onDiskSize = await outputFile.size(); + if (!await locations.availableLocation) { + if (locations.failures.all(each => each.code === 404)) { + let msg = i`Unable to download file`; + if (options?.credentials) { + msg += (i` - It could be that your authentication credentials are not correct`); + } + + session.channels.error(msg); + throw new RemoteFileUnavailable(uris); + } + } + // first, make sure that there is a remote that is accessible. + strict.ok(!!await locations.availableLocation, `Requested file ${outputFilename} has no accessible locations ${uris.map(each => each.toString()).join(',')}`); + + url = await locations.resumableLocation; + // ok, does it support resume? + if (url) { + // yes, let's check what the size is expected to be. + + if (!options?.algorithm) { + + if (contentLength === onDiskSize) { + session.channels.debug(`Acquire '${outputFilename}': on disk file matches length of remote file`); + const algorithm = (await locations.algorithm); + const value = await locations.hash; + session.channels.debug(`Acquire '${outputFilename}': remote alg/hash: '${algorithm}'/'${value}`); + if (algorithm && value && outputFile.hashValid(events, { algorithm, value, ...options })) { + session.channels.debug(`Acquire '${outputFilename}': on disk file hash matches the server hash`); + // so *we* don't have the hash, but ... if the server has a hash, we could see if what we have is what they have? + // it does match what the server has. + // I call this an win. + return outputFile; + } + + // we don't have a hash, or what we have doesn't match. + // maybe we will get a match below (or resume) + } + } + + if (onDiskSize > size64K) { + // it's bigger than 64k. Good. otherwise, we're just wasting time. + + // so, how big is the remote + if (contentLength >= onDiskSize) { + session.channels.debug(`Acquire '${outputFilename}': local file length is less than or equal to remote file length`); + // looks like there could be more remotely than we have. + // lets compare the first 32k and the last 32k of what we have + // against what they have and see if they match. + const top = (await get(url, { start: 0, end: size32K - 1, credentials: options?.credentials })).rawBody; + const bottom = (await get(url, { start: onDiskSize - size32K, end: onDiskSize - 1, credentials: options?.credentials })).rawBody; + + const onDiskTop = await outputFile.readBlock(0, size32K - 1); + const onDiskBottom = await outputFile.readBlock(onDiskSize - size32K, onDiskSize - 1); + + if (top.compare(onDiskTop) === 0 && bottom.compare(onDiskBottom) === 0) { + session.channels.debug(`Acquire '${outputFilename}': first/last blocks are equal`); + // the start and end of what we have does match what they have. + // is this file the same size? + if (contentLength === onDiskSize) { + // same file size, front and back match, let's accept this. begrudgingly + session.channels.debug(`Acquire '${outputFilename}': file size is identical. keeping this one`); + return outputFile; + } + // looks like we can continue from here. + session.channels.debug(`Acquire '${outputFilename}': ok to resume`); + resumeAtOffset = onDiskSize; + } + } + } + } + } + + if (resumeAtOffset === 0) { + // clearly we mean to not resume. clean any existing file. + session.channels.debug(`Acquire '${outputFilename}': not resuming file, full download`); + await outputFile.delete(); + } + + url = url || await locations.availableLocation; + strict.ok(!!url, `Requested file ${outputFilename} has no accessible locations ${uris.map(each => each.toString()).join(',')}`); + session.channels.debug(`Acquire '${outputFilename}': initiating download`); + const length = await locations.contentLength; + + const inputStream = getStream(url, { start: resumeAtOffset, end: length > 0 ? length : undefined, credentials: options?.credentials }); + let progressStream; + if (length > 0) { + progressStream = new ProgressTrackingStream(resumeAtOffset, length); + progressStream.on('progress', (filePercentage) => ee.emit('download', outputFilename, filePercentage)); + } + const outputStream = await outputFile.writeStream({ append: true }); + ee.emit('download', outputFilename, 0); + + // whoooosh. write out the file + if (progressStream) { + await pipeline(inputStream, progressStream, outputStream); + } else { + await pipeline(inputStream, outputStream); + } + + // we've downloaded the file, let's see if it matches the hash we have. + if (options?.algorithm) { + session.channels.debug(`Acquire '${outputFilename}': checking downloaded file hash`); + // does it match the hash that we have? + if (!await outputFile.hashValid(events, options)) { + await outputFile.delete(); + throw new Error(i`Downloaded file '${outputFile.fsPath}' did not have the correct hash (${options.algorithm}: ${options.value}) `); + } + session.channels.debug(`Acquire '${outputFilename}': downloaded file hash matches specified hash`); + } + + session.channels.debug(`Acquire '${outputFilename}': downloading file successful`); + ee.emit('download', outputFilename, 1000); + ee.emit('complete'); + return outputFile; +} + +export async function resolveNugetUrl(session: Session, pkg: string) { + const [, name, version] = pkg.match(/^(.*)\/(.*)$/) ?? []; + strict.ok(version, i`package reference '${pkg}' is not a valid nuget package reference ({name}/{version})`); + + // let's resolve the redirect first, since nuget servers don't like us getting HEAD data on the targets via a redirect. + // even if this wasn't the case, this is lower cost now rather than later. + const url = await resolveRedirect(session.parseUri(`https://www.nuget.org/api/v2/package/${name}/${version}`)); + + session.channels.debug(`Resolving nuget package for '${pkg}' to '${url}'`); + return url; +} + +export async function acquireNugetFile(session: Session, pkg: string, outputFilename: string, events: Partial, options?: AcquireOptions): Promise { + return https(session, [await resolveNugetUrl(session, pkg)], outputFilename, events, options); +} + diff --git a/ce/ce/fs/filesystem.ts b/ce/ce/fs/filesystem.ts new file mode 100644 index 0000000000..fc54c4761c --- /dev/null +++ b/ce/ce/fs/filesystem.ts @@ -0,0 +1,410 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/ban-types */ + +import { EventEmitter } from 'ee-ts'; +import { Readable, Writable } from 'stream'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; + +const size64K = 1 << 16; +const size32K = 1 << 15; + +/** + * The `FileStat`-type represents metadata about a file + */ +export interface FileStat { + /** + * The type of the file, e.g. is a regular file, a directory, or symbolic link + * to a file. + * + * *Note:* This value might be a bitmask, e.g. `FileType.File | FileType.SymbolicLink`. + */ + type: FileType; + /** + * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + ctime: number; + /** + * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + * + * *Note:* If the file changed, it is important to provide an updated `mtime` that advanced + * from the previous value. Otherwise there may be optimizations in place that will not show + * the updated file contents in an editor for example. + */ + mtime: number; + /** + * The size in bytes. + * + * *Note:* If the file changed, it is important to provide an updated `size`. Otherwise there + * may be optimizations in place that will not show the updated file contents in an editor for + * example. + */ + size: number; + /** + * The file mode (unix permissions). + */ + mode: number; +} + +/** +* Enumeration of file types. The types `File` and `Directory` can also be +* a symbolic links, in that case use `FileType.File | FileType.SymbolicLink` and +* `FileType.Directory | FileType.SymbolicLink`. +*/ +export enum FileType { + /** + * The file type is unknown. + */ + Unknown = 0, + /** + * A regular file. + */ + File = 1, + /** + * A directory. + */ + Directory = 2, + /** + * A symbolic link to a file. + */ + SymbolicLink = 64 +} + +export interface WriteStreamOptions { + append?: boolean; + mode?: number; + mtime?: Date; +} + +/** + * A random-access reading interface to access a file in a FileSystem. + * + * Ideally, we keep reads in a file to a forward order, so that this can be implemented on filesystems + * that do not support random access (ie, please do your best to order reads so that they go forward only as much as possible) + * + * Underneath on FSes that do not support random access, this would likely require multiple 'open' operation for the same + * target file. + */ +export abstract class ReadHandle { + /** + * Reads a block from a file + * + * @param buffer The buffer that the data will be written to. + * @param offset The offset in the buffer at which to start writing. + * @param length The number of bytes to read. + * @param position The offset from the beginning of the file from which data should be read. If `null`, data will be read from the current position. + */ + abstract read(buffer: TBuffer, offset?: number | null, length?: number | null, position?: number | null): Promise<{ bytesRead: number, buffer: TBuffer }>; + + async readComplete(buffr: TBuffer, offset = 0, length = buffr.byteLength, position: number | null = null, totalRead = 0): Promise<{ bytesRead: number, buffer: TBuffer }> { + const { bytesRead, buffer } = await this.read(buffr, offset, length, position); + if (length) { + if (bytesRead && bytesRead < length) { + return await this.readComplete(buffr, offset + bytesRead, length - bytesRead, position ? position + bytesRead : null, bytesRead + totalRead); + } + } + return { bytesRead: bytesRead + totalRead, buffer }; + } + /** + * Returns a Readable for consuming an opened ReadHandle + * @param start the first byte to read of the target + * @param end the last byte to read of the target (inclusive!) + */ + readStream(start = 0, end = Infinity): Readable { + return Readable.from(asyncIterableOverHandle(start, end, this), {}); + } + + abstract size(): Promise; + + abstract close(): Promise; + + range(start: number, length: number) { + return new RangeReadHandle(this, start, length); + } +} + +class RangeReadHandle extends ReadHandle { + + pos = 0; + readHandle?: ReadHandle; + + constructor(readHandle: ReadHandle, private start: number, private length: number) { + super(); + this.readHandle = readHandle; + } + + async read(buffer: TBuffer, offset?: number | null, length?: number | null, position?: number | null): Promise<{ bytesRead: number; buffer: TBuffer; }> { + if (this.readHandle) { + position = position !== undefined && position !== null ? (position + this.start) : (this.pos + this.start); + length = length === null ? this.length : length; + + const result = await this.readHandle.read(buffer, offset, length, position); + this.pos += result.bytesRead; + return result; + } + + return { + bytesRead: 0, buffer + }; + + } + + async size(): Promise { + return this.length; + } + + async close(): Promise { + this.readHandle = undefined; + } + +} + +/** + * Picks a reasonable buffer size. Not more than 64k + * + * @param length + */ +function reasonableBuffer(length: number) { + return Buffer.alloc(length > size64K ? size32K : length); +} + +/** + * Creates an AsyncIterable over a ReadHandle + * @param start the first byte in the target read from + * @param end the last byte in the target to read from + * @param handle the ReadHandle + */ +async function* asyncIterableOverHandle(start: number, end: number, handle: ReadHandle): AsyncIterable { + while (start < end) { + // buffer alloc must be inside the loop; zlib will hold the buffers until it can deal with a whole stream. + const buffer = reasonableBuffer(1 + end - start); + const count = Math.min(1 + end - start, buffer.byteLength); + const b = await handle.read(buffer, 0, count, start); + if (b.bytesRead === 0) { + return; + } + start += b.bytesRead; + // return only what was actually read. (just a view) + if (b.bytesRead === buffer.byteLength) { + yield buffer; + } + else { + yield buffer.slice(0, b.bytesRead); + } + } +} + +export abstract class FileSystem extends EventEmitter { + + protected baseUri?: Uri; + + /** + * Creates a new URI from a file system path, e.g. `c:\my\files`, + * `/usr/home`, or `\\server\share\some\path`. + * + * associates this FileSystem with the Uri + * + * @param path A file system path (see `URI#fsPath`) + */ + file(path: string): Uri { + return Uri.file(this, path); + } + + /** construct an Uri from the various parts */ + from(components: { + scheme: string; + authority?: string; + path?: string; + query?: string; + fragment?: string; + }): Uri { + return Uri.from(this, components); + } + + /** + * Creates a new URI from a string, e.g. `https://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @param value A string which represents an URI (see `URI#toString`). + */ + parse(value: string, _strict?: boolean): Uri { + return Uri.parse(this, value, _strict); + } + + /** + * Retrieve metadata about a file. + * + * @param uri The uri of the file to retrieve metadata about. + * @return The file metadata about the file. + */ + abstract stat(uri: Uri, options?: {}): Promise; + + /** + * Retrieve all entries of a [directory](#FileType.Directory). + * + * @param uri The uri of the folder. + * @return An array of name/type-tuples or a Promise that resolves to such. + */ + abstract readDirectory(uri: Uri, options?: { recursive?: boolean }): Promise>; + + /** + * Create a new directory (Note, that new files are created via `write`-calls). + * + * *Note* that missing directories are created automatically, e.g this call has + * `mkdirp` semantics. + * + * @param uri The uri of the new folder. + */ + abstract createDirectory(uri: Uri, options?: {}): Promise; + + /** + * Read the entire contents of a file. + * + * @param uri The uri of the file. + * @return An array of bytes or a Promise that resolves to such. + */ + abstract readFile(uri: Uri, options?: {}): Promise; + + /** + * Creates a stream to read a file from the filesystem + * + * @param uri The uri of the file. + * @return a Readable stream + */ + abstract readStream(uri: Uri, options?: { start?: number, end?: number }): Promise; + + /** + * Write data to a file, replacing its entire contents. + * + * @param uri The uri of the file. + * @param content The new content of the file. + */ + abstract writeFile(uri: Uri, content: Uint8Array): Promise; + + /** + * Creates a stream to write a file to the filesystem + * + * @param uri The uri of the file. + * @return a Writeable stream + */ + abstract writeStream(uri: Uri, options?: WriteStreamOptions): Promise; + + /** + * Delete a file. + * + * @param uri The resource that is to be deleted. + * @param options Defines if trash can should be used and if deletion of folders is recursive + */ + abstract delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): Promise; + + /** + * Rename a file or folder. + * + * @param oldUri The existing file. + * @param newUri The new location. + * @param options Defines if existing files should be overwritten. + */ + abstract rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): Promise; + + abstract openFile(uri: Uri): Promise; + + /** + * Copy files or folders. + * + * @param source The existing file. + * @param destination The destination location. + * @param options Defines if existing files should be overwritten. + */ + abstract copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Promise; + + abstract createSymlink(symlink: Uri, target: Uri): Promise; + + /** checks to see if the target exists */ + async exists(uri: Uri) { + try { + return !!(await this.stat(uri)); + } catch (e) { + // if this fails, we're assuming false + } + return false; + } + + /** checks to see if the target is a directory/folder */ + async isDirectory(uri: Uri) { + try { + return !!((await this.stat(uri)).type & FileType.Directory); + } catch { + // if this fails, we're assuming false + } + return false; + } + + /** checks to see if the target is a file */ + async isFile(uri: Uri) { + try { + const s = await this.stat(uri); + + return !!(s.type & FileType.File); + } catch { + // if this fails, we're assuming false + } + return false; + } + + /** checks to see if the target is a symbolic link */ + async isSymlink(uri: Uri) { + try { + return !!((await this.stat(uri)) && FileType.SymbolicLink); + } catch { + // if this fails, we're assuming false + } + return false; + } + + constructor(protected session: Session) { + super(); + } + + /** EventEmitter for when files are read */ + protected read(path: Uri, context?: any) { + this.emit('read', path, context, this.session.stopwatch.total); + } + + /** EventEmitter for when files are written */ + protected write(path: Uri, context?: any) { + this.emit('write', path, context, this.session.stopwatch.total); + } + + /** EventEmitter for when files are deleted */ + protected deleted(path: Uri, context?: any) { + this.emit('deleted', path, context, this.session.stopwatch.total); + } + + /** EventEmitter for when files are renamed */ + protected renamed(path: Uri, context?: any) { + this.emit('renamed', path, context, this.session.stopwatch.total); + } + + /** EventEmitter for when directories are read */ + protected directoryRead(path: Uri, contents?: Promise>) { + this.emit('directoryRead', path, contents, this.session.stopwatch.total); + } + + /** EventEmitter for when direcotries are created */ + protected directoryCreated(path: Uri, context?: any) { + this.emit('directoryCreated', path, context, this.session.stopwatch.total); + } +} + + +/** Event definitions for FileSystem events */ +interface FileSystemEvents { + read(path: Uri, context: any, msec: number): void; + write(path: Uri, context: any, msec: number): void; + deleted(path: Uri, context: any, msec: number): void; + renamed(path: Uri, context: any, msec: number): void; + directoryRead(path: Uri, contents: Promise> | undefined, msec: number): void; + directoryCreated(path: Uri, context: any, msec: number): void; +} diff --git a/ce/ce/fs/http-filesystem.ts b/ce/ce/fs/http-filesystem.ts new file mode 100644 index 0000000000..477934fdab --- /dev/null +++ b/ce/ce/fs/http-filesystem.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Readable, Writable } from 'stream'; +import { Uri } from '../util/uri'; +import { FileStat, FileSystem, FileType, ReadHandle } from './filesystem'; +import { get, getStream, head } from './https'; + +/** + * HTTPS Filesystem + * + */ +export class HttpsFileSystem extends FileSystem { + + async stat(uri: Uri): Promise { + const result = await head(uri); + + return { + type: FileType.File, + mtime: Date.parse(result.headers.date || ''), + ctime: Date.parse(result.headers.date || ''), + size: Number.parseInt(result.headers['content-length'] || '0'), + mode: 0o555 // https is read only but always 'executable' + }; + } + readDirectory(uri: Uri): Promise> { + throw new Error('Method not implemented'); + } + createDirectory(uri: Uri): Promise { + throw new Error('Method not implemented'); + } + async readFile(uri: Uri): Promise { + return (await get(uri)).rawBody; + } + writeFile(uri: Uri, content: Uint8Array): Promise { + throw new Error('Method not implemented'); + } + delete(uri: Uri, options?: { recursive?: boolean | undefined; useTrash?: boolean | undefined; }): Promise { + throw new Error('Method not implemented'); + } + rename(source: Uri, target: Uri, options?: { overwrite?: boolean | undefined; }): Promise { + throw new Error('Method not implemented'); + } + copy(source: Uri, target: Uri, options?: { overwrite?: boolean | undefined; }): Promise { + throw new Error('Method not implemented'); + } + async createSymlink(original: Uri, symlink: Uri): Promise { + throw new Error('Method not implemented'); + } + async readStream(uri: Uri, options?: { start?: number, end?: number }): Promise { + return getStream(uri, options); + } + writeStream(uri: Uri): Promise { + throw new Error('Method not implemented'); + } + + async openFile(uri: Uri): Promise { + return new HttpsReadHandle(uri); + } +} + + +class HttpsReadHandle extends ReadHandle { + position = 0; + constructor(private target: Uri) { + super(); + } + + async read(buffer: TBuffer, offset = 0, length = buffer.byteLength, position: number | null = null): Promise<{ bytesRead: number; buffer: TBuffer; }> { + if (position !== null) { + this.position = position; + } + + const r = getStream(this.target, { start: this.position, end: this.position + length }); + let bytesRead = 0; + + for await (const chunk of r) { + const c = chunk; + c.copy(buffer, offset); + bytesRead += c.length; + offset += c.length; + } + return { bytesRead, buffer }; + } + + async size(): Promise { + return this.target.size(); + } + + async close() { + //return this.handle.close(); + } +} diff --git a/ce/ce/fs/https.ts b/ce/ce/fs/https.ts new file mode 100644 index 0000000000..9b95538b98 --- /dev/null +++ b/ce/ce/fs/https.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { default as got, Headers, HTTPError, Response } from 'got'; +import { Credentials } from '../util/credentials'; +import { anyWhere } from '../util/promise'; +import { Uri } from '../util/uri'; + +/** + * Resolves an HTTPS GET redirect by doing the GET, grabbing the redirects and then cancelling the rest of the request + * @param location the URL to get the final location of + */ +export async function resolveRedirect(location: Uri) { + let finalUrl = location; + + const stream = got.get(location.toUrl(), { timeout: 15000, isStream: true }); + + // when the response comes thru, we can grab the headers & stuff from it + stream.on('response', (response: Response) => { + finalUrl = location.fileSystem.parse(response.redirectUrls.last || finalUrl.toString()); + }); + + // we have to get at least some data for the response event to trigger. + for await (const chunk of stream) { + // but we don't need any of it :D + break; + } + stream.destroy(); + return finalUrl; +} + +/** + * Does an HTTPS HEAD request, and on a 404, tries to do an HTTPS GET and see if we get a redirect, and harvest the headers from that. + * @param location the target URL + * @param headers any headers to put in the request. + */ +export async function head(location: Uri, headers: Headers = {}, credentials?: Credentials): Promise> { + try { + setCredentials(headers, location, credentials); + // on a successful HEAD request, do nothing different + return await got.head(location.toUrl(), { timeout: 15000, headers }); + } catch (E) { + // O_o + // + // So, it turns out that nuget servers (maybe others too?) don't do redirects on HEAD requests, + // and instead issue a 404. + // let's retry the request as a GET, and dump it after the first chunk. + // typically, a HEAD request should see a 300-400msec response time + // and yes, this does stretch that out to 500-700msec, but whatcha gonna do? + if (E instanceof HTTPError && E.response.statusCode === 404) { + try { + const syntheticResponse = >{}; + const stream = got.get(location.toUrl(), { timeout: 15000, headers, isStream: true }); + + // when the response comes thru, we can grab the headers & stuff from it + stream.on('response', (response: Response) => { + syntheticResponse.headers = response.headers; + syntheticResponse.statusCode = response.statusCode; + syntheticResponse.redirectUrls = response.redirectUrls; + }); + + // we have to get at least some data for the response event to trigger. + for await (const chunk of stream) { + // but we don't need any of it :D + break; + } + stream.destroy(); + return syntheticResponse; + } + catch { + // whatever, it didn't work. let the rethrow happen. + } + } + throw E; + } +} + +/** HTTPS Get request, returns a buffer */ +export function get(location: Uri, options?: { start?: number, end?: number, headers?: Headers, credentials?: Credentials }) { + let headers: Headers | undefined = undefined; + headers = setRange(headers, options?.start, options?.end); + headers = setCredentials(headers, location, options?.credentials); + + return got.get(location.toUrl(), { headers }); +} + +function setRange(headers: Headers | undefined, start?: number, end?: number) { + if (start !== undefined || end !== undefined) { + headers = headers || {}; + headers['range'] = `bytes=${start !== undefined ? start : ''}-${end !== undefined ? end : ''}`; + } + return headers; +} + + +function setCredentials(headers: Headers | undefined, target: Uri, credentials?: Credentials) { + if (credentials) { + // todo: if we have to add some credential headers, we'd do it here. + // we've removed github auth support until we actually need such a thing + } + return headers; +} + +/** HTTPS Get request, returns a stream + * @internal +*/ +export function getStream(location: Uri, options?: { start?: number, end?: number, headers?: Headers, credentials?: Credentials }) { + let headers: Headers | undefined = options?.headers; + headers = setRange(headers, options?.start, undefined); + headers = setCredentials(headers, location, options?.credentials); + + return got.get(location.toUrl(), { isStream: true, retry: 3, headers }); +} + +export interface Info { + failed?: boolean; + location: Uri; + resumeable: boolean; + contentLength: number; + hash?: string; + algorithm?: string; +} + +function digest(headers: Headers) { + let hash = hashAlgorithm(headers['digest'], 'sha-256'); + + // any of the sha* hashes.. + if (hash) { + return { hash, algorithm: 'sha256' }; + } + hash = hashAlgorithm(headers['digest'], 'sha-384'); + if (hash) { + return { hash, algorithm: 'sha384' }; + } + hash = hashAlgorithm(headers['digest'], 'sha-512'); + if (hash) { + return { hash, algorithm: 'sha512' }; + } + + // nothing we know about. + return { hash: undefined, algorithm: undefined }; +} + +/** + * RemoteFile is a class that represents a single remote file, but mapped to multiple mirrored URLs + * on creation, it kicks off HEAD requests to each URL so that we can get hash/digest, length, resumability etc + * + * the properties are Promises<> to the results, where it grabs data from the first returning valid query without + * blocking elsewhere. + * +*/ +export class RemoteFile { + info: Array>; + constructor(protected locations: Array, options?: { credentials?: Credentials }) { + this.info = locations.map(location => { + return head(location, setCredentials({ + 'want-digest': 'sha-256;q=1, sha-512;q=0.9', + 'accept-encoding': 'identity;q=0', // we need to know the content length without gzip encoding, + }, location, options?.credentials)).then(data => { + if (data.statusCode === 200) { + const { hash, algorithm } = digest(data.headers); + return { + location, + resumeable: data.headers['accept-ranges'] === 'bytes', + contentLength: Number.parseInt(data.headers['content-length']!) || -1, // -1 means we were not told. + hash, + algorithm, + }; + } + this.failures.push({ + code: data.statusCode, + reason: `A non-ok status code was returned: ${data.statusMessage}` + }); + throw new Error(`A non-ok status code was returned: ${data.statusCode}`); + }, err => { + this.failures.push({ + code: err?.response?.statusCode, + reason: `A non-ok status code was returned: ${err?.response?.statusMessage}` + }); + throw err; + }); + }); + + + // lazy properties (which do not throw on errors.) + this.availableLocation = Promise.any(this.info).then(success => success.location, fail => undefined); + this.resumable = anyWhere(this.info, each => each.resumeable).then(success => true, fail => false); + this.resumableLocation = anyWhere(this.info, each => each.resumeable).then(success => success.location, fail => undefined); + this.contentLength = anyWhere(this.info, each => !!each.contentLength).then(success => success.contentLength, fail => -2); + this.hash = anyWhere(this.info, each => !!each.hash).then(success => success.hash, fail => undefined); + this.algorithm = anyWhere(this.info, each => !!each.algorithm).then(success => success.algorithm, fail => undefined); + } + + resumable: Promise; + contentLength: Promise; + hash: Promise; + algorithm: Promise; + availableLocation: Promise; + resumableLocation: Promise; + failures = new Array<{ code: number, reason: string }>(); +} + +/** + * Digest/hash in headers are base64 encoded strings. + * @param data the base64 encoded string + */ +function decode(data?: string): string | undefined { + return data ? Buffer.from(data, 'base64').toString('hex').toLowerCase() : undefined; +} + +/** + * Get the hash alg/hash from the digest. + * @param digest the digest header + * @param algorithm the algorithm we're trying to match + */ +function hashAlgorithm(digest: string | Array | undefined, algorithm: 'sha-256' | 'sha-384' | 'sha-512'): string | undefined { + for (const each of (digest ? Array.isArray(digest) ? digest : [digest] : [])) { + if (each.startsWith(algorithm)) { + return decode(each.substr(8)); + } + } + + // nothing. + return undefined; +} diff --git a/ce/ce/fs/local-filesystem.ts b/ce/ce/fs/local-filesystem.ts new file mode 100644 index 0000000000..2df7a2ef2f --- /dev/null +++ b/ce/ce/fs/local-filesystem.ts @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { COPYFILE_EXCL } from 'constants'; +import { close, createReadStream, createWriteStream, futimes, NoParamCallback, open as openFd, Stats, write as writeFd, writev as writevFd } from 'fs'; +import { copyFile, FileHandle, mkdir, open, readdir, readFile, rename, rm, stat, symlink, writeFile } from 'fs/promises'; +import { basename, join } from 'path'; +import { Readable, Writable } from 'stream'; +import { i } from '../i18n'; +import { delay } from '../util/events'; +import { TargetFileCollision } from '../util/exceptions'; +import { Queue } from '../util/promise'; +import { Uri } from '../util/uri'; +import { FileStat, FileSystem, FileType, ReadHandle, WriteStreamOptions } from './filesystem'; + +function getFileType(stats: Stats) { + return FileType.Unknown | + (stats.isDirectory() ? FileType.Directory : 0) | + (stats.isFile() ? FileType.File : 0) | + (stats.isSymbolicLink() ? FileType.SymbolicLink : 0); +} + +class LocalFileStats implements FileStat { + constructor(private stats: Stats) { + strict.ok(stats, i`stats may not be undefined`); + } + get type() { + return getFileType(this.stats); + } + get ctime() { + return this.stats.ctimeMs; + } + get mtime() { + return this.stats.mtimeMs; + } + get size() { + return this.stats.size; + } + get mode() { + return this.stats.mode; + } +} + + +/** + * Implementation of the Local File System + * + * This is used to handle the access to the local disks. + */ +export class LocalFileSystem extends FileSystem { + async stat(uri: Uri): Promise { + const path = uri.fsPath; + const s = await stat(path); + return new LocalFileStats(s); + } + + async readDirectory(uri: Uri, options?: { recursive?: boolean }): Promise> { + let retval!: Promise>; + try { + const folder = uri.fsPath; + const retval = new Array<[Uri, FileType]>(); + + // use forEachAsync instead so we can throttle this appropriately. + await (await readdir(folder)).forEachAsync(async each => { + const path = uri.fileSystem.file(join(folder, each)); + const type = getFileType(await stat(uri.join(each).fsPath)); + retval.push(<[Uri, FileType]>[path, type]); + if (options?.recursive && type === FileType.Directory) { + retval.push(... await this.readDirectory(path, options)); + } + }).done; + + return retval; + } finally { + // log that. + this.directoryRead(uri, retval); + } + } + + async createDirectory(uri: Uri): Promise { + await mkdir(uri.fsPath, { recursive: true }); + this.directoryCreated(uri); + } + + createSymlink(original: Uri, slink: Uri): Promise { + return symlink(original.fsPath, slink.fsPath, 'file'); + } + + async readFile(uri: Uri): Promise { + let contents!: Promise; + try { + contents = readFile(uri.fsPath); + return await contents; + } finally { + this.read(uri, contents); + } + } + + async writeFile(uri: Uri, content: Uint8Array): Promise { + try { + await uri.parent.createDirectory(); + return writeFile(uri.fsPath, content); + } finally { + this.write(uri, content); + } + } + + async delete(uri: Uri, options?: { recursive?: boolean | undefined; useTrash?: boolean | undefined; }): Promise { + try { + options = options || { recursive: false }; + await rm(uri.fsPath, { recursive: options.recursive, force: true, maxRetries: 3, retryDelay: 20 }); + // todo: Hack -- on windows, when something is used and then deleted, the delete might not actually finish + // before the Promise is resolved. Adding a delay fixes this (but probably is an underlying node bug) + await delay(50); + return; + } finally { + this.deleted(uri); + } + } + + rename(source: Uri, target: Uri, options?: { overwrite?: boolean | undefined; }): Promise { + try { + strict.equal(source.fileSystem, target.fileSystem, i`Cannot rename files across filesystems`); + return rename(source.fsPath, target.fsPath); + } finally { + this.renamed(source, { target, options }); + } + } + + async copy(source: Uri, target: Uri, options?: { overwrite?: boolean | undefined; }): Promise { + const { type } = await source.stat(); + const opts = (options || {}); + const overwrite = opts.overwrite ? 0 : COPYFILE_EXCL; + + if (type & FileType.File) { + // make sure the target folder is there + await target.parent.createDirectory(); + await copyFile(source.fsPath, target.fsPath, overwrite); + return 1; + } + + strict.ok(type & FileType.Directory, 'Unknown file type should never happen during copy'); + + let targetIsFile = false; + try { + targetIsFile = !!((await target.stat()).type & FileType.File); + } catch { + // not a file + } + + // if it's a folder, then the target has to be a folder, or not exist + if (targetIsFile) { + throw new TargetFileCollision(target, i`Copy failed: source (${source.fsPath}) is a folder, target (${target.fsPath}) is a file`); + } + + // make sure the target folder exists + await target.createDirectory(); + + // only the initial call gets to wait for everybody to finish. + let queue: Queue | undefined; + + // track the count, starting at the base folder. + if (opts.queue === undefined) { + queue = opts.queue = new Queue(); + } + + // loop thru the contents of this folder + for (const [sourceUri, fileType] of await source.readDirectory()) { + const targetUri = target.join(basename(sourceUri.path)); + if (fileType & FileType.Directory) { + await this.copy(sourceUri, targetUri, opts); + continue; + } + // queue up the copy file + void opts.queue.enqueue(() => copyFile(sourceUri.fsPath, targetUri.fsPath, overwrite)); + } + return queue ? queue.done : -1 /* innerloop */; + } + + async readStream(uri: Uri, options?: { start?: number, end?: number }): Promise { + this.read(uri); + return createReadStream(uri.fsPath, options); + } + + async writeStream(uri: Uri, options?: WriteStreamOptions): Promise { + this.write(uri); + const flags = options?.append ? 'a' : 'w'; + const createWriteOptions: any = { flags, mode: options?.mode, autoClose: true, emitClose: true }; + if (options?.mtime) { + const mtime = options.mtime; + // inject futimes call as part of close + createWriteOptions.fs = { + open: openFd, + write: writeFd, + writev: writevFd, + close: (fd: number, callback: NoParamCallback) => { + futimes(fd, new Date(), mtime, (futimesErr) => { + close(fd, (closeErr) => { + callback(futimesErr || closeErr); + }); + }); + } + }; + } + + return createWriteStream(uri.fsPath, createWriteOptions); + } + + async openFile(uri: Uri): Promise { + return new LocalReadHandle(await open(uri.fsPath, 'r')); + } +} + +class LocalReadHandle extends ReadHandle { + constructor(private handle: FileHandle) { + super(); + } + + read(buffer: TBuffer, offset = 0, length = buffer.byteLength, position: number | null = null): Promise<{ bytesRead: number; buffer: TBuffer; }> { + return this.handle.read(buffer, offset, length, position); + } + + async size(): Promise { + const stat = await this.handle.stat(); + return stat.size; + } + + async close() { + return this.handle.close(); + } +} diff --git a/ce/ce/fs/streams.ts b/ce/ce/fs/streams.ts new file mode 100644 index 0000000000..16da8cb92a --- /dev/null +++ b/ce/ce/fs/streams.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import EventEmitter = require('events'); +import { Transform, TransformCallback } from 'stream'; +import { Stopwatch } from '../util/channels'; +import { PercentageScaler } from '../util/percentage-scaler'; + +export interface Progress { + progress(percent: number, bytes: number, msec: number): void; +} + +export interface ProgressTrackingEvents extends EventEmitter { + on(event: 'progress', callback: (progress: number, currentPosition: number, msec: number) => void): this; +} + +export class ProgressTrackingStream extends Transform implements ProgressTrackingEvents { + private readonly stopwatch = new Stopwatch; + private readonly scaler: PercentageScaler; + private currentPosition: number; + + constructor(start: number, end: number) { + super(); + this.scaler = new PercentageScaler(start, end); + this.currentPosition = start; + } + + override _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void { + if (encoding !== 'buffer') { + return callback(new Error('unexpected chunk type')); + } + + const chunkBuffer = chunk; + this.currentPosition += chunkBuffer.byteLength; + this.emit('progress', this.scaler.scalePosition(this.currentPosition), this.currentPosition, this.stopwatch.total); + return callback(null, chunk); + } + + get currentPercentage() { + return this.scaler.scalePosition(this.currentPosition); + } +} diff --git a/ce/ce/fs/unified-filesystem.ts b/ce/ce/fs/unified-filesystem.ts new file mode 100644 index 0000000000..c6b7257bf4 --- /dev/null +++ b/ce/ce/fs/unified-filesystem.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { Readable, Writable } from 'stream'; +import { i } from '../i18n'; +import { Dictionary } from '../util/linq'; +import { Uri } from '../util/uri'; +import { FileStat, FileSystem, FileType, ReadHandle, WriteStreamOptions } from './filesystem'; + +/** + * gets the scheme off the front of an uri. + * @param uri the uri to get the scheme for. + * @returns the scheme, undefined if the uri has no scheme (colon) + */ +export function schemeOf(uri: string) { + strict.ok(uri, i`Uri may not be empty`); + return /^(\w*):/.exec(uri)?.[1]; +} + +export class UnifiedFileSystem extends FileSystem { + + private filesystems: Dictionary = {}; + + /** registers a scheme to a given filesystem + * + * @param scheme the Uri scheme to reserve + * @param fileSystem the filesystem to associate with the scheme + */ + register(scheme: string | Array, fileSystem: FileSystem) { + if (Array.isArray(scheme)) { + for (const each of scheme) { + this.register(each, fileSystem); + } + return this; + } + strict.ok(!this.filesystems[scheme], i`scheme '${scheme}' already registered`); + this.filesystems[scheme] = fileSystem; + return this; + } + + /** + * gets the filesystem for the given uri. + * + * @param uri the uri to check the filesystem for + * + * @returns the filesystem. Will throw if no filesystem is valid. + */ + public filesystem(uri: string | Uri) { + const scheme = schemeOf(uri.toString()); + + strict.ok(scheme, i`uri ${uri.toString()} has no scheme`); + + const filesystem = this.filesystems[scheme]; + strict.ok(filesystem, i`scheme ${scheme} has no filesystem associated with it`); + + return filesystem; + } + + /** + * Creates a new URI from a string, e.g. `https://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @param uri A string which represents an URI (see `URI#toString`). + */ + override parse(uri: string, _strict?: boolean): Uri { + return this.filesystem(uri).parse(uri); + } + + + stat(uri: Uri): Promise { + return this.filesystem(uri).stat(uri); + } + + async readDirectory(uri: Uri, options?: { recursive?: boolean }): Promise> { + return this.filesystem(uri).readDirectory(uri, options); + } + + createDirectory(uri: Uri): Promise { + return this.filesystem(uri).createDirectory(uri); + } + + readFile(uri: Uri): Promise { + return this.filesystem(uri).readFile(uri); + } + + openFile(uri: Uri): Promise { + return this.filesystem(uri).openFile(uri); + } + + writeFile(uri: Uri, content: Uint8Array): Promise { + return this.filesystem(uri).writeFile(uri, content); + } + + readStream(uri: Uri, options?: { start?: number, end?: number }): Promise { + return this.filesystem(uri).readStream(uri, options); + } + + writeStream(uri: Uri, options?: WriteStreamOptions): Promise { + return this.filesystem(uri).writeStream(uri, options); + } + + delete(uri: Uri, options?: { recursive?: boolean | undefined; useTrash?: boolean | undefined; }): Promise { + return this.filesystem(uri).delete(uri, options); + } + + rename(source: Uri, target: Uri, options?: { overwrite?: boolean | undefined; }): Promise { + strict.ok(source.fileSystem === target.fileSystem, i`may not rename across filesystems`); + return source.fileSystem.rename(source, target, options); + } + + copy(source: Uri, target: Uri, options?: { overwrite?: boolean | undefined; }): Promise { + return target.fileSystem.copy(source, target); + } + + createSymlink(original: Uri, symlink: Uri): Promise { + return symlink.fileSystem.createSymlink(original, symlink); + } +} diff --git a/ce/ce/fs/vsix-local-filesystem.ts b/ce/ce/fs/vsix-local-filesystem.ts new file mode 100644 index 0000000000..6ef03d492c --- /dev/null +++ b/ce/ce/fs/vsix-local-filesystem.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { LocalFileSystem } from './local-filesystem'; + +export class VsixLocalFilesystem extends LocalFileSystem { + private readonly vsixBaseUri: Uri | undefined; + + constructor(session: Session) { + super(session); + const programData = session.environment['ProgramData']; + if (programData) { + this.vsixBaseUri = this.file(programData).join('Microsoft/VisualStudio/Packages'); + } + } + + /** + * Creates a new URI from a string, e.g. `https://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @param value A string which represents an URI (see `URI#toString`). + */ + override parse(value: string, _strict?: boolean): Uri { + return Uri.parseFilterVsix(this, value, _strict, this.vsixBaseUri); + } +} diff --git a/ce/ce/i18n.ts b/ce/ce/i18n.ts new file mode 100644 index 0000000000..d47613d019 --- /dev/null +++ b/ce/ce/i18n.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { join } from 'path'; + +/** what a language map looks like. */ +interface language { + [key: string]: (...args: Array) => string; +} + +type PrimitiveValue = string | number | boolean | undefined | Date; + +let translatorModule: language | undefined = undefined; + +function loadTranslatorModule(newLocale: string, basePath?: string) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return (require(join(basePath || `${__dirname}/../i18n`, newLocale.toLowerCase())).map); +} + +export function setLocale(newLocale: string, basePath?: string) { + try { + translatorModule = loadTranslatorModule(newLocale, basePath); + } catch { + // translation did not load. + // let's try to trim the locale and see if it fits + const l = newLocale.lastIndexOf('-'); + if (l > -1) { + try { + const localeFiltered = newLocale.substr(0, l); + translatorModule = loadTranslatorModule(localeFiltered, basePath); + } catch { + // intentionally fall down to undefined setting below + } + } + + // fallback to no translation + translatorModule = undefined; + } +} + +/** + * processes a TaggedTemplateLiteral to return either: + * - a template string with numbered placeholders + * - or to resolve the template with the values given. + * + * @param literals The templateStringsArray from the templateFunction + * @param values the values from the template Function + * @param formatter an optional formatter (formats to ${##} if not specified) + */ +function normalize(literals: TemplateStringsArray, values: Array, formatter?: (value: PrimitiveValue) => string) { + const content = formatter ? literals.flatMap((k, i) => [k, formatter(values[i])]) : literals.flatMap((k, i) => [k, `$\{${i}}`]); + content.length--; // drop the trailing undefined. + return content.join(''); +} + +/** + * Support for tagged template literals for i18n. + * + * Leverages translation files in ../i18n + * + * @param literals the literal values in the tagged template + * @param values the inserted values in the template + * + * @translator + */ +export function i(literals: TemplateStringsArray, ...values: Array) { + // if the language has no translation, use the default content. + if (!translatorModule) { + return normalize(literals, values, (content) => `${content}`); + } + // use the translator module, but fallback to no translation if the file doesn't have a translation. + const fn = translatorModule[normalize(literals, values)]; + return fn ? fn(...values) : normalize(literals, values, (content) => `${content}`); +} diff --git a/ce/ce/insights.ts b/ce/ce/insights.ts new file mode 100644 index 0000000000..86a9c9ddd2 --- /dev/null +++ b/ce/ce/insights.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +import { defaultClient, DistributedTracingModes, setup } from 'applicationinsights'; +import { createHash } from 'crypto'; +import { session } from './main'; +import { Version } from './version'; + +process.env['APPLICATION_INSIGHTS_NO_STATSBEAT'] = 'true'; +export const insights = setup('b4e88960-4393-4dd9-ab8e-97e8fe6d7603'). + setAutoCollectConsole(false). + setAutoCollectDependencies(false). + setAutoCollectExceptions(false). + setAutoCollectHeartbeat(false). + setAutoCollectPerformance(false). + setAutoCollectPreAggregatedMetrics(false). + setAutoCollectRequests(false). + setAutoDependencyCorrelation(false). + setDistributedTracingMode(DistributedTracingModes.AI). + setInternalLogging(false). + setSendLiveMetrics(false). + setUseDiskRetryCaching(false). + start(); + +defaultClient.context.keys.applicationVersion = Version; + + +// todo: This will be refactored to allow appInsights to be called out-of-proc from the main process. +// in order to not potentially slow down or block on activation/etc. + +export function flushTelemetry() { + session.channels.debug('Ensuring Telemetry data is finished sending.'); + defaultClient.flush({}); +} + +defaultClient.addTelemetryProcessor((envelope, contextObjects) => { + if (session.context['printmetrics']) { + session.channels.message(`Telemetry Event: \n${JSON.stringify(envelope.data, null, 2)}`); + } + + // only actually send telemetry if it's enabled. + return session.telemetryEnabled; +}); + +export function trackEvent(name: string, properties: { [key: string]: string } = {}) { + session.channels.debug(`Triggering Telemetry Event ce.${name}`); + + defaultClient.trackEvent({ + name: `ce/${name}`, + time: new Date(), + + properties: { + ...properties, + } + }); +} + +export function trackActivation() { + return trackEvent('activate', {}); +} + +export function trackAcquire(artifactId: string, artifactVersion: string) { + return trackEvent('acquire', { + 'artifactId': createHash('sha256').update(artifactId, 'ascii').digest('hex'), + 'artifactVersion': artifactVersion + }); +} diff --git a/ce/ce/installers/git.ts b/ce/ce/installers/git.ts new file mode 100644 index 0000000000..f9c3f7fcae --- /dev/null +++ b/ce/ce/installers/git.ts @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CloneOptions, Git } from '../archivers/git'; +import { Activation } from '../artifacts/activation'; +import { i } from '../i18n'; +import { InstallEvents, InstallOptions } from '../interfaces/events'; +import { CloneSettings, GitInstaller } from '../interfaces/metadata/installers/git'; +import { Session } from '../session'; +import { linq } from '../util/linq'; +import { Uri } from '../util/uri'; + +export async function installGit(session: Session, activation: Activation, name: string, targetLocation: Uri, install: GitInstaller, events: Partial, options: Partial): Promise { + const gitPath = linq.find(activation.tools, 'git'); + + if (!gitPath) { + throw new Error(i`Git is not installed`); + } + + const repo = session.parseUri(install.location); + const targetDirectory = targetLocation.join(options.subdirectory ?? ''); + + const gitTool = new Git(session, gitPath, activation.environmentBlock, targetDirectory); + + await gitTool.clone(repo, events, { + recursive: install.recurse, + depth: install.full ? undefined : 1, + }); + + if (install.commit) { + if (install.full) { + await gitTool.reset(events, { + commit: install.commit, + recurse: install.recurse, + hard: true + }); + } + else { + await gitTool.fetch('origin', events, { + commit: install.commit, + recursive: install.recurse, + depth: install.full ? undefined : 1 + }); + await gitTool.checkout(events, { + commit: install.commit + }); + } + } +} diff --git a/ce/ce/installers/nuget.ts b/ce/ce/installers/nuget.ts new file mode 100644 index 0000000000..da4c712d2b --- /dev/null +++ b/ce/ce/installers/nuget.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ZipUnpacker } from '../archivers/ZipUnpacker'; +import { Activation } from '../artifacts/activation'; +import { acquireNugetFile } from '../fs/acquire'; +import { InstallEvents, InstallOptions } from '../interfaces/events'; +import { NupkgInstaller } from '../interfaces/metadata/installers/nupkg'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { applyAcquireOptions } from './util'; + +export async function installNuGet(session: Session, activation: Activation, name: string, targetLocation: Uri, install: NupkgInstaller, events: Partial, options: Partial): Promise { + const file = await acquireNugetFile(session, install.location, `${name}.zip`, events, applyAcquireOptions(options, install)); + + return new ZipUnpacker(session).unpack( + file, + targetLocation, + events, + { + strip: install.strip, + transform: [...install.transform], + }); +} diff --git a/ce/ce/installers/untar.ts b/ce/ce/installers/untar.ts new file mode 100644 index 0000000000..3224a69b97 --- /dev/null +++ b/ce/ce/installers/untar.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TarBzUnpacker, TarGzUnpacker, TarUnpacker } from '../archivers/tar'; +import { Unpacker } from '../archivers/unpacker'; +import { Activation } from '../artifacts/activation'; +import { acquireArtifactFile } from '../fs/acquire'; +import { InstallEvents, InstallOptions } from '../interfaces/events'; +import { UnTarInstaller } from '../interfaces/metadata/installers/tar'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { applyAcquireOptions, artifactFileName } from './util'; + + +export async function installUnTar(session: Session, activation: Activation, name: string, targetLocation: Uri, install: UnTarInstaller, events: Partial, options: Partial): Promise { + const file = await acquireArtifactFile(session, [...install.location].map(each => session.parseUri(each)), artifactFileName(name, install, '.tar'), events, applyAcquireOptions(options, install)); + const x = await file.readBlock(0, 128); + let unpacker: Unpacker; + if (x[0] === 0x1f && x[1] === 0x8b) { + unpacker = new TarGzUnpacker(session); + } else if (x[0] === 66 && x[1] === 90) { + unpacker = new TarBzUnpacker(session); + } else { + unpacker = new TarUnpacker(session); + } + + return unpacker.unpack(file, targetLocation, events, { strip: install.strip, transform: [...install.transform] }); +} diff --git a/ce/ce/installers/unzip.ts b/ce/ce/installers/unzip.ts new file mode 100644 index 0000000000..4201f19e37 --- /dev/null +++ b/ce/ce/installers/unzip.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ZipUnpacker } from '../archivers/ZipUnpacker'; +import { Activation } from '../artifacts/activation'; +import { acquireArtifactFile } from '../fs/acquire'; +import { InstallEvents, InstallOptions } from '../interfaces/events'; +import { UnZipInstaller } from '../interfaces/metadata/installers/zip'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { applyAcquireOptions, artifactFileName } from './util'; + +export async function installUnZip(session: Session, activation: Activation, name: string, targetLocation: Uri, install: UnZipInstaller, events: Partial, options: Partial): Promise { + const file = await acquireArtifactFile(session, [...install.location].map(each => session.parseUri(each)), artifactFileName(name, install, '.zip'), events, applyAcquireOptions(options, install)); + await new ZipUnpacker(session).unpack( + file, + targetLocation, + events, + { + strip: install.strip, + transform: [...install.transform], + }); +} diff --git a/ce/ce/installers/util.ts b/ce/ce/installers/util.ts new file mode 100644 index 0000000000..60e46f7359 --- /dev/null +++ b/ce/ce/installers/util.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { AcquireOptions } from '../fs/acquire'; +import { Installer } from '../interfaces/metadata/installers/Installer'; +import { Verifiable } from '../interfaces/metadata/installers/verifiable'; + +export function artifactFileName(name: string, install: Installer, extension: string): string { + let result = name; + if (install.nametag) { + result += '-'; + result += install.nametag; + } + + if (install.lang) { + result += '-'; + result += install.lang; + } + + result += extension; + return result.replace(/[^\w]+/g, '.'); +} + +export function applyAcquireOptions(options: AcquireOptions, install: Verifiable): AcquireOptions { + if (install.sha256) { + return { ...options, algorithm: 'sha256', value: install.sha256 }; + } + if (install.sha512) { + return { ...options, algorithm: 'sha512', value: install.sha512 }; + } + + return options; +} + diff --git a/ce/ce/interfaces/collections.ts b/ce/ce/interfaces/collections.ts new file mode 100644 index 0000000000..faad3cc450 --- /dev/null +++ b/ce/ce/interfaces/collections.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export type Range = [number, number, number]; + +export interface Dictionary extends Iterable<[string, T]> { + clear(): void; + delete(key: string): boolean; + get(key: string): T | undefined; + has(key: string): boolean; + add(key: string): T; + sourcePosition(key: string): Range | undefined; + readonly length: number; + readonly keys: Array; +} + +export interface Sequence extends Iterable { + [Symbol.iterator](): Iterator; + readonly length: number; + clear(): void; +} + +export interface Strings extends Sequence { + get(index: number): string | undefined; + delete(val: string | Array): void; +} \ No newline at end of file diff --git a/ce/ce/interfaces/error-kind.ts b/ce/ce/interfaces/error-kind.ts new file mode 100644 index 0000000000..c8acaa9f30 --- /dev/null +++ b/ce/ce/interfaces/error-kind.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export enum ErrorKind { + SectionNotFound = 'SectionMessing', + FieldMissing = 'FieldMissing', + IncorrectType = 'IncorrectType', + ParseError = 'ParseError', + DuplicateKey = 'DuplicateKey', + NoInstallInDemand = 'NoInstallInDemand', + HostOnly = 'HostOnly', + MissingHash = 'MissingHashValue', + InvalidDefinition = 'InvalidDefinition', +} diff --git a/ce/ce/interfaces/events.ts b/ce/ce/interfaces/events.ts new file mode 100644 index 0000000000..c49caaed3a --- /dev/null +++ b/ce/ce/interfaces/events.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Uri } from '../util/uri'; + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +export interface Progress { + progress(percent: number, bytes: number, msec: number): void; +} + +export interface AcquireEvents extends Progress { + download(file: string, percent: number): void; + verifying(file: string, percent: number): void; + complete(): void; +} + +/** The event definitions for for unpackers */ +export interface UnpackEvents { + progress(archivePercentage: number): void; + fileProgress(entry: Readonly, filePercentage: number): void; + unpacked(entry: Readonly): void; + error(entry: Readonly, message: string): void; +} + +export interface FileEntry { + archiveUri: Uri; + destination: Uri | undefined; + path: string; + extractPath: string | undefined; +} + +export interface InstallEvents { + // download/verifying + download(file: string, percent: number): void; + verifying(file: string, percent: number): void; + + // unpacking individual files + fileProgress(entry: Readonly, filePercentage: number): void; + unpacked(entry: Readonly): void; + error(entry: Readonly, message: string): void; + + // message when we have no ability to give a linear progress + heartbeat(text: string): void; + + // overall progress events + start(): void; + progress(archivePercentage: number): void; + complete(): void; +} + +export interface InstallOptions { + force?: boolean, + allLanguages?: boolean, + language?: string +} \ No newline at end of file diff --git a/ce/ce/interfaces/metadata/Settings.ts b/ce/ce/interfaces/metadata/Settings.ts new file mode 100644 index 0000000000..afbfd8fadb --- /dev/null +++ b/ce/ce/interfaces/metadata/Settings.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Dictionary, Strings } from '../collections'; +import { Validation } from '../validation'; + +/** settings that should be applied to the context */ + +export interface Settings extends Validation { + /** a map of path categories to one or more values */ + paths: Dictionary; + + /** a map of the known tools to actual tool executable name */ + tools: Dictionary; + + /** + * a map of (environment) variables that should be set in the context. + * + * arrays mean that the values should be joined with spaces + */ + variables: Dictionary; + + /** a map of properties that are activation-type specific (ie, msbuild) */ + properties: Dictionary; + + /** a map of locations that are activation-type specific (ie, msbuild) */ + locations: Dictionary; + + // this is where we'd see things like + // CFLAGS: [...] where you can have a bunch of things that would end up in the CFLAGS variable (or used to set values in a vcxproj/cmake settings file.) + // + /** + * a map of #defines for the artifact. + * + * these would likely also be turned into 'variables', but + * it's significant enough that we need them separately + */ + defines: Dictionary; +} diff --git a/ce/ce/interfaces/metadata/alternative-fulfillment.ts b/ce/ce/interfaces/metadata/alternative-fulfillment.ts new file mode 100644 index 0000000000..bfcc6ee0a1 --- /dev/null +++ b/ce/ce/interfaces/metadata/alternative-fulfillment.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Strings } from '../collections'; +import { Demands } from './demands'; +import { Settings } from './Settings'; + + +export interface AlternativeFulfillment extends Demands { + /** places to look for an executable file */ + from: Strings; + + /** executable names */ + where: Strings; + + /** command line to run to verify the executable */ + run: string | undefined; + + /** filter to apply to the output */ + select: string | undefined; + + /** the expression to match the selected output with */ + matches: string | undefined; + + /** settings that should be applied to the context when activated if this is a match */ + settings: Settings; +} diff --git a/ce/ce/interfaces/metadata/contact.ts b/ce/ce/interfaces/metadata/contact.ts new file mode 100644 index 0000000000..30440e4b9d --- /dev/null +++ b/ce/ce/interfaces/metadata/contact.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Strings } from '../collections'; +import { Validation } from '../validation'; + +/** A person/organization/etc who either has contributed or is connected to the artifact */ +export interface Contact extends Validation { + email?: string; + readonly roles: Strings; +} diff --git a/ce/ce/interfaces/metadata/demands.ts b/ce/ce/interfaces/metadata/demands.ts new file mode 100644 index 0000000000..90621b8379 --- /dev/null +++ b/ce/ce/interfaces/metadata/demands.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +import { Activation } from '../../artifacts/activation'; +import { Session } from '../../session'; +import { Dictionary, Sequence } from '../collections'; +import { Validation } from '../validation'; +import { AlternativeFulfillment } from './alternative-fulfillment'; +import { Installer } from './installers/Installer'; +import { Settings } from './Settings'; +import { VersionReference } from './version-reference'; + +/** + * These are the things that are necessary to install/set/depend-on/etc for a given 'artifact' + */ + +export interface Demands extends Validation { + /** set of required artifacts */ + requires: Dictionary; + + /** An error message that the user should get, and abort the installation */ + error: string | undefined; // markdown text with ${} replacements + + + /** A warning message that the user should get, does not abort the installation */ + warning: string | undefined; // markdown text with ${} replacements + + + /** A text message that the user should get, does not abort the installation */ + message: string | undefined; // markdown text with ${} replacements + + + /** set of artifacts that the consumer should be aware of */ + seeAlso: Dictionary; + + /** settings that should be applied to the context when activated */ + settings: Settings; + + /** + * defines what should be physically laid out on disk for this artifact + * + * Note: once the host/environment queries have been completed, there should + * only be one single package/file/repo/etc that gets downloaded and + * installed for this artifact. If there needs to be more than one, + * then there would need to be a 'requires' that refers to the additional + * package. + */ + install: Sequence; + + /** a means to an alternative fulfillment */ + unless: AlternativeFulfillment; + + init(session: Session): Promise; + setActivation(activation: Activation): void; +} + diff --git a/ce/ce/interfaces/metadata/info.ts b/ce/ce/interfaces/metadata/info.ts new file mode 100644 index 0000000000..83a6499b3f --- /dev/null +++ b/ce/ce/interfaces/metadata/info.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Validation } from '../validation'; + +/** Canonical Information about this artifact */ + +export interface Info extends Validation { + /** Artifact identity + * + * this should be the 'path' to the artifact (following the guidelines) + * + * ie, 'compilers/microsoft/msvc' + * + * FYI: artifacts install to $VCPKG_ROOT// or if from another artifact source: $VCPKG_ROOT/// + */ + id: string; + + /** the version of this artifact */ + version: string; + + /** a short 1 line descriptive text */ + summary?: string; + + /** if a longer description is required, the value should go here */ + description?: string; + + /** if true, intended to be used only as a dependency; for example, do not show in search results or lists */ + dependencyOnly: boolean; + + /** higher priority artifacts should install earlier */ + priority?: number; +} diff --git a/ce/ce/interfaces/metadata/installers/Installer.ts b/ce/ce/interfaces/metadata/installers/Installer.ts new file mode 100644 index 0000000000..d522f9230b --- /dev/null +++ b/ce/ce/interfaces/metadata/installers/Installer.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Validation } from '../../validation'; + +/** + * defines what should be physically laid out on disk for this artifact + * + * Note: once the host/environment queries have been completed, there should + * only be one single package/file/repo/etc that gets downloaded and + * installed for this artifact. If there needs to be more than one, + * then there would need to be a 'requires' that refers to the additional + * package. + * + * More types to follow. + */ + +export interface Installer extends Validation { + readonly installerKind: string; + readonly lang?: string; // note to only install this entry when the current locale is this language + readonly nametag?: string; // note to include this tag in the file name of the cached artifact +} diff --git a/ce/ce/interfaces/metadata/installers/git.ts b/ce/ce/interfaces/metadata/installers/git.ts new file mode 100644 index 0000000000..537319b5cd --- /dev/null +++ b/ce/ce/interfaces/metadata/installers/git.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Installer } from './Installer'; + +export interface CloneSettings { + /** optionally, a tag/branch to be checkout out */ + commit?: string; + + /** + * determines if the whole repo is cloned. + * + * Note: + * - when false (default), indicates that the repo should be cloned with --depth 1 + * - when true, indicates that the full repo should be cloned + * */ + full?: boolean; + + /** + * determines if the repo should be cloned recursively. + * + * Note: + * - when false (default), indicates that the repo should clone recursive submodules + * - when true, indicates that the repo should be cloned recursively. + */ + recurse?: boolean; + + /** + * Gives a subdirectory to clone the repo to, if given. + */ + subdirectory?: string; +} + +/** + * Installer that clones a git repository + */ +export interface GitInstaller extends Installer, CloneSettings { + /** the git repo location to be cloned */ + location: string; + + /** + * determines if the thing being installed is esp-idf, and if so, it should do some other installations after + * the git install. + */ + espidf?: boolean; +} diff --git a/ce/ce/interfaces/metadata/installers/nupkg.ts b/ce/ce/interfaces/metadata/installers/nupkg.ts new file mode 100644 index 0000000000..ebcc7b7f80 --- /dev/null +++ b/ce/ce/interfaces/metadata/installers/nupkg.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Installer } from './Installer'; +import { UnpackSettings } from './unpack-settings'; +import { Verifiable } from './verifiable'; + +/** + * a special version of UnZip, this assumes the nuget.org package service + * the 'nupkg' value is the package id (ie, 'Microsoft.Windows.SDK.CPP.x64/10.0.19041.5') + * + * and that is appended to the known-url https://www.nuget.org/api/v2/package/ to get + * the final url. + * + * post MVP we could add the ability to use artifact sources and grab the package that way. + * + * combined with Verifiable, the hash should be matched before proceeding + */ + +export interface NupkgInstaller extends Verifiable, UnpackSettings, Installer { + /** the source location of a file to unzip/untar/unrar/etc */ + location: string; +} diff --git a/ce/ce/interfaces/metadata/installers/tar.ts b/ce/ce/interfaces/metadata/installers/tar.ts new file mode 100644 index 0000000000..5f6336d48a --- /dev/null +++ b/ce/ce/interfaces/metadata/installers/tar.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Strings } from '../../collections'; +import { Installer } from './Installer'; +import { UnpackSettings } from './unpack-settings'; +import { Verifiable } from './verifiable'; + +/** + * a file that can be untar'd + * + * combined with Verifiable, the hash should be matched before proceeding + */ + +export interface UnTarInstaller extends Verifiable, UnpackSettings, Installer { + /** the source location of a file to untar */ + location: Strings; +} diff --git a/ce/ce/interfaces/metadata/installers/unpack-settings.ts b/ce/ce/interfaces/metadata/installers/unpack-settings.ts new file mode 100644 index 0000000000..bfd7e6f11f --- /dev/null +++ b/ce/ce/interfaces/metadata/installers/unpack-settings.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Strings } from '../../collections'; + + +export interface UnpackSettings { + /** a number of levels of directories to strip off the front of the file names in the archive when restoring (think tar --strip 1) */ + strip?: number; + + /** one or more transform strings to apply to the filenames as they are restored (think tar --xform ... ) */ + transform: Strings; +} diff --git a/ce/ce/interfaces/metadata/installers/verifiable.ts b/ce/ce/interfaces/metadata/installers/verifiable.ts new file mode 100644 index 0000000000..7fe98940eb --- /dev/null +++ b/ce/ce/interfaces/metadata/installers/verifiable.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** One of several choices for a HASH etc */ + +export interface Verifiable { + /** SHA-256 hash */ + sha256?: string; + sha512?: string; +} diff --git a/ce/ce/interfaces/metadata/installers/zip.ts b/ce/ce/interfaces/metadata/installers/zip.ts new file mode 100644 index 0000000000..76c2a46b20 --- /dev/null +++ b/ce/ce/interfaces/metadata/installers/zip.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Strings } from '../../collections'; +import { Installer } from './Installer'; +import { UnpackSettings } from './unpack-settings'; +import { Verifiable } from './verifiable'; + +/** + * a file that can be unzipp'd + * + * combined with Verifiable, the hash should be matched before proceeding + */ + +export interface UnZipInstaller extends Verifiable, UnpackSettings, Installer { + /** the source location of a file to unzip */ + location: Strings; +} diff --git a/ce/ce/interfaces/metadata/metadata-format.ts b/ce/ce/interfaces/metadata/metadata-format.ts new file mode 100644 index 0000000000..9e7b1b8e47 --- /dev/null +++ b/ce/ce/interfaces/metadata/metadata-format.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ProfileBase } from './profile-base'; +import { GitRegistry } from './registries/git-registry'; +import { LocalRegistry } from './registries/local-registry'; +import { NugetRegistry } from './registries/nuget-registry'; + + +/** + * a profile defines the requirements and/or artifact that should be installed + * + * Any other keys are considered HostQueries and a matching set of Demands + * A HostQuery is a query string that can be used to qualify + * 'requires'/'see-also'/'settings'/'install'/'use' objects + */ +export type Profile = ProfileBase; + +export type RegistryDeclaration = NugetRegistry | LocalRegistry | GitRegistry; + +/** values that can be either a single string, or an array of strings */ +export type StringOrStrings = string | Array; diff --git a/ce/ce/interfaces/metadata/paths.ts b/ce/ce/interfaces/metadata/paths.ts new file mode 100644 index 0000000000..01c119545f --- /dev/null +++ b/ce/ce/interfaces/metadata/paths.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Dictionary, Strings } from '../collections'; + +/** + * types of paths that we can handle when crafting the context + * + * Paths has a well-known list of path types that we handle, but we make it a dictionary anyway. + */ + +export interface Paths extends Dictionary { + /** entries that should be added to the PATH environment variable */ + bin: Strings; + + /** entries that should be in the INCLUDE environment variable */ + include: Strings; + + /** entries that should be in the LIB environment variable */ + lib: Strings; + + /** entries that should be used for GCC's LDSCRIPT */ + ldscript: Strings; + + /** object files that should be linked */ + object: Strings; +} diff --git a/ce/ce/interfaces/metadata/profile-base.ts b/ce/ce/interfaces/metadata/profile-base.ts new file mode 100644 index 0000000000..6e4e6d4df6 --- /dev/null +++ b/ce/ce/interfaces/metadata/profile-base.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Dictionary } from '../collections'; +import { Contact } from './contact'; +import { Demands } from './demands'; +import { Info } from './info'; +import { RegistryDeclaration } from './metadata-format'; + + +type Primitive = string | number | boolean; + +/** + * a profile defines the requirements and/or artifact that should be installed + * + * Any other keys are considered HostQueries and a matching set of Demands + * A HostQuery is a query string that can be used to qualify + * 'requires'/'see-also'/'settings'/'install'/'use' objects + * + * @see the section below in this document entitled 'Host/Environment Queries" + */ + +export interface ProfileBase extends Demands { + /** this profile/package information/metadata */ + info: Info; + + /** any contact information related to this profile/package */ + contacts: Dictionary; // optional + + /** artifact registries list the references necessary to install artifacts in this file */ + registries?: Dictionary; + + /** global settings */ + globalSettings: Dictionary>; + + /** is this document valid */ + readonly isFormatValid: boolean; + + /** parsing errors in this document */ + readonly formatErrors: Array; + + /** does the document pass validation checks? */ + readonly isValid: boolean; + + /** what are the valiation check errors? */ + readonly validationErrors: Array; +} diff --git a/ce/ce/interfaces/metadata/registries/artifact-registry.ts b/ce/ce/interfaces/metadata/registries/artifact-registry.ts new file mode 100644 index 0000000000..d7da7d4ec8 --- /dev/null +++ b/ce/ce/interfaces/metadata/registries/artifact-registry.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Strings } from '../../collections'; +import { Validation } from '../../validation'; + +export interface Registry { + readonly registryKind?: string; +} + +export interface ArtifactRegistry extends Registry, Validation { + /** the uri to the artifact source location */ + readonly location: Strings; +} diff --git a/ce/ce/interfaces/metadata/registries/git-registry.ts b/ce/ce/interfaces/metadata/registries/git-registry.ts new file mode 100644 index 0000000000..78b044bcf7 --- /dev/null +++ b/ce/ce/interfaces/metadata/registries/git-registry.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ArtifactRegistry } from './artifact-registry'; + + +export interface GitRegistry extends ArtifactRegistry { +} diff --git a/ce/ce/interfaces/metadata/registries/local-registry.ts b/ce/ce/interfaces/metadata/registries/local-registry.ts new file mode 100644 index 0000000000..8e3257319d --- /dev/null +++ b/ce/ce/interfaces/metadata/registries/local-registry.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ArtifactRegistry } from './artifact-registry'; + + +export interface LocalRegistry extends ArtifactRegistry { +} diff --git a/ce/ce/interfaces/metadata/registries/nuget-registry.ts b/ce/ce/interfaces/metadata/registries/nuget-registry.ts new file mode 100644 index 0000000000..b76ddde052 --- /dev/null +++ b/ce/ce/interfaces/metadata/registries/nuget-registry.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ArtifactRegistry } from './artifact-registry'; + + +export interface NugetRegistry extends ArtifactRegistry { +} diff --git a/ce/ce/interfaces/metadata/version-reference.ts b/ce/ce/interfaces/metadata/version-reference.ts new file mode 100644 index 0000000000..a192ceaa96 --- /dev/null +++ b/ce/ce/interfaces/metadata/version-reference.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Range, SemVer } from 'semver'; +import { Validation } from '../validation'; + + +export interface VersionReference extends Validation { + range: Range; + resolved?: SemVer; + readonly raw?: string; +} diff --git a/ce/ce/interfaces/validation-error.ts b/ce/ce/interfaces/validation-error.ts new file mode 100644 index 0000000000..2bf7dbf600 --- /dev/null +++ b/ce/ce/interfaces/validation-error.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ErrorKind } from './error-kind'; + + +export interface ValidationError { + message: string; + range?: [number, number, number] | { sourcePosition(): [number, number, number] | undefined }; + rangeOffset?: { line: number; column: number; }; + category: ErrorKind; +} diff --git a/ce/ce/interfaces/validation.ts b/ce/ce/interfaces/validation.ts new file mode 100644 index 0000000000..7689fdea6b --- /dev/null +++ b/ce/ce/interfaces/validation.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ValidationError } from './validation-error'; + + +export interface Validation { + /** + * @internal + * + * actively validate this node. + */ + validate(): Iterable; +} diff --git a/ce/ce/main.ts b/ce/ce/main.ts new file mode 100644 index 0000000000..5e43509ddf --- /dev/null +++ b/ce/ce/main.ts @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { green, white } from 'chalk'; +import { spawn } from 'child_process'; +import { argv } from 'process'; +import { CommandLine } from './cli/command-line'; +import { AcquireCommand } from './cli/commands/acquire'; +import { ActivateCommand } from './cli/commands/activate'; +import { AddCommand } from './cli/commands/add'; +import { ApplyVsManCommand } from './cli/commands/apply-vsman'; +import { CacheCommand } from './cli/commands/cache'; +import { CleanCommand } from './cli/commands/clean'; +import { DeactivateCommand } from './cli/commands/deactivate'; +import { DeleteCommand } from './cli/commands/delete'; +import { FindCommand } from './cli/commands/find'; +import { HelpCommand } from './cli/commands/help'; +import { ListCommand } from './cli/commands/list'; +import { NewCommand } from './cli/commands/new'; +import { RegenerateCommand } from './cli/commands/regenerate-index'; +import { RemoveCommand } from './cli/commands/remove'; +import { UpdateCommand } from './cli/commands/update'; +import { UseCommand } from './cli/commands/use'; +import { VersionCommand } from './cli/commands/version'; +import { blank, cli, product } from './cli/constants'; +import { command as formatCommand, hint } from './cli/format'; +import { debug, error, initStyling, log } from './cli/styling'; +import { i, setLocale } from './i18n'; +import { flushTelemetry, trackEvent } from './insights'; +import { Session } from './session'; +import { Version as cliVersion } from './version'; + +// parse the command line +const commandline = new CommandLine(argv.slice(2)); + +// try to set the locale based on the users's settings. +setLocale(commandline.language, `${__dirname}/i18n/`); + +function header() { + if (!commandline.fromVCPKG) { + if (commandline.debug) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + log(`${green.bold(`${product} command line utility`)} ${white.bold(cliVersion)} [node: ${white.bold(process.version)}; max-memory: ${white.bold(Math.round((require('v8').getHeapStatistics().heap_size_limit) / (1024 * 1024)) & 0xffffffff00)} gb]`); + } else { + log(`${green.bold(`${product} command line utility`)} ${white.bold(cliVersion)}`); + } + log(''); + } +} + +export let session: Session; +require('./exports'); + +trackEvent; // ensure it's loaded asap. + +async function main() { + + // ensure we can execute commands from this process. + // this works around an odd bug in the way that node handles + // executing child processes where the target is a windows store symlink + spawn(process.argv0, ['--version']); + + // create our session for this process. + session = new Session(process.cwd(), commandline.context, commandline, process.env); + + initStyling(commandline, session); + + // dump out the version information + header(); + + // start up the session and init the channel listeners. + await session.init(); + + const telemetryEnabled = await session.telemetryEnabled; + debug(`Anonymous Telemetry Enabled: ${telemetryEnabled}`); + // find a project profile. + + const zApplyVsMan = new ApplyVsManCommand(commandline); + const help = new HelpCommand(commandline); + + const find = new FindCommand(commandline); + const list = new ListCommand(commandline); + + const add = new AddCommand(commandline); + const acquire = new AcquireCommand(commandline); + const use = new UseCommand(commandline); + + const remove = new RemoveCommand(commandline); + const del = new DeleteCommand(commandline); + + const activate = new ActivateCommand(commandline); + const deactivate = new DeactivateCommand(commandline); + + const newcmd = new NewCommand(commandline); + + const regenerate = new RegenerateCommand(commandline); + const update = new UpdateCommand(commandline); + + const version = new VersionCommand(commandline); + const cache = new CacheCommand(commandline); + const clean = new CleanCommand(commandline); + + debug(`Postscript file ${session.postscriptFile}`); + + const needsHelp = !!(commandline.switches['help'] || commandline.switches['?'] || (['-h', '-help', '-?', '/?'].find(each => argv.includes(each)))); + // check if --help -h -? --? /? are asked for + if (needsHelp) { + // let's just run general help + await help.run(); + return process.exit(0); + } + + const command = commandline.command; + if (!command) { + // no command recognized. + + // did they specify inputs? + if (commandline.inputs.length > 0) { + // unrecognized command + error(i`Unrecognized command '${commandline.inputs[0]}'`); + log(blank); + log(hint(i`Use ${formatCommand(`${cli} ${help.command}`)} to get help`)); + return process.exitCode = 1; + } + + log(blank); + log(hint(i`Use ${formatCommand(`${cli} ${help.command}`)} to get help`)); + + return process.exitCode = 0; + } + let result = true; + try { + result = await command.run(); + log(blank); + + await session.writePostscript(); + } catch (e) { + // in --debug mode we want to see the stack trace(s). + if (commandline.debug && e instanceof Error) { + log(e.stack); + if (e instanceof AggregateError) { + e.errors.forEach(each => log(each.stack)); + } + } + + error(e); + return process.exitCode = 1; + } finally { + flushTelemetry(); + } + return process.exitCode = (result ? 0 : 1); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +main(); diff --git a/ce/ce/mediaquery/character-codes.ts b/ce/ce/mediaquery/character-codes.ts new file mode 100644 index 0000000000..34810f5edd --- /dev/null +++ b/ce/ce/mediaquery/character-codes.ts @@ -0,0 +1,239 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const enum CharacterCodes { + nullCharacter = 0, + maxAsciiCharacter = 0x7F, + + lineFeed = 0x0A, + carriageReturn = 0x0D, + lineSeparator = 0x2028, + paragraphSeparator = 0x2029, + nextLine = 0x0085, + + // Unicode 3.0 space characters + space = 0x0020, + nonBreakingSpace = 0x00A0, + enQuad = 0x2000, + emQuad = 0x2001, + enSpace = 0x2002, + emSpace = 0x2003, + threePerEmSpace = 0x2004, + fourPerEmSpace = 0x2005, + sixPerEmSpace = 0x2006, + figureSpace = 0x2007, + punctuationSpace = 0x2008, + thinSpace = 0x2009, + hairSpace = 0x200A, + zeroWidthSpace = 0x200B, + narrowNoBreakSpace = 0x202F, + ideographicSpace = 0x3000, + mathematicalSpace = 0x205F, + ogham = 0x1680, + + _ = 0x5F, + $ = 0x24, + + _0 = 0x30, + _1 = 0x31, + _2 = 0x32, + _3 = 0x33, + _4 = 0x34, + _5 = 0x35, + _6 = 0x36, + _7 = 0x37, + _8 = 0x38, + _9 = 0x39, + + a = 0x61, + b = 0x62, + c = 0x63, + d = 0x64, + e = 0x65, + f = 0x66, + g = 0x67, + h = 0x68, + i = 0x69, + j = 0x6A, + k = 0x6B, + l = 0x6C, + m = 0x6D, + n = 0x6E, + o = 0x6F, + p = 0x70, + q = 0x71, + r = 0x72, + s = 0x73, + t = 0x74, + u = 0x75, + v = 0x76, + w = 0x77, + x = 0x78, + y = 0x79, + z = 0x7A, + + A = 0x41, + B = 0x42, + C = 0x43, + D = 0x44, + E = 0x45, + F = 0x46, + G = 0x47, + H = 0x48, + I = 0x49, + J = 0x4A, + K = 0x4B, + L = 0x4C, + M = 0x4D, + N = 0x4E, + O = 0x4F, + P = 0x50, + Q = 0x51, + R = 0x52, + S = 0x53, + T = 0x54, + U = 0x55, + V = 0x56, + W = 0x57, + X = 0x58, + Y = 0x59, + Z = 0x5a, + + ampersand = 0x26, + asterisk = 0x2A, + at = 0x40, + backslash = 0x5C, + backtick = 0x60, + bar = 0x7C, + caret = 0x5E, + closeBrace = 0x7D, + closeBracket = 0x5D, + closeParen = 0x29, + colon = 0x3A, + comma = 0x2C, + dot = 0x2E, + doubleQuote = 0x22, + equals = 0x3D, + exclamation = 0x21, + greaterThan = 0x3E, + hash = 0x23, + lessThan = 0x3C, + minus = 0x2D, + openBrace = 0x7B, + openBracket = 0x5B, + openParen = 0x28, + percent = 0x25, + plus = 0x2B, + question = 0x3F, + semicolon = 0x3B, + singleQuote = 0x27, + slash = 0x2F, + tilde = 0x7E, + + backspace = 0x08, + formFeed = 0x0C, + byteOrderMark = 0xFEFF, + tab = 0x09, + verticalTab = 0x0B +} + + +/** Does not include line breaks. For that, see isWhiteSpaceLike. */ +export function isWhiteSpaceSingleLine(ch: number): boolean { + // Note: nextLine is in the Zs space, and should be considered to be a whitespace. + // It is explicitly not a line-break as it isn't in the exact set specified by EcmaScript. + return ch === CharacterCodes.space || + ch === CharacterCodes.tab || + ch === CharacterCodes.verticalTab || + ch === CharacterCodes.formFeed || + ch === CharacterCodes.nonBreakingSpace || + ch === CharacterCodes.nextLine || + ch === CharacterCodes.ogham || + ch >= CharacterCodes.enQuad && ch <= CharacterCodes.zeroWidthSpace || + ch === CharacterCodes.narrowNoBreakSpace || + ch === CharacterCodes.mathematicalSpace || + ch === CharacterCodes.ideographicSpace || + ch === CharacterCodes.byteOrderMark; +} + +export function isLineBreak(ch: number): boolean { + // Other new line or line + // breaking characters are treated as white space but not as line terminators. + return ch === CharacterCodes.lineFeed || + ch === CharacterCodes.carriageReturn || + ch === CharacterCodes.lineSeparator || + ch === CharacterCodes.paragraphSeparator; +} + +export function isDigit(ch: number): boolean { + return ch >= CharacterCodes._0 && ch <= CharacterCodes._9; +} + +export function isHexDigit(ch: number): boolean { + return isDigit(ch) || ch >= CharacterCodes.A && ch <= CharacterCodes.F || ch >= CharacterCodes.a && ch <= CharacterCodes.f; +} + +export function isBinaryDigit(ch: number): boolean { + return ch === CharacterCodes._0 || ch === CharacterCodes._1; +} + +export function isIdentifierStart(ch: number): boolean { + return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || + ch >= CharacterCodes.a && ch <= CharacterCodes.z || + ch === CharacterCodes.$ || ch === CharacterCodes._ || + ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierStart(ch); +} + +export function isIdentifierPart(ch: number,): boolean { + return ch >= CharacterCodes.A && ch <= CharacterCodes.Z || + ch >= CharacterCodes.a && ch <= CharacterCodes.z || + ch >= CharacterCodes._0 && ch <= CharacterCodes._9 || + ch === CharacterCodes.$ || ch === CharacterCodes._ || + ch > CharacterCodes.maxAsciiCharacter && isUnicodeIdentifierPart(ch); +} + +/** Characters that are in this range are actually code points that take two characters in utf16 */ +export function sizeOf(ch: number): number { + return ch >= 0xD800 && ch <= 0xDBFF ? 2 : 1; +} + +function lookupInUnicodeMap(code: number, map: ReadonlyArray): boolean { + // Bail out quickly if it couldn't possibly be in the map. + if (code < map[0]) { + return false; + } + + // Perform binary search in one of the Unicode range maps + let lo = 0; + let hi: number = map.length; + let mid: number; + + while (lo + 1 < hi) { + mid = lo + (hi - lo) / 2; + // mid has to be even to catch a range's beginning + mid -= mid % 2; + if (map[mid] <= code && code <= map[mid + 1]) { + return true; + } + + if (code < map[mid]) { + hi = mid; + } + else { + lo = mid + 2; + } + } + + return false; +} + +const unicodeESNextIdentifierStart = [65, 90, 97, 122, 170, 170, 181, 181, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 880, 884, 886, 887, 890, 893, 895, 895, 902, 902, 904, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1488, 1514, 1519, 1522, 1568, 1610, 1646, 1647, 1649, 1747, 1749, 1749, 1765, 1766, 1774, 1775, 1786, 1788, 1791, 1791, 1808, 1808, 1810, 1839, 1869, 1957, 1969, 1969, 1994, 2026, 2036, 2037, 2042, 2042, 2048, 2069, 2074, 2074, 2084, 2084, 2088, 2088, 2112, 2136, 2144, 2154, 2208, 2228, 2230, 2237, 2308, 2361, 2365, 2365, 2384, 2384, 2392, 2401, 2417, 2432, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2493, 2493, 2510, 2510, 2524, 2525, 2527, 2529, 2544, 2545, 2556, 2556, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2649, 2652, 2654, 2654, 2674, 2676, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2749, 2749, 2768, 2768, 2784, 2785, 2809, 2809, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2877, 2877, 2908, 2909, 2911, 2913, 2929, 2929, 2947, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3024, 3024, 3077, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3133, 3160, 3162, 3168, 3169, 3200, 3200, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3261, 3261, 3294, 3294, 3296, 3297, 3313, 3314, 3333, 3340, 3342, 3344, 3346, 3386, 3389, 3389, 3406, 3406, 3412, 3414, 3423, 3425, 3450, 3455, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3585, 3632, 3634, 3635, 3648, 3654, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3760, 3762, 3763, 3773, 3773, 3776, 3780, 3782, 3782, 3804, 3807, 3840, 3840, 3904, 3911, 3913, 3948, 3976, 3980, 4096, 4138, 4159, 4159, 4176, 4181, 4186, 4189, 4193, 4193, 4197, 4198, 4206, 4208, 4213, 4225, 4238, 4238, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5905, 5920, 5937, 5952, 5969, 5984, 5996, 5998, 6000, 6016, 6067, 6103, 6103, 6108, 6108, 6176, 6264, 6272, 6312, 6314, 6314, 6320, 6389, 6400, 6430, 6480, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6656, 6678, 6688, 6740, 6823, 6823, 6917, 6963, 6981, 6987, 7043, 7072, 7086, 7087, 7098, 7141, 7168, 7203, 7245, 7247, 7258, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7401, 7404, 7406, 7411, 7413, 7414, 7418, 7418, 7424, 7615, 7680, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8305, 8305, 8319, 8319, 8336, 8348, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11502, 11506, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11648, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 12293, 12295, 12321, 12329, 12337, 12341, 12344, 12348, 12353, 12438, 12443, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42527, 42538, 42539, 42560, 42606, 42623, 42653, 42656, 42735, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43009, 43011, 43013, 43015, 43018, 43020, 43042, 43072, 43123, 43138, 43187, 43250, 43255, 43259, 43259, 43261, 43262, 43274, 43301, 43312, 43334, 43360, 43388, 43396, 43442, 43471, 43471, 43488, 43492, 43494, 43503, 43514, 43518, 43520, 43560, 43584, 43586, 43588, 43595, 43616, 43638, 43642, 43642, 43646, 43695, 43697, 43697, 43701, 43702, 43705, 43709, 43712, 43712, 43714, 43714, 43739, 43741, 43744, 43754, 43762, 43764, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44002, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64285, 64287, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65136, 65140, 65142, 65276, 65313, 65338, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66176, 66204, 66208, 66256, 66304, 66335, 66349, 66378, 66384, 66421, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68096, 68112, 68115, 68117, 68119, 68121, 68149, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68324, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68899, 69376, 69404, 69415, 69415, 69424, 69445, 69600, 69622, 69635, 69687, 69763, 69807, 69840, 69864, 69891, 69926, 69956, 69956, 69968, 70002, 70006, 70006, 70019, 70066, 70081, 70084, 70106, 70106, 70108, 70108, 70144, 70161, 70163, 70187, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70366, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70461, 70461, 70480, 70480, 70493, 70497, 70656, 70708, 70727, 70730, 70751, 70751, 70784, 70831, 70852, 70853, 70855, 70855, 71040, 71086, 71128, 71131, 71168, 71215, 71236, 71236, 71296, 71338, 71352, 71352, 71424, 71450, 71680, 71723, 71840, 71903, 71935, 71935, 72096, 72103, 72106, 72144, 72161, 72161, 72163, 72163, 72192, 72192, 72203, 72242, 72250, 72250, 72272, 72272, 72284, 72329, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72750, 72768, 72768, 72818, 72847, 72960, 72966, 72968, 72969, 72971, 73008, 73030, 73030, 73056, 73061, 73063, 73064, 73066, 73097, 73112, 73112, 73440, 73458, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92880, 92909, 92928, 92975, 92992, 92995, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94032, 94032, 94099, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 123136, 123180, 123191, 123197, 123214, 123214, 123584, 123627, 124928, 125124, 125184, 125251, 125259, 125259, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101]; +const unicodeESNextIdentifierPart = [48, 57, 65, 90, 95, 95, 97, 122, 170, 170, 181, 181, 183, 183, 186, 186, 192, 214, 216, 246, 248, 705, 710, 721, 736, 740, 748, 748, 750, 750, 768, 884, 886, 887, 890, 893, 895, 895, 902, 906, 908, 908, 910, 929, 931, 1013, 1015, 1153, 1155, 1159, 1162, 1327, 1329, 1366, 1369, 1369, 1376, 1416, 1425, 1469, 1471, 1471, 1473, 1474, 1476, 1477, 1479, 1479, 1488, 1514, 1519, 1522, 1552, 1562, 1568, 1641, 1646, 1747, 1749, 1756, 1759, 1768, 1770, 1788, 1791, 1791, 1808, 1866, 1869, 1969, 1984, 2037, 2042, 2042, 2045, 2045, 2048, 2093, 2112, 2139, 2144, 2154, 2208, 2228, 2230, 2237, 2259, 2273, 2275, 2403, 2406, 2415, 2417, 2435, 2437, 2444, 2447, 2448, 2451, 2472, 2474, 2480, 2482, 2482, 2486, 2489, 2492, 2500, 2503, 2504, 2507, 2510, 2519, 2519, 2524, 2525, 2527, 2531, 2534, 2545, 2556, 2556, 2558, 2558, 2561, 2563, 2565, 2570, 2575, 2576, 2579, 2600, 2602, 2608, 2610, 2611, 2613, 2614, 2616, 2617, 2620, 2620, 2622, 2626, 2631, 2632, 2635, 2637, 2641, 2641, 2649, 2652, 2654, 2654, 2662, 2677, 2689, 2691, 2693, 2701, 2703, 2705, 2707, 2728, 2730, 2736, 2738, 2739, 2741, 2745, 2748, 2757, 2759, 2761, 2763, 2765, 2768, 2768, 2784, 2787, 2790, 2799, 2809, 2815, 2817, 2819, 2821, 2828, 2831, 2832, 2835, 2856, 2858, 2864, 2866, 2867, 2869, 2873, 2876, 2884, 2887, 2888, 2891, 2893, 2902, 2903, 2908, 2909, 2911, 2915, 2918, 2927, 2929, 2929, 2946, 2947, 2949, 2954, 2958, 2960, 2962, 2965, 2969, 2970, 2972, 2972, 2974, 2975, 2979, 2980, 2984, 2986, 2990, 3001, 3006, 3010, 3014, 3016, 3018, 3021, 3024, 3024, 3031, 3031, 3046, 3055, 3072, 3084, 3086, 3088, 3090, 3112, 3114, 3129, 3133, 3140, 3142, 3144, 3146, 3149, 3157, 3158, 3160, 3162, 3168, 3171, 3174, 3183, 3200, 3203, 3205, 3212, 3214, 3216, 3218, 3240, 3242, 3251, 3253, 3257, 3260, 3268, 3270, 3272, 3274, 3277, 3285, 3286, 3294, 3294, 3296, 3299, 3302, 3311, 3313, 3314, 3328, 3331, 3333, 3340, 3342, 3344, 3346, 3396, 3398, 3400, 3402, 3406, 3412, 3415, 3423, 3427, 3430, 3439, 3450, 3455, 3458, 3459, 3461, 3478, 3482, 3505, 3507, 3515, 3517, 3517, 3520, 3526, 3530, 3530, 3535, 3540, 3542, 3542, 3544, 3551, 3558, 3567, 3570, 3571, 3585, 3642, 3648, 3662, 3664, 3673, 3713, 3714, 3716, 3716, 3718, 3722, 3724, 3747, 3749, 3749, 3751, 3773, 3776, 3780, 3782, 3782, 3784, 3789, 3792, 3801, 3804, 3807, 3840, 3840, 3864, 3865, 3872, 3881, 3893, 3893, 3895, 3895, 3897, 3897, 3902, 3911, 3913, 3948, 3953, 3972, 3974, 3991, 3993, 4028, 4038, 4038, 4096, 4169, 4176, 4253, 4256, 4293, 4295, 4295, 4301, 4301, 4304, 4346, 4348, 4680, 4682, 4685, 4688, 4694, 4696, 4696, 4698, 4701, 4704, 4744, 4746, 4749, 4752, 4784, 4786, 4789, 4792, 4798, 4800, 4800, 4802, 4805, 4808, 4822, 4824, 4880, 4882, 4885, 4888, 4954, 4957, 4959, 4969, 4977, 4992, 5007, 5024, 5109, 5112, 5117, 5121, 5740, 5743, 5759, 5761, 5786, 5792, 5866, 5870, 5880, 5888, 5900, 5902, 5908, 5920, 5940, 5952, 5971, 5984, 5996, 5998, 6000, 6002, 6003, 6016, 6099, 6103, 6103, 6108, 6109, 6112, 6121, 6155, 6157, 6160, 6169, 6176, 6264, 6272, 6314, 6320, 6389, 6400, 6430, 6432, 6443, 6448, 6459, 6470, 6509, 6512, 6516, 6528, 6571, 6576, 6601, 6608, 6618, 6656, 6683, 6688, 6750, 6752, 6780, 6783, 6793, 6800, 6809, 6823, 6823, 6832, 6845, 6912, 6987, 6992, 7001, 7019, 7027, 7040, 7155, 7168, 7223, 7232, 7241, 7245, 7293, 7296, 7304, 7312, 7354, 7357, 7359, 7376, 7378, 7380, 7418, 7424, 7673, 7675, 7957, 7960, 7965, 7968, 8005, 8008, 8013, 8016, 8023, 8025, 8025, 8027, 8027, 8029, 8029, 8031, 8061, 8064, 8116, 8118, 8124, 8126, 8126, 8130, 8132, 8134, 8140, 8144, 8147, 8150, 8155, 8160, 8172, 8178, 8180, 8182, 8188, 8255, 8256, 8276, 8276, 8305, 8305, 8319, 8319, 8336, 8348, 8400, 8412, 8417, 8417, 8421, 8432, 8450, 8450, 8455, 8455, 8458, 8467, 8469, 8469, 8472, 8477, 8484, 8484, 8486, 8486, 8488, 8488, 8490, 8505, 8508, 8511, 8517, 8521, 8526, 8526, 8544, 8584, 11264, 11310, 11312, 11358, 11360, 11492, 11499, 11507, 11520, 11557, 11559, 11559, 11565, 11565, 11568, 11623, 11631, 11631, 11647, 11670, 11680, 11686, 11688, 11694, 11696, 11702, 11704, 11710, 11712, 11718, 11720, 11726, 11728, 11734, 11736, 11742, 11744, 11775, 12293, 12295, 12321, 12335, 12337, 12341, 12344, 12348, 12353, 12438, 12441, 12447, 12449, 12538, 12540, 12543, 12549, 12591, 12593, 12686, 12704, 12730, 12784, 12799, 13312, 19893, 19968, 40943, 40960, 42124, 42192, 42237, 42240, 42508, 42512, 42539, 42560, 42607, 42612, 42621, 42623, 42737, 42775, 42783, 42786, 42888, 42891, 42943, 42946, 42950, 42999, 43047, 43072, 43123, 43136, 43205, 43216, 43225, 43232, 43255, 43259, 43259, 43261, 43309, 43312, 43347, 43360, 43388, 43392, 43456, 43471, 43481, 43488, 43518, 43520, 43574, 43584, 43597, 43600, 43609, 43616, 43638, 43642, 43714, 43739, 43741, 43744, 43759, 43762, 43766, 43777, 43782, 43785, 43790, 43793, 43798, 43808, 43814, 43816, 43822, 43824, 43866, 43868, 43879, 43888, 44010, 44012, 44013, 44016, 44025, 44032, 55203, 55216, 55238, 55243, 55291, 63744, 64109, 64112, 64217, 64256, 64262, 64275, 64279, 64285, 64296, 64298, 64310, 64312, 64316, 64318, 64318, 64320, 64321, 64323, 64324, 64326, 64433, 64467, 64829, 64848, 64911, 64914, 64967, 65008, 65019, 65024, 65039, 65056, 65071, 65075, 65076, 65101, 65103, 65136, 65140, 65142, 65276, 65296, 65305, 65313, 65338, 65343, 65343, 65345, 65370, 65382, 65470, 65474, 65479, 65482, 65487, 65490, 65495, 65498, 65500, 65536, 65547, 65549, 65574, 65576, 65594, 65596, 65597, 65599, 65613, 65616, 65629, 65664, 65786, 65856, 65908, 66045, 66045, 66176, 66204, 66208, 66256, 66272, 66272, 66304, 66335, 66349, 66378, 66384, 66426, 66432, 66461, 66464, 66499, 66504, 66511, 66513, 66517, 66560, 66717, 66720, 66729, 66736, 66771, 66776, 66811, 66816, 66855, 66864, 66915, 67072, 67382, 67392, 67413, 67424, 67431, 67584, 67589, 67592, 67592, 67594, 67637, 67639, 67640, 67644, 67644, 67647, 67669, 67680, 67702, 67712, 67742, 67808, 67826, 67828, 67829, 67840, 67861, 67872, 67897, 67968, 68023, 68030, 68031, 68096, 68099, 68101, 68102, 68108, 68115, 68117, 68119, 68121, 68149, 68152, 68154, 68159, 68159, 68192, 68220, 68224, 68252, 68288, 68295, 68297, 68326, 68352, 68405, 68416, 68437, 68448, 68466, 68480, 68497, 68608, 68680, 68736, 68786, 68800, 68850, 68864, 68903, 68912, 68921, 69376, 69404, 69415, 69415, 69424, 69456, 69600, 69622, 69632, 69702, 69734, 69743, 69759, 69818, 69840, 69864, 69872, 69881, 69888, 69940, 69942, 69951, 69956, 69958, 69968, 70003, 70006, 70006, 70016, 70084, 70089, 70092, 70096, 70106, 70108, 70108, 70144, 70161, 70163, 70199, 70206, 70206, 70272, 70278, 70280, 70280, 70282, 70285, 70287, 70301, 70303, 70312, 70320, 70378, 70384, 70393, 70400, 70403, 70405, 70412, 70415, 70416, 70419, 70440, 70442, 70448, 70450, 70451, 70453, 70457, 70459, 70468, 70471, 70472, 70475, 70477, 70480, 70480, 70487, 70487, 70493, 70499, 70502, 70508, 70512, 70516, 70656, 70730, 70736, 70745, 70750, 70751, 70784, 70853, 70855, 70855, 70864, 70873, 71040, 71093, 71096, 71104, 71128, 71133, 71168, 71232, 71236, 71236, 71248, 71257, 71296, 71352, 71360, 71369, 71424, 71450, 71453, 71467, 71472, 71481, 71680, 71738, 71840, 71913, 71935, 71935, 72096, 72103, 72106, 72151, 72154, 72161, 72163, 72164, 72192, 72254, 72263, 72263, 72272, 72345, 72349, 72349, 72384, 72440, 72704, 72712, 72714, 72758, 72760, 72768, 72784, 72793, 72818, 72847, 72850, 72871, 72873, 72886, 72960, 72966, 72968, 72969, 72971, 73014, 73018, 73018, 73020, 73021, 73023, 73031, 73040, 73049, 73056, 73061, 73063, 73064, 73066, 73102, 73104, 73105, 73107, 73112, 73120, 73129, 73440, 73462, 73728, 74649, 74752, 74862, 74880, 75075, 77824, 78894, 82944, 83526, 92160, 92728, 92736, 92766, 92768, 92777, 92880, 92909, 92912, 92916, 92928, 92982, 92992, 92995, 93008, 93017, 93027, 93047, 93053, 93071, 93760, 93823, 93952, 94026, 94031, 94087, 94095, 94111, 94176, 94177, 94179, 94179, 94208, 100343, 100352, 101106, 110592, 110878, 110928, 110930, 110948, 110951, 110960, 111355, 113664, 113770, 113776, 113788, 113792, 113800, 113808, 113817, 113821, 113822, 119141, 119145, 119149, 119154, 119163, 119170, 119173, 119179, 119210, 119213, 119362, 119364, 119808, 119892, 119894, 119964, 119966, 119967, 119970, 119970, 119973, 119974, 119977, 119980, 119982, 119993, 119995, 119995, 119997, 120003, 120005, 120069, 120071, 120074, 120077, 120084, 120086, 120092, 120094, 120121, 120123, 120126, 120128, 120132, 120134, 120134, 120138, 120144, 120146, 120485, 120488, 120512, 120514, 120538, 120540, 120570, 120572, 120596, 120598, 120628, 120630, 120654, 120656, 120686, 120688, 120712, 120714, 120744, 120746, 120770, 120772, 120779, 120782, 120831, 121344, 121398, 121403, 121452, 121461, 121461, 121476, 121476, 121499, 121503, 121505, 121519, 122880, 122886, 122888, 122904, 122907, 122913, 122915, 122916, 122918, 122922, 123136, 123180, 123184, 123197, 123200, 123209, 123214, 123214, 123584, 123641, 124928, 125124, 125136, 125142, 125184, 125259, 125264, 125273, 126464, 126467, 126469, 126495, 126497, 126498, 126500, 126500, 126503, 126503, 126505, 126514, 126516, 126519, 126521, 126521, 126523, 126523, 126530, 126530, 126535, 126535, 126537, 126537, 126539, 126539, 126541, 126543, 126545, 126546, 126548, 126548, 126551, 126551, 126553, 126553, 126555, 126555, 126557, 126557, 126559, 126559, 126561, 126562, 126564, 126564, 126567, 126570, 126572, 126578, 126580, 126583, 126585, 126588, 126590, 126590, 126592, 126601, 126603, 126619, 126625, 126627, 126629, 126633, 126635, 126651, 131072, 173782, 173824, 177972, 177984, 178205, 178208, 183969, 183984, 191456, 194560, 195101, 917760, 917999]; + +/* @internal */ export function isUnicodeIdentifierStart(code: number) { + return lookupInUnicodeMap(code, unicodeESNextIdentifierStart); +} + +function isUnicodeIdentifierPart(code: number) { + return lookupInUnicodeMap(code, unicodeESNextIdentifierPart); +} diff --git a/ce/ce/mediaquery/media-query.ts b/ce/ce/mediaquery/media-query.ts new file mode 100644 index 0000000000..332ac24a85 --- /dev/null +++ b/ce/ce/mediaquery/media-query.ts @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../i18n'; +import { Kind, MediaQueryError, Scanner, Token } from './scanner'; + +export function parseQuery(text: string) { + const cursor = new Scanner(text); + + return QueryList.parse(cursor); +} + +function takeWhitespace(cursor: Scanner) { + while (!cursor.eof && isWhiteSpace(cursor)) { + cursor.take(); + } +} + +function isWhiteSpace(cursor: Scanner) { + return cursor.kind === Kind.Whitespace; +} + +class QueryList { + queries = new Array(); + get isValid() { + return !this.error; + } + error?: MediaQueryError; + + protected constructor() { + // + } + + get length() { + return this.queries.length; + } + static parse(cursor: Scanner) { + const result = new QueryList(); + + try { + cursor.scan(); // start the scanner + for (const statement of QueryList.parseQuery(cursor)) { + result.queries.push(statement); + } + } catch (error: any) { + result.error = error; + } + return result; + } + + static *parseQuery(cursor: Scanner): Iterable { + takeWhitespace(cursor); + if (cursor.eof) { + return; + } + yield Query.parse(cursor); + takeWhitespace(cursor); + if (cursor.eof) { + return; + } + switch (cursor.kind) { + case Kind.Comma: + cursor.take(); + return yield* QueryList.parseQuery(cursor); + case Kind.EndOfFile: + return; + } + throw new MediaQueryError(i`Expected comma, found ${JSON.stringify(cursor.text)}`, cursor.position.line, cursor.position.column); + } + + get features() { + const result = new Set(); + for (const query of this.queries) { + for (const expression of query.expressions) { + if (expression.feature) { + result.add(expression.feature); + } + } + } + return result; + } + + match(properties: Record) { + if (this.isValid) { + queries: for (const query of this.queries) { + for (const { feature, constant, not } of query.expressions) { + // get the value from the context + const contextValue = stringValue(properties[feature]); + if (not) { + // negative/not present query + + if (contextValue) { + // we have a value + if (constant && contextValue !== constant) { + continue; // the values are NOT a match. + } + if (!constant && contextValue === 'false') { + continue; + } + } else { + // no value + if (!constant || contextValue === 'false') { + continue; + } + } + } else { + // positive/present query + if (contextValue) { + if (contextValue === constant || contextValue !== 'false' && !constant) { + continue; + } + } else { + if (constant === 'false') { + continue; + } + } + } + continue queries; // no match + } + // we matched a whole query, we're good + return true; + } + } + // no query matched. + return false; + } +} + +function stringValue(value: unknown): string | undefined { + switch (typeof value) { + case 'string': + case 'number': + case 'boolean': + return value.toString(); + + case 'object': + return value === null ? 'true' : Array.isArray(value) ? stringValue(value[0]) || 'true' : 'true'; + } + return undefined; +} + +class Query { + protected constructor(public readonly expressions: Array) { + + } + + static parse(cursor: Scanner): Query { + const result = new Array(); + takeWhitespace(cursor); + // eslint-disable-next-line no-constant-condition + while (true) { + result.push(Expression.parse(cursor)); + takeWhitespace(cursor); + if (cursor.kind === Kind.AndKeyword) { + cursor.take(); // consume and + continue; + } + // the next token is not an 'and', so we bail now. + return new Query(result); + } + } + +} + +class Expression { + protected constructor(protected readonly featureToken: Token, protected readonly constantToken: Token | undefined, public readonly not: boolean) { + + } + get feature() { + return this.featureToken.text; + } + get constant() { + return this.constantToken?.stringValue || this.constantToken?.text || undefined; + } + + + /** @internal */ + static parse(cursor: Scanner, isNotted = false, inParen = false): Expression { + takeWhitespace(cursor); + + switch (cursor.kind) { + case Kind.Identifier: { + // start of an expression + const feature = cursor.take(); + takeWhitespace(cursor); + + if (cursor.kind === Kind.Colon) { + cursor.take(); // consume colon; + + // we have a constant for the + takeWhitespace(cursor); + switch (cursor.kind) { + case Kind.NumericLiteral: + case Kind.BooleanLiteral: + case Kind.Identifier: + case Kind.StringLiteral: { + // we have a good const value. + const constant = cursor.take(); + return new Expression(feature, constant, isNotted); + } + } + throw new MediaQueryError(i`Expected one of {Number, Boolean, Identifier, String}, found token ${JSON.stringify(cursor.text)}`, cursor.position.line, cursor.position.column); + } + return new Expression(feature, undefined, isNotted); + } + + case Kind.NotKeyword: + if (isNotted) { + throw new MediaQueryError(i`Expression specified NOT twice`, cursor.position.line, cursor.position.column); + } + cursor.take(); // suck up the not token + return Expression.parse(cursor, true, inParen); + + case Kind.OpenParen: { + cursor.take(); + const result = Expression.parse(cursor, isNotted, inParen); + takeWhitespace(cursor); + if (cursor.kind !== Kind.CloseParen) { + throw new MediaQueryError(i`Expected close parenthesis for expression, found ${JSON.stringify(cursor.text)}`, cursor.position.line, cursor.position.column); + } + + cursor.take(); + return result; + } + + default: + throw new MediaQueryError(i`Expected expression, found ${JSON.stringify(cursor.text)}`, cursor.position.line, cursor.position.column); + } + } +} diff --git a/ce/ce/mediaquery/scanner.ts b/ce/ce/mediaquery/scanner.ts new file mode 100644 index 0000000000..be45bcb2ff --- /dev/null +++ b/ce/ce/mediaquery/scanner.ts @@ -0,0 +1,910 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../i18n'; +import { CharacterCodes, isBinaryDigit, isDigit, isHexDigit, isIdentifierPart, isIdentifierStart, isLineBreak, isWhiteSpaceSingleLine, sizeOf } from './character-codes'; + +export enum MessageCategory { + Warning, + Error, + Suggestion, + Message +} + +export interface Message { + code: number; + category: MessageCategory; + text: string; +} + +export const messages = { + DigitExpected: { code: 1100, category: MessageCategory.Error, text: 'Digit expected (0-9)' }, + HexDigitExpected: { code: 1101, category: MessageCategory.Error, text: 'Hex Digit expected (0-F,0-f)' }, + BinaryDigitExpected: { code: 1102, category: MessageCategory.Error, text: 'Binary Digit expected (0,1)' }, + UnexpectedEndOfFile: { code: 1103, category: MessageCategory.Error, text: 'Unexpected end of file while searching for \'{0}\'' }, + InvalidEscapeSequence: { code: 1104, category: MessageCategory.Error, text: 'Invalid escape sequence' }, +}; + +export function format(text: string, ...args: Array): string { + return text.replace(/{(\d+)}/g, (_match, index: string) => '' + args[+index] || ''); +} + +export interface Token { + /** the character offset within the document */ + readonly offset: number; + + /** the text of the current token (when appropriate) */ + text: string; + + /** the literal value */ + stringValue?: string; + + /** the token kind */ + readonly kind: Kind; +} + + +// All conflict markers consist of the same character repeated seven times. If it is +// a <<<<<<< or >>>>>>> marker then it is also followed by a space. +const mergeConflictMarkerLength = 7; + +/** + * Position in a text document expressed as zero-based line and character offset. + * The offsets are based on a UTF-16 string representation. So a string of the form + * `a𐐀b` the character offset of the character `a` is 0, the character offset of `𐐀` + * is 1 and the character offset of b is 3 since `𐐀` is represented using two code + * units in UTF-16. + * + * Positions are line end character agnostic. So you can not specify a position that + * denotes `\r|\n` or `\n|` where `|` represents the character offset. + */ +export interface Position { + /** + * Line position in a document (zero-based). + * If a line number is greater than the number of lines in a document, it defaults back to the number of lines in the document. + * If a line number is negative, it defaults to 0. + */ + line: number; + /** + * Character offset on a line in a document (zero-based). Assuming that the line is + * represented as a string, the `character` value represents the gap between the + * `character` and `character + 1`. + * + * If the character value is greater than the line length it defaults back to the + * line length. + * If a line number is negative, it defaults to 0. + */ + column: number; +} + +export enum Kind { + Unknown, + EndOfFile, + + SingleLineComment, + MultiLineComment, + NewLine, + Whitespace, + + // We detect and provide better error recovery when we encounter a git merge marker. This + // allows us to edit files with git-conflict markers in them in a much more pleasant manner. + ConflictMarker, + + // Literals + NumericLiteral, + StringLiteral, + + // Boolean Literals + BooleanLiteral, + + TrueKeyword, + FalseKeyword, + + // Punctuation + OpenBrace, + CloseBrace, + OpenParen, + CloseParen, + OpenBracket, + CloseBracket, + Dot, + Elipsis, + Semicolon, + Comma, + QuestionDot, + LessThan, + OpenAngle = LessThan, + LessThanSlash, + GreaterThan, + CloseAngle = GreaterThan, + LessThanEquals, + GreaterThanEquals, + EqualsEquals, + ExclamationEquals, + EqualsEqualsEquals, + ExclamationEqualsEquals, + EqualsArrow, + Plus, + Minus, + Asterisk, + AsteriskAsterisk, + Slash, + Percent, + PlusPlus, + MinusMinus, + LessThanLessThan, + GreaterThanGreaterThan, + GreaterThanGreaterThanGreaterThan, + Ampersand, + Bar, + Caret, + Exclamation, + Tilde, + AmpersandAmpersand, + BarBar, + Question, + Colon, + At, + QuestionQuestion, + + // Assignments + Equals, + PlusEquals, + MinusEquals, + AsteriskEquals, + AsteriskAsteriskEquals, + SlashEquals, + PercentEquals, + LessThanLessThanEquals, + GreaterThanGreaterThanEquals, + GreaterThanGreaterThanGreaterThanEquals, + AmpersandEquals, + BarEquals, + BarBarEquals, + AmpersandAmpersandEquals, + QuestionQuestionEquals, + CaretEquals, + + // Identifiers + Identifier, + + // Keywords + KeywordsStart = 1000, + AndKeyword, + NotKeyword, + + KeywordsEnd, + + + // Tokens that can represent elements + Elements = 2000, + Model, + Enum, + EnumValue, + Import, + TypeAlias, + ParameterAlias, + ResponseAlias, + Interface, + Operation, + Annotation, + Documentation, + Label, + Preamble, + Property, + Parameter, + TemplateDeclaration, + TemplateParameters, + Parent, + Response, + ResponseExpression, + Result, + TypeExpression, + Union, +} + +const keywords = new Map([ + ['NOT', Kind.NotKeyword], + ['not', Kind.NotKeyword], + ['AND', Kind.AndKeyword], + ['and', Kind.AndKeyword], + + + ['true', Kind.BooleanLiteral], // TrueKeyword + ['false', Kind.BooleanLiteral] // FalseKeyword +]); + +interface TokenLocation extends Position { + offset: number; +} + +export class Scanner implements Token { + #offset = 0; + #line = 0; + #column = 0; + #map = new Array(); + + #length: number; + #text: string; + + #ch!: number; + #chNext!: number; + #chNextNext!: number; + + #chSz!: number; + #chNextSz!: number; + #chNextNextSz!: number; + + /** The assumed tab width. If this is set before scanning, it enables accurate Position tracking. */ + tabWidth = 2; + + // current token information + + /** the character offset within the document */ + offset!: number; + + /** the token kind */ + kind!: Kind; + + /** the text of the current token (when appropriate) */ + text!: string; + + /** the string value of current string literal token (unquoted, unescaped) */ + stringValue?: string; + + /** returns the Position (line/column) of the current token */ + get position(): Position { + return this.positionFromOffset(this.offset); + } + + constructor(text: string) { + this.#text = text; + this.#length = text.length; + this.advance(0); + this.markPosition(); + + // let's hide these, then we can clone this nicely. + Object.defineProperty(this, 'tabWidth', { enumerable: false }); + } + + get eof() { + return this.#offset > (this.#length); + } + + private advance(count?: number): number { + let codeOrChar: number; + let newOffset: number; + let offsetAdvancedBy = 0; + + switch (count) { + case undefined: + case 1: + offsetAdvancedBy = this.#chSz; + this.#offset += this.#chSz; + this.#ch = this.#chNext; this.#chSz = this.#chNextSz; + this.#chNext = this.#chNextNext; this.#chNextSz = this.#chNextNextSz; + + newOffset = this.#offset + this.#chSz + this.#chNextSz; + codeOrChar = this.#text.charCodeAt(newOffset); + this.#chNextNext = (this.#chNextNextSz = sizeOf(codeOrChar)) === 1 ? codeOrChar : this.#text.codePointAt(newOffset)!; + return offsetAdvancedBy; + + case 2: + offsetAdvancedBy = this.#chSz + this.#chNextSz; + this.#offset += this.#chSz + this.#chNextSz; + this.#ch = this.#chNextNext; this.#chSz = this.#chNextNextSz; + + newOffset = this.#offset + this.#chSz; + codeOrChar = this.#text.charCodeAt(newOffset); + this.#chNext = (this.#chNextSz = sizeOf(codeOrChar)) === 1 ? codeOrChar : this.#text.codePointAt(newOffset)!; + + newOffset += this.#chNextSz; + codeOrChar = this.#text.charCodeAt(newOffset); + this.#chNextNext = (this.#chNextNextSz = sizeOf(codeOrChar)) === 1 ? codeOrChar : this.#text.codePointAt(newOffset)!; + return offsetAdvancedBy; + + default: + case 3: + offsetAdvancedBy = this.#chSz + this.#chNextSz + this.#chNextNextSz; + count -= 3; + while (count) { + // skip over characters while we work. + offsetAdvancedBy += sizeOf(this.#text.charCodeAt(this.#offset + offsetAdvancedBy)); + } + this.#offset += offsetAdvancedBy; + + // eslint-disable-next-line no-fallthrough + case 0: + newOffset = this.#offset; + codeOrChar = this.#text.charCodeAt(newOffset); + this.#ch = (this.#chSz = sizeOf(codeOrChar)) === 1 ? codeOrChar : this.#text.codePointAt(newOffset)!; + + newOffset += this.#chSz; + codeOrChar = this.#text.charCodeAt(newOffset); + this.#chNext = (this.#chNextSz = sizeOf(codeOrChar)) === 1 ? codeOrChar : this.#text.codePointAt(newOffset)!; + + newOffset += this.#chNextSz; + codeOrChar = this.#text.charCodeAt(newOffset); + this.#chNextNext = (this.#chNextNextSz = sizeOf(codeOrChar)) === 1 ? codeOrChar : this.#text.codePointAt(newOffset)!; + return offsetAdvancedBy; + } + } + + private next(token: Kind, count = 1, value?: string) { + const originalOffset = this.#offset; + const offsetAdvancedBy = this.advance(count); + this.text = value || this.#text.substr(originalOffset, offsetAdvancedBy); + + this.#column += count; + return this.kind = token; + } + + /** adds the current position to the token to the offset:position map */ + private markPosition() { + this.#map.push({ offset: this.#offset, column: this.#column, line: this.#line }); + } + + /** updates the position and marks the location */ + private newLine(count = 1) { + this.text = this.#text.substr(this.#offset, count); + this.advance(count); + + this.#line++; + this.#column = 0; + this.markPosition(); // make sure the map has the new location + + return this.kind = Kind.NewLine; + } + + start() { + if (this.offset === undefined) { + this.scan(); + } + return this; + } + + /** + * Identifies and returns the next token type in the document + * + * @returns the state of the scanner will have the properties `token`, `value`, `offset` pointing to the current token at the end of this call. + * + * @notes before this call, `#offset` is pointing to the next character to be evaluated. + * + */ + scan(): Kind { + + // this token starts at + this.offset = this.#offset; + this.stringValue = undefined; + + if (!this.eof) { + switch (this.#ch) { + case CharacterCodes.carriageReturn: + return this.newLine(this.#chNext === CharacterCodes.lineFeed ? 2 : 1); + + case CharacterCodes.lineFeed: + return this.newLine(); + + case CharacterCodes.tab: + case CharacterCodes.verticalTab: + case CharacterCodes.formFeed: + case CharacterCodes.space: + case CharacterCodes.nonBreakingSpace: + case CharacterCodes.ogham: + case CharacterCodes.enQuad: + case CharacterCodes.emQuad: + case CharacterCodes.enSpace: + case CharacterCodes.emSpace: + case CharacterCodes.threePerEmSpace: + case CharacterCodes.fourPerEmSpace: + case CharacterCodes.sixPerEmSpace: + case CharacterCodes.figureSpace: + case CharacterCodes.punctuationSpace: + case CharacterCodes.thinSpace: + case CharacterCodes.hairSpace: + case CharacterCodes.zeroWidthSpace: + case CharacterCodes.narrowNoBreakSpace: + case CharacterCodes.mathematicalSpace: + case CharacterCodes.ideographicSpace: + case CharacterCodes.byteOrderMark: + return this.scanWhitespace(); + + case CharacterCodes.openParen: + return this.next(Kind.OpenParen); + + case CharacterCodes.closeParen: + return this.next(Kind.CloseParen); + + case CharacterCodes.comma: + return this.next(Kind.Comma); + + case CharacterCodes.colon: + return this.next(Kind.Colon); + + case CharacterCodes.semicolon: + return this.next(Kind.Semicolon); + + case CharacterCodes.openBracket: + return this.next(Kind.OpenBracket); + + case CharacterCodes.closeBracket: + return this.next(Kind.CloseBracket); + + case CharacterCodes.openBrace: + return this.next(Kind.OpenBrace); + + case CharacterCodes.closeBrace: + return this.next(Kind.CloseBrace); + + case CharacterCodes.tilde: + return this.next(Kind.Tilde); + + case CharacterCodes.at: + return this.next(Kind.At); + + case CharacterCodes.caret: + return this.#chNext === CharacterCodes.equals ? this.next(Kind.CaretEquals, 2) : this.next(Kind.Caret); + + case CharacterCodes.percent: + return this.#chNext === CharacterCodes.equals ? this.next(Kind.PercentEquals, 2) : this.next(Kind.Percent); + + case CharacterCodes.question: + return this.#chNext === CharacterCodes.dot && !isDigit(this.#chNextNext) ? + this.next(Kind.QuestionDot, 2) : + this.#chNext === CharacterCodes.question ? + this.#chNextNext === CharacterCodes.equals ? + this.next(Kind.QuestionQuestionEquals, 3) : + this.next(Kind.QuestionQuestion, 2) : + this.next(Kind.Question); + + case CharacterCodes.exclamation: + return this.#chNext === CharacterCodes.equals ? + this.#chNextNext === CharacterCodes.equals ? + this.next(Kind.ExclamationEqualsEquals, 3) : + this.next(Kind.ExclamationEquals, 2) : + this.next(Kind.Exclamation); + + case CharacterCodes.ampersand: + return this.#chNext === CharacterCodes.ampersand ? + this.#chNextNext === CharacterCodes.equals ? + this.next(Kind.AmpersandAmpersandEquals, 3) : + this.next(Kind.AmpersandAmpersand, 2) : + this.#chNext === CharacterCodes.equals ? + this.next(Kind.AmpersandEquals, 2) : + this.next(Kind.Ampersand); + + case CharacterCodes.asterisk: + return this.#chNext === CharacterCodes.asterisk ? + this.#chNextNext === CharacterCodes.equals ? + this.next(Kind.AsteriskAsteriskEquals, 3) : + this.next(Kind.AsteriskAsterisk, 2) : + this.#chNext === CharacterCodes.equals ? + this.next(Kind.AsteriskEquals, 2) : + this.next(Kind.Asterisk); + + case CharacterCodes.plus: + return this.#chNext === CharacterCodes.plus ? + this.next(Kind.PlusPlus, 2) : + this.#chNext === CharacterCodes.equals ? + this.next(Kind.PlusEquals, 2) : + this.next(Kind.Plus); + + case CharacterCodes.minus: + return this.#chNext === CharacterCodes.minus ? + this.next(Kind.MinusMinus, 2) : + this.#chNext === CharacterCodes.equals ? + this.next(Kind.MinusEquals, 2) : + this.next(Kind.Minus); + + case CharacterCodes.dot: + return isDigit(this.#chNext) ? + this.scanNumber() : + this.#chNext === CharacterCodes.dot && this.#chNextNext === CharacterCodes.dot ? + this.next(Kind.Elipsis, 3) : + this.next(Kind.Dot); + + case CharacterCodes.slash: + return this.#chNext === CharacterCodes.slash ? + this.scanSingleLineComment() : + this.#chNext === CharacterCodes.asterisk ? + this.scanMultiLineComment() : + + this.#chNext === CharacterCodes.equals ? + this.next(Kind.SlashEquals) : + this.next(Kind.Slash); + + case CharacterCodes._0: + return this.#chNext === CharacterCodes.x || this.#chNext === CharacterCodes.X ? + this.scanHexNumber() : + this.#chNext === CharacterCodes.B || this.#chNext === CharacterCodes.B ? + this.scanBinaryNumber() : + this.scanNumber(); + + case CharacterCodes._1: + case CharacterCodes._2: + case CharacterCodes._3: + case CharacterCodes._4: + case CharacterCodes._5: + case CharacterCodes._6: + case CharacterCodes._7: + case CharacterCodes._8: + case CharacterCodes._9: + return this.scanNumber(); + + case CharacterCodes.lessThan: + return this.isConflictMarker() ? + this.next(Kind.ConflictMarker, mergeConflictMarkerLength) : + this.#chNext === CharacterCodes.lessThan ? + this.#chNextNext === CharacterCodes.equals ? + this.next(Kind.LessThanLessThanEquals, 3) : + this.next(Kind.LessThanLessThan, 2) : + this.#chNext === CharacterCodes.equals ? + this.next(Kind.LessThanEquals, 2) : + this.next(Kind.LessThan); + + case CharacterCodes.greaterThan: + return this.isConflictMarker() ? + this.next(Kind.ConflictMarker, mergeConflictMarkerLength) : + this.next(Kind.GreaterThan); + + case CharacterCodes.equals: + return this.isConflictMarker() ? + this.next(Kind.ConflictMarker, mergeConflictMarkerLength) : + this.#chNext === CharacterCodes.equals ? + this.#chNextNext === CharacterCodes.equals ? + this.next(Kind.EqualsEqualsEquals, 3) : + this.next(Kind.EqualsEquals, 2) : + this.#chNext === CharacterCodes.greaterThan ? + this.next(Kind.EqualsArrow, 2) : + this.next(Kind.Equals); + + case CharacterCodes.bar: + return this.isConflictMarker() ? + this.next(Kind.ConflictMarker, mergeConflictMarkerLength) : + this.#chNext === CharacterCodes.bar ? + this.#chNextNext === CharacterCodes.equals ? + this.next(Kind.BarBarEquals, 3) : + this.next(Kind.BarBar, 2) : + this.#chNext === CharacterCodes.equals ? + this.next(Kind.BarEquals, 2) : + this.next(Kind.Bar); + + case CharacterCodes.singleQuote: + case CharacterCodes.doubleQuote: + case CharacterCodes.backtick: + return this.scanString(); + + default: + // FYI: + // Well-known characters that are currently not processed + // # \ + // will need to update the scanner if there is a need to recognize them + return isIdentifierStart(this.#ch) ? this.scanIdentifier() : this.next(Kind.Unknown); + } + } + + this.text = ''; + return this.kind = Kind.EndOfFile; + } + + take() { + const result = { ...this }; + this.scan(); + return result; + } + + /** + * When the current token is greaterThan, this will return any tokens with characters + * after the greater than character. This has to be scanned separately because greater + * thans appear in positions where longer tokens are incorrect, e.g. `model x=y;`. + * The solution is to call rescanGreaterThan from the parser in contexts where longer + * tokens starting with `>` are allowed (i.e. when parsing binary expressions). + */ + rescanGreaterThan(): Kind { + if (this.kind === Kind.GreaterThan) { + return this.#ch === CharacterCodes.greaterThan ? + this.#chNext === CharacterCodes.equals ? + this.next(Kind.GreaterThanGreaterThanEquals, 3) : + this.next(Kind.GreaterThanGreaterThan, 2) : + this.#ch === CharacterCodes.equals ? + this.next(Kind.GreaterThanEquals, 2) : + this.next(Kind.GreaterThan); + } + return this.kind; + } + + private isConflictMarker() { + // Conflict markers must be at the start of a line. + if (this.#offset === 0 || isLineBreak(this.#text.charCodeAt(this.#offset - 1))) { + if ((this.#offset + mergeConflictMarkerLength) < this.#length) { + for (let i = 0; i < mergeConflictMarkerLength; i++) { + if (this.#text.charCodeAt(this.#offset + i) !== this.#ch) { + return false; + } + } + return this.#ch === CharacterCodes.equals || this.#text.charCodeAt(this.#offset + mergeConflictMarkerLength) === CharacterCodes.space; + } + } + + return false; + } + + private scanWhitespace(): Kind { + // since whitespace are not always 1 character wide, we're going to mark the position before the whitespace. + this.markPosition(); + + do { + // advance the position + this.#column += this.widthOfCh; + this.advance(); + } while (isWhiteSpaceSingleLine(this.#ch)); + + // and after... + this.markPosition(); + + this.text = this.#text.substring(this.offset, this.#offset); + return this.kind = Kind.Whitespace; + } + + private scanDigits(): string { + const start = this.#offset; + while (isDigit(this.#ch)) { + this.advance(); + } + return this.#text.substring(start, this.#offset); + } + + private scanNumber() { + const start = this.#offset; + + const main = this.scanDigits(); + let decimal: string | undefined; + let scientific: string | undefined; + + if (this.#ch === CharacterCodes.dot) { + this.advance(); + decimal = this.scanDigits(); + } + + if (this.#ch === CharacterCodes.E || this.#ch === CharacterCodes.e) { + this.assert(isDigit(this.#chNext), i`ParseError: Digit expected (0-9)`); + this.advance(); + scientific = this.scanDigits(); + } + + this.text = scientific ? + decimal ? + `${main}.${decimal}e${scientific}` : + `${main}e${scientific}` : + decimal ? + `${main}.${decimal}` : + main; + + // update the position + this.#column += (this.#offset - start); + return this.kind = Kind.NumericLiteral; + } + + private scanHexNumber() { + this.assert(isHexDigit(this.#chNextNext), i`ParseError: Hex Digit expected (0-F,0-f)`); + this.advance(2); + + this.text = `0x${this.scanUntil((ch) => !isHexDigit(ch), 'Hex Digit')}`; + return this.kind = Kind.NumericLiteral; + } + + private scanBinaryNumber() { + this.assert(isBinaryDigit(this.#chNextNext), i`ParseError: Binary Digit expected (0,1)`); + + this.advance(2); + + this.text = `0b${this.scanUntil((ch) => !isBinaryDigit(ch), 'Binary Digit')}`; + return this.kind = Kind.NumericLiteral; + + } + + private get widthOfCh() { + return this.#ch === CharacterCodes.tab ? (this.#column % this.tabWidth || this.tabWidth) : 1; + } + + private scanUntil(predicate: (char: number, charNext: number, charNextNext: number) => boolean, expectedClose?: string, consumeClose?: number) { + const start = this.#offset; + + do { + // advance the position + if (isLineBreak(this.#ch)) { + this.advance(this.#ch === CharacterCodes.carriageReturn && this.#chNext === CharacterCodes.lineFeed ? 2 : 1); + this.#line++; + this.#column = 0; + this.markPosition(); // make sure the map has the new location + } else { + this.#column += this.widthOfCh; + this.advance(); + } + + if (this.eof) { + this.assert(!expectedClose, i`Unexpected end of file while searching for '${expectedClose}'`); + break; + } + + } while (!predicate(this.#ch, this.#chNext, this.#chNextNext)); + + if (consumeClose) { + this.advance(consumeClose); + } + + // and after... + this.markPosition(); + + return this.#text.substring(start, this.#offset); + } + + private scanSingleLineComment() { + this.text = this.scanUntil(isLineBreak); + return this.kind = Kind.SingleLineComment; + } + + private scanMultiLineComment() { + this.text = this.scanUntil((ch, chNext) => ch === CharacterCodes.asterisk && chNext === CharacterCodes.slash, '*/', 2); + return this.kind = Kind.MultiLineComment; + } + + private scanString() { + const quote = this.#ch; + const quoteLength = 1; + const closing = String.fromCharCode(this.#ch); + let escaped = false; + let crlf = false; + let isEscaping = false; + + const text = this.scanUntil((ch, chNext, chNextNext) => { + if (isEscaping) { + isEscaping = false; + return false; + } + + if (ch === CharacterCodes.backslash) { + isEscaping = escaped = true; + return false; + } + + if (ch == CharacterCodes.carriageReturn) { + if (chNext == CharacterCodes.lineFeed) { + crlf = true; + } + return false; + } + + return ch === quote; + }, closing, quoteLength); + + // TODO: optimize to single pass over string, easier if we refactor some bookkeeping first. + + // strip quotes + let value = text.substring(quoteLength, text.length - quoteLength); + + // Normalize CRLF to LF when interpreting value of multi-line string + // literals. Matches JavaScript behavior and ensures program behavior does + // not change due to line-ending conversion. + if (crlf) { + value = value.replace(/\r\n/g, '\n'); + } + + if (escaped) { + value = this.unescapeString(value); + } + + this.text = text; + this.stringValue = value; + return this.kind = Kind.StringLiteral; + } + + private unescapeString(text: string) { + let result = ''; + let start = 0; + let pos = 0; + const end = text.length; + + while (pos < end) { + let ch = text.charCodeAt(pos); + if (ch != CharacterCodes.backslash) { + pos++; + continue; + } + + result += text.substring(start, pos); + pos++; + ch = text.charCodeAt(pos); + + switch (ch) { + case CharacterCodes.r: + result += '\r'; + break; + case CharacterCodes.n: + result += '\n'; + break; + case CharacterCodes.t: + result += '\t'; + break; + case CharacterCodes.singleQuote: + result += '\''; + break; + case CharacterCodes.doubleQuote: + result += '"'; + break; + case CharacterCodes.backslash: + result += '\\'; + break; + case CharacterCodes.backtick: + result += '`'; + break; + default: + throw new MediaQueryError(i`Invalid escape sequence`, this.position.line, this.position.column); + } + + pos++; + start = pos; + } + + result += text.substring(start, pos); + return result; + } + + scanIdentifier() { + this.text = this.scanUntil((ch) => !isIdentifierPart(ch)); + return this.kind = keywords.get(this.text) ?? Kind.Identifier; + } + + /** + * Returns the zero-based line/column from the given offset + * (binary search thru the token start locations) + * @param offset the character position in the document + */ + positionFromOffset(offset: number): Position { + let position = { line: 0, column: 0, offset: 0 }; + + // eslint-disable-next-line keyword-spacing + if (offset < 0 || offset > this.#length) { + return { line: position.line, column: position.column }; + } + + let first = 0; //left endpoint + let last = this.#map.length - 1; //right endpoint + let middle = Math.floor((first + last) / 2); + + while (first <= last) { + middle = Math.floor((first + last) / 2); + position = this.#map[middle]; + if (position.offset === offset) { + return { line: position.line, column: position.column }; + } + if (position.offset < offset) { + first = middle + 1; + continue; + } + last = middle - 1; + position = this.#map[last]; + } + return { line: position.line, column: position.column + (offset - position.offset) }; + } + + static *TokensFrom(text: string): Iterable { + const scanner = new Scanner(text).start(); + while (!scanner.eof) { + yield scanner.take(); + } + } + + protected assert(assertion: boolean, message: string) { + if (!assertion) { + const p = this.position; + throw new MediaQueryError(message, p.line, p.column); + } + } +} + +export class MediaQueryError extends Error { + constructor(message: string, public readonly line: number, public readonly column: number) { + super(message); + } +} diff --git a/ce/ce/package.json b/ce/ce/package.json new file mode 100644 index 0000000000..8046b01e58 --- /dev/null +++ b/ce/ce/package.json @@ -0,0 +1,78 @@ +{ + "name": "@microsoft/vcpkg-ce", + "version": "0.7.0", + "description": "vcpkg-ce", + "main": "dist/main.js", + "typings": "dist/exports.d.ts", + "directories": { + "doc": "docs" + }, + "engines": { + "node": ">=10.12.0" + }, + "scripts": { + "eslint-fix": "eslint . --fix --ext .ts", + "eslint": "eslint . --ext .ts", + "clean": "shx rm -rf dist .rush *.log && shx echo Done", + "build": "tsc -p .", + "watch": "tsc -p . --watch", + "prepare": "npm run build", + "watch-test": "mocha dist/test --timeout 200000 --watch", + "translate": "translate-strings ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/vcpkg-ce.git" + }, + "keywords": [ + "vcpkg-ce", + "vcpkg", + "ce" + ], + "author": "Microsoft", + "license": "MIT", + "bugs": { + "url": "https://github.com/Microsoft/vcpkg-ce/issues" + }, + "homepage": "https://github.com/Microsoft/vcpkg-ce#readme", + "readme": "https://github.com/Microsoft/vcpkg-ce/blob/master/readme.md", + "devDependencies": { + "@types/node": "17.0.15", + "@typescript-eslint/eslint-plugin": "5.10.2", + "@typescript-eslint/parser": "5.10.2", + "@types/micromatch": "4.0.2", + "eslint-plugin-notice": "0.9.10", + "eslint": "8.8.0", + "@types/semver": "7.3.9", + "@types/tar-stream": "~2.2.0", + "typescript": "4.5.5", + "shx": "0.3.4", + "chalk": "4.1.2", + "translate-strings": "1.0.11", + "@types/marked-terminal": "3.1.3", + "@types/marked": "4.0.2", + "@types/cli-progress": "3.9.2", + "@types/mocha": "9.1.0", + "source-map-support": "0.5.21" + }, + "dependencies": { + "@snyk/nuget-semver": "1.3.0", + "vscode-uri": "3.0.3", + "ee-ts": "2.0.0-rc.6", + "yaml": "2.0.0-10", + "semver": "7.3.5", + "tar-stream": "~2.3.0", + "got": "11.8.3", + "sorted-btree": "1.6.0", + "sed-lite": "0.8.4", + "unbzip2-stream": "1.4.3", + "micromatch": "4.0.4", + "chalk": "4.1.2", + "marked-terminal": "5.1.1", + "marked": "4.0.12", + "cli-progress": "3.10.0", + "applicationinsights": "2.2.1", + "fast-glob": "3.2.11", + "fast-xml-parser": "4.0.3" + } +} \ No newline at end of file diff --git a/ce/ce/readme.md b/ce/ce/readme.md new file mode 100644 index 0000000000..a80ef3149a --- /dev/null +++ b/ce/ce/readme.md @@ -0,0 +1,16 @@ +# vcpkg-ce Project + + +# Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + diff --git a/ce/ce/registries/ArtifactRegistry.ts b/ce/ce/registries/ArtifactRegistry.ts new file mode 100644 index 0000000000..bf91e4a001 --- /dev/null +++ b/ce/ce/registries/ArtifactRegistry.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { compare } from 'semver'; +import { MetadataFile } from '../amf/metadata-file'; +import { Artifact } from '../artifacts/artifact'; +import { Registry, SearchCriteria } from '../artifacts/registry'; +import { FileType } from '../fs/filesystem'; +import { Session } from '../session'; +import { Queue } from '../util/promise'; +import { Uri } from '../util/uri'; +import { isYAML, serialize } from '../yaml/yaml'; +import { ArtifactIndex } from './artifact-index'; +import { Index } from './indexer'; +import { Registries } from './registries'; +import { THIS_IS_NOT_A_MANIFEST_ITS_AN_INDEX_STRING } from './standard-registry'; + + +export abstract class ArtifactRegistry implements Registry { + constructor(protected session: Session, readonly location: Uri) { + } + abstract load(): Promise; + + abstract readonly installationFolder: Uri; + + protected abstract readonly cacheFolder: Uri; + protected index = new Index(ArtifactIndex); + protected abstract indexYaml: Uri; + + get count() { + return this.index.indexOfTargets.length; + } + + #loaded = false; + + get loaded() { + return this.#loaded; + } + + protected set loaded(loaded: boolean) { + this.#loaded = loaded; + } + + abstract update(): Promise; + + async regenerate(): Promise { + // reset the index to blank. + this.index = new Index(ArtifactIndex); + + const repo = this; + const q = new Queue(); + const session = this.session; + + async function processFile(uri: Uri) { + + const content = await uri.readUTF8(); + // if you see this, it's an index, and we can skip even trying. + if (content.startsWith(THIS_IS_NOT_A_MANIFEST_ITS_AN_INDEX_STRING)) { + return; + } + try { + const amf = await MetadataFile.parseConfiguration(uri.fsPath, content, session); + + if (!amf.isFormatValid) { + for (const err of amf.formatErrors) { + repo.session.channels.warning(`Parse errors in metadata file ${err}}`); + } + throw new Error('invalid yaml'); + } + + amf.validate(); + + if (!amf.isValid) { + for (const err of amf.validationErrors) { + repo.session.channels.warning(`Validation errors in metadata file ${err}}`); + } + throw new Error('invalid manifest'); + } + repo.session.channels.debug(`Inserting ${uri.formatted} into index.`); + repo.index.insert(amf, repo.cacheFolder.relative(uri)); + + } catch (e: any) { + repo.session.channels.debug(e.toString()); + repo.session.channels.warning(`skipping invalid metadata file ${uri.fsPath}`); + } + } + + async function process(folder: Uri) { + for (const [entry, type] of await folder.readDirectory()) { + if (type & FileType.Directory) { + await process(entry); + continue; + } + + if (type & FileType.File && isYAML(entry.path)) { + void q.enqueue(() => processFile(entry)); + } + } + } + + // process the files in the local folder + await process(this.cacheFolder); + await q.done; + + // we're done inserting values + this.index.doneInsertion(); + + this.loaded = true; + } + + async search(parent: Registries, criteria?: SearchCriteria): Promise]>> { + await this.load(); + const query = this.index.where; + + if (criteria?.idOrShortName) { + query.id.nameOrShortNameIs(criteria.idOrShortName); + if (criteria.version) { + query.version.rangeMatch(criteria.version); + } + } + + if (criteria?.keyword) { + query.summary.contains(criteria.keyword); + } + + return [...(await this.openArtifacts(query.items, parent)).entries()].map(each => [this, ...each]); + } + + + private async openArtifact(manifestPath: string, parent: Registries): Promise { + const metadata = await MetadataFile.parseMetadata(this.cacheFolder.join(manifestPath), this.session, this); + + return new Artifact(this.session, + metadata, + this.index.indexSchema.id.getShortNameOf(metadata.info.id) || metadata.info.id, + this.installationFolder.join(metadata.info.id.replace(/[^\w]+/g, '.'), metadata.info.version), + parent.getRegistryName(this), + this.location + ).init(this.session); + } + + private async openArtifacts(manifestPaths: Array, parent: Registries) { + let metadataFiles = new Array(); + + // load them up async, but throttled via a queue + await manifestPaths.forEachAsync(async (manifest) => metadataFiles.push(await this.openArtifact(manifest, parent))).done; + + // sort the contents by version before grouping. (descending version) + metadataFiles = metadataFiles.sort((a, b) => compare(b.metadata.info.version, a.metadata.info.version)); + + // return a map. + return metadataFiles.groupByMap(m => m.metadata.info.id, artifact => artifact); + } + + async save(): Promise { + await this.indexYaml.writeFile(Buffer.from(`${THIS_IS_NOT_A_MANIFEST_ITS_AN_INDEX_STRING}\n${serialize(this.index.serialize()).replace(/\s*(\d*,)\n/g, '$1')}`)); + } + +} diff --git a/ce/ce/registries/LocalRegistry.ts b/ce/ce/registries/LocalRegistry.ts new file mode 100644 index 0000000000..b59b9247ae --- /dev/null +++ b/ce/ce/registries/LocalRegistry.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { createHash } from 'crypto'; +import { parse } from 'yaml'; +import { Registry } from '../artifacts/registry'; +import { registryIndexFile } from '../constants'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { ArtifactRegistry } from './ArtifactRegistry'; + + +export class LocalRegistry extends ArtifactRegistry implements Registry { + protected indexYaml: Uri; + readonly installationFolder; + readonly cacheFolder: Uri; + + constructor(session: Session, location: Uri) { + strict.ok(location.scheme === 'file', `local registry location must be a file uri (${location})`); + + super(session, location); + this.cacheFolder = location; + this.indexYaml = this.cacheFolder.join(registryIndexFile); + this.installationFolder = session.installFolder.join(this.localName); + } + + update(): Promise { + return this.regenerate(); + } + + override async load(force?: boolean): Promise { + if (force || !this.loaded) { + if (! await this.indexYaml.exists()) { + // generate an index from scratch + await this.regenerate(); + this.loaded = true; + return; + } + this.session.channels.debug(`Loading registry from '${this.indexYaml.fsPath}'`); + this.index.deserialize(parse(await this.indexYaml.readUTF8())); + this.loaded = true; + } + } + + private get localName() { + // We use this to generate the subdirectory that we install artifacts into. + // It's not reqired to be very unique, but we'll generate it based of the path of the local location. + return createHash('sha256').update(this.location.fsPath, 'utf8').digest('hex').substring(0, 8); + } +} diff --git a/ce/ce/registries/RemoteRegistry.ts b/ce/ce/registries/RemoteRegistry.ts new file mode 100644 index 0000000000..02700fd70b --- /dev/null +++ b/ce/ce/registries/RemoteRegistry.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { parse } from 'yaml'; +import { ZipUnpacker } from '../archivers/ZipUnpacker'; +import { Registry } from '../artifacts/registry'; +import { registryIndexFile } from '../constants'; +import { acquireArtifactFile } from '../fs/acquire'; +import { i } from '../i18n'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { ArtifactIndex } from './artifact-index'; +import { ArtifactRegistry } from './ArtifactRegistry'; +import { Index } from './indexer'; + + +export class RemoteRegistry extends ArtifactRegistry implements Registry { + + protected indexYaml: Uri; + readonly installationFolder; + readonly cacheFolder: Uri; + #localName: string | undefined; + + constructor(session: Session, location: Uri) { + strict.ok(location.scheme === 'https', `remote registry location must be an HTTPS uri (${location})`); + super(session, location); + this.cacheFolder = session.registryFolder.join(this.localName); + this.indexYaml = this.cacheFolder.join(registryIndexFile); + this.installationFolder = session.installFolder.join(this.localName); + } + + /* + notes: + // does this look like a github repo (in which case assume '${url}/archive/refs/heads/main.zip') as the packed repo. + // does this point to a .zip file ? + // https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip + */ + private get localName() { + if (!this.#localName) { + // if this is this a reference to a github repo, use the org/repo name + this.#localName = /^https:\/\/github.com\/([a-zA-Z0-9-_]*\/[a-zA-Z0-9-_]*)\/?(.*)$/gi.exec(this.location.toString())?.[1]; + + // if we didn't get a match, use the url to generate a local filesystem name + if (!this.#localName) { + this.#localName = this.location.toString().replace(/^[a-zA-Z0-9]*:\/*/g, '').replace(/[^a-zA-Z0-9/]/g, '_'); + } + } + return this.#localName; + } + + private get safeName() { + return this.localName.replace(/[^a-zA-Z0-9]/g, '.'); + } + + override async load(force?: boolean): Promise { + + if (force || !this.loaded) { + if (!await this.indexYaml.exists()) { + await this.update(); + } + + strict.ok(await this.indexYaml.exists(), `Index file is missing '${this.indexYaml.fsPath}'`); + + // load it fresh. + this.index = new Index(ArtifactIndex); + + this.session.channels.debug(`Loading registry from '${this.indexYaml.fsPath}'`); + this.index.deserialize(parse(await this.indexYaml.readUTF8())); + this.loaded = true; + } + } + + async update() { + this.session.channels.message(i`Updating registry data from ${this.location.toString()}`); + + // get zip file location if + const ref = /^https:\/\/github.com\/([a-zA-Z0-9-_]*\/[a-zA-Z0-9-_]*\/?)$/gi.exec(this.location.toString()); + let locations = [this.location]; + if (ref) { + // it's just a github uri, let's use the main/m*ster branch as the zip file location. + locations = [this.location.join('archive/refs/heads/main.zip'), this.location.join('archive/refs/heads/master.zip')]; + } + + const file = await acquireArtifactFile(this.session, [this.location], `${this.safeName}-registry.zip`, {}); + if (await file.exists()) { + const unpacker = new ZipUnpacker(this.session); + await unpacker.unpack(file, this.cacheFolder, {}, { strip: 1 }); + await file.delete(); + } + } +} diff --git a/ce/ce/registries/aggregate-registry.ts b/ce/ce/registries/aggregate-registry.ts new file mode 100644 index 0000000000..fed4f53a91 --- /dev/null +++ b/ce/ce/registries/aggregate-registry.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Artifact } from '../artifacts/artifact'; +import { Registry, SearchCriteria } from '../artifacts/registry'; +import { FileSystem } from '../fs/filesystem'; +import { i } from '../i18n'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; +import { Registries } from './registries'; + +export class AggregateRegistry extends Registries implements Registry { + readonly count = 0; + + get loaded(): boolean { + return true; + } + + constructor(session: Session) { + super(session); + } + override search(criteria?: SearchCriteria): Promise]>> + override search(parent: Registries, criteria?: SearchCriteria): Promise]>> + override async search(parentOrCriteria?: Registries | SearchCriteria, criteria?: SearchCriteria): Promise]>> { + const parent = parentOrCriteria instanceof Registries ? parentOrCriteria : this; + criteria = criteria || parentOrCriteria; + + const [source, name] = this.session.parseName(criteria?.idOrShortName || ''); + if (source !== 'default') { + // search the explicitly asked for registry. + return this.getRegistryWithNameOrLocation(source).search(parent, { ...criteria, idOrShortName: name }); + } + + // search them all + return (await Promise.all([...this].map(async ([registry,]) => await registry.search(parent, criteria)))).flat(); + } + + override getRegistryName(registry: Registry): string { + for (const [name, reg] of this.registries) { + if (reg === registry) { + return name; + } + } + // worst-case scenario if we don't have a name in the parent context. + return registry.location.scheme === 'file' ? `[${registry.location.fsPath}]` : `[${registry.location.toString()}]`; + } + + async load(force?: boolean): Promise { + await Promise.all([...this].map(async ([registry,]) => registry.load(force))); + } + + save(): Promise { + // nothing to save. + return Promise.resolve(); + } + update(): Promise { + // nothing to update. + return Promise.resolve(); + } + regenerate(): Promise { + // nothing to regenerate + return Promise.resolve(); + } + + openArtifact(manifestPath: string): Promise { + throw new Error('Method not implemented.'); + } + + openArtifacts(manifestPaths: Array): Promise>> { + throw new Error('Method not implemented.'); + } + + readonly installationFolder = Uri.parse(undefined, 'artifacts:installFolder'); + readonly location = Uri.invalid; + + override getRegistry(id: string): Registry | undefined { + if (id === 'default') { + return this; + } + return this.registries.get(id.toString()); + } + + override getRegistryWithNameOrLocation(registryNameOrUri: string) { + if (registryNameOrUri === 'default') { + return this; + } + const result = this.getRegistry(registryNameOrUri); + if (!result) { + throw new Error(i`Unknown registry '${registryNameOrUri}'`); + } + return result; + } +} \ No newline at end of file diff --git a/ce/ce/registries/artifact-index.ts b/ce/ce/registries/artifact-index.ts new file mode 100644 index 0000000000..f9c84d00ba --- /dev/null +++ b/ce/ce/registries/artifact-index.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SemVer } from 'semver'; +import { MetadataFile } from '../amf/metadata-file'; +import { IdentityKey, IndexSchema, SemverKey, StringKey } from './indexer'; + + +export class ArtifactIndex extends IndexSchema { + id = new IdentityKey(this, (i) => i.info.id); + version = new SemverKey(this, (i) => new SemVer(i.info.version)); + summary = new StringKey(this, (i) => i.info.summary); +} diff --git a/ce/ce/registries/https-registry.ts b/ce/ce/registries/https-registry.ts new file mode 100644 index 0000000000..5f34b52a07 --- /dev/null +++ b/ce/ce/registries/https-registry.ts @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + diff --git a/ce/ce/registries/indexer.ts b/ce/ce/registries/indexer.ts new file mode 100644 index 0000000000..056bee94f7 --- /dev/null +++ b/ce/ce/registries/indexer.ts @@ -0,0 +1,575 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { Range, SemVer } from 'semver'; +import BTree from 'sorted-btree'; +import { isIterable } from '../util/checks'; +import { intersect } from '../util/intersect'; +import { Dictionary, entries, keys, ManyMap } from '../util/linq'; + +/* eslint-disable @typescript-eslint/ban-types */ + +/** Keys have to support toString so that we can serialize them */ +interface HasToString { + toString(): string; +} + +/** + * An Index is the means to search a registry + * + * @param TGraph The type of object to create an index for + * @param TIndexSchema the custom index schema (layout). + */ +export class Index> { + /** @internal */ + indexSchema: TIndexSchema; + /** @internal */ + indexOfTargets = new Array(); + + /** + * Creates an index for fast searching. + * + * @param indexConstructor the class for the custom index. + */ + constructor(protected indexConstructor: new (index: Index) => TIndexSchema) { + this.indexSchema = new indexConstructor(this); + } + + reset() { + this.indexSchema = new this.indexConstructor(this); + } + + /** + * Serializes the index to a javascript object graph that can be persisted. + */ + serialize() { + return { + items: this.indexOfTargets, + indexes: this.indexSchema.serialize() + }; + } + + /** + * Deserializes an object graph to the expected indexes. + * + * @param content the object graph to deserialize. + */ + deserialize(content: any) { + this.indexOfTargets = content.items; + this.indexSchema.deserialize(content.indexes); + } + + /** + * Returns a clone of the index that can be searched, which narrows the list of + */ + get where(): TIndexSchema { + // clone the index so that the consumer can filter on it. + const index = new Index(this.indexConstructor); + index.indexOfTargets = this.indexOfTargets; + for (const [key, impl] of this.indexSchema.mapOfKeyObjects.entries()) { + index.indexSchema.mapOfKeyObjects.get(key)!.cloneKey(impl); + } + return index.indexSchema; + } + + /** inserts an object into the index */ + insert(content: TGraph, target: string) { + const n = this.indexOfTargets.push(target) - 1; + const start = process.uptime() * 1000; + for (const indexKey of this.indexSchema.mapOfKeyObjects.values()) { + indexKey.insert(content, n); + } + } + + doneInsertion() { + for (const indexKey of this.indexSchema.mapOfKeyObjects.values()) { + indexKey.doneInsertion(); + } + } +} + +/** deconstructs a function declaration so that we can figure out the most logical path */ +function deconstruct(accessor: string) { + const params = /\(?([^(=)]+)\)?=>/g.exec(accessor); + let names = ['i']; + if (params) { + names = params[1].split(',').map(each => each.trim()); + } + + // find the part that looks for the value + let path = names.slice(1); + const expression = /=>.*i\.(.*?)(;| |\n|$)/g.exec(accessor); + if (expression) { + path = expression[1].replace(/\[.*?\]/g, '').replace(/[^\w.]/g, '').replace(/\.+/g, '.').split('.'); + } + + strict.ok(path.length, 'Unable to deconstruct the path to the element'); + + return path; +} + +/** + * A Key is a means to creating a searchable, sortable index + */ +abstract class Key> { + + /** child class must implement a standard compare function */ + abstract compare(a: TKey, b: TKey): number; + + /** child class must implement a function to transform value into comparable key */ + abstract coerce(value: TKey | string): TKey; + + protected nestedKeys = new Array>(); + protected values = new BTree>(undefined, this.compare); + protected words = new BTree>(); + protected indexSchema: TIndexSchema; + protected path: Array; + + /** attaches a nested key in the index. */ + with>>(nestedKey: TNestedKey): Key & TNestedKey { + for (const child of keys(nestedKey)) { + this.nestedKeys.push(nestedKey[child]); + } + return intersect(this, nestedKey); + } + + /** returns the textual 'identity' of this key */ + get identity() { + return `${this.constructor.name}/${this.path.join('.')}`; + } + + /** persists the key to an object graph */ + serialize() { + const result = { + keys: {}, + words: {}, + }; + for (const each of this.values.entries()) { + result.keys[each[0]] = [...each[1]]; + } + for (const each of this.words.entries()) { + result.words[each[0]] = [...each[1]]; + } + return result; + } + + /** deserializes an object graph back into this key */ + deserialize(content: any) { + for (const [key, ids] of entries(content.keys)) { + this.values.set(this.coerce(key), new Set(ids)); + } + for (const [key, ids] of entries(content.words)) { + this.words.set(key, new Set(ids)); + } + } + + /** @internal */ + cloneKey(from: this) { + this.values = from.values.greedyClone(); + this.words = from.words.greedyClone(); + } + + /** adds key value to this Key */ + protected addKey(each: TKey, n: number) { + let set = this.values.get(each); + if (!set) { + set = new Set(); + this.values.set(each, set); + } + set.add(n); + } + + /** adds a 'word' value to this key */ + protected addWord(each: TKey, n: number) { + const words = each.toString().split(/(\W+)/g); + + for (let word = 0; word < words.length; word += 2) { + for (let i = word; i < words.length; i += 2) { + const s = words.slice(word, i + 1).join(''); + if (s && s.indexOf(' ') === -1) { + let set = this.words.get(s); + if (!set) { + set = new Set(); + this.words.set(s, set); + } + set.add(n); + } + } + } + + } + + /** processes an object to generate key/word values for it. */ + insert(graph: TGraph, n: number) { + let value = this.accessor(graph); + if (value) { + value = >(Array.isArray(value) ? value + : typeof value === 'string' ? [value] + : isIterable(value) ? [...value] : [value]); + + this.insertKey(graph, n, value); + } + } + + /** insert the key/word values and process any children */ + private insertKey(graph: TGraph, n: number, value: TKey | Iterable) { + if (isIterable(value)) { + for (const each of value) { + this.addKey(each, n); + this.addWord(each, n); + if (this.nestedKeys) { + for (const child of this.nestedKeys) { + const v = child.accessor(graph, each.toString()); + if (v) { + child.insertKey(graph, n, v); + } + } + } + } + } else { + this.addKey(value, n); + this.addWord(value, n); + } + } + + /** construct a Key */ + constructor(indexSchema: IndexSchema, public accessor: (value: TGraph, ...args: Array) => TKey | undefined | Array | Iterable) { + this.path = deconstruct(accessor.toString()); + this.indexSchema = indexSchema; + this.indexSchema.mapOfKeyObjects.set(this.identity, this); + } + + /** word search */ + contains(value: TKey | string): TIndexSchema { + if (value !== undefined && value !== '') { + const matches = this.words.get(value.toString()); + this.indexSchema.filter(matches || []); + } + return this.indexSchema; + } + + /** exact match search */ + equals(value: TKey | string): TIndexSchema { + if (value !== undefined && value !== '') { + const matches = this.values.get(this.coerce(value)); + this.indexSchema.filter(matches || []); + } + return this.indexSchema; + } + + /** metadata value is greater than search */ + greaterThan(value: TKey | string): TIndexSchema { + const max = this.values.maxKey(); + const set = new Set(); + if (max && value !== undefined && value !== '') { + this.values.forRange(this.coerce(value), max, true, (k, v) => { + for (const n of v) { + set.add(n); + } + }); + } + this.indexSchema.filter(set.values()); + return this.indexSchema; + } + + /** metadata value is less than search */ + lessThan(value: TKey | string): TIndexSchema { + const min = this.values.minKey(); + const set = new Set(); + if (min && value !== undefined && value !== '') { + value = this.coerce(value); + this.values.forRange(min, this.coerce(value), false, (k, v) => { + for (const n of v) { + set.add(n); + } + }); + } + this.indexSchema.filter(set.values()); + return this.indexSchema; + } + + /** regex search -- WARNING: slower */ + match(regex: string): TIndexSchema { + // This could be faster if we stored a reverse lookup + // array that had the id for each key, but .. I don't + // think the perf will suffer much doing it this way. + + const set = new Set(); + + for (const node of this.values.entries()) { + for (const id of node[1]) { + if (!this.indexSchema.selectedElements || this.indexSchema.selectedElements.has(id)) { + // it's currently in the keep list. + if (regex.match(node.toString())) { + set.add(id); + } + } + } + } + + this.indexSchema.filter(set.values()); + return this.indexSchema; + } + /** substring match -- slower */ + startsWith(value: TKey | string): TIndexSchema { + // ok, I'm being lazy here. I can add a check to see if we're past + // the point where this could be a match, but I don't know if I'll + // even need this enough to keep it. + + const set = new Set(); + + for (const node of this.values.entries()) { + for (const id of node[1]) { + if (!this.indexSchema.selectedElements || this.indexSchema.selectedElements.has(id)) { + // it's currently in the keep list. + if (node[0].toString().startsWith((value).toString())) { + set.add(id); + } + } + } + } + + this.indexSchema.filter(set.values()); + return this.indexSchema; + } + /** substring match -- slower */ + endsWith(value: TKey | string): TIndexSchema { + // Same thing here, but I'd have to do a reversal of all the strings. + + const set = new Set(); + + for (const node of this.values.entries()) { + for (const id of node[1]) { + if (!this.indexSchema.selectedElements || this.indexSchema.selectedElements.has(id)) { + // it's currently in the keep list. + if (node[0].toString().endsWith((value).toString())) { + set.add(id); + } + } + } + } + + this.indexSchema.filter(set.values()); + return this.indexSchema; + } + + doneInsertion() { + // nothing normally + } +} + +/** An key for string values. */ +export class StringKey> extends Key { + + compare(a: string, b: string): number { + if (a && b) { + return a.localeCompare(b); + } + if (a) { + return 1; + } + if (b) { + return -1; + } + return 0; + } + + /** impl: transform value into comparable key */ + coerce(value: string): string { + return value; + } +} + +function shortName(value: string, n: number) { + const v = value.split('/'); + let p = v.length - n; + if (p < 0) { + p = 0; + } + return v.slice(p).join('/'); +} + +export class IdentityKey> extends StringKey { + + protected identities = new BTree>(undefined, this.compare); + protected idShortName = new Map(); + + override doneInsertion() { + // go thru each of the values, find short name for each. + const ids = new ManyMap]>(); + + for (const idAndIndexNumber of this.values.entries()) { + ids.push(shortName(idAndIndexNumber[0], 1), idAndIndexNumber); + } + + let n = 1; + while (ids.size > 0) { + n++; + for (const [snKey, artifacts] of [...ids.entries()]) { + // remove it from the list. + ids.delete(snKey); + if (artifacts.length === 1) { + // keep this one, it's unique + this.identities.set(snKey, artifacts[0][1]); + this.idShortName.set(artifacts[0][0], snKey); + } else { + for (const each of artifacts) { + ids.push(shortName(each[0], n), each); + } + } + } + } + } + + /** @internal */ + override cloneKey(from: this) { + super.cloneKey(from); + this.identities = from.identities.greedyClone(); + this.idShortName = new Map(from.idShortName); + } + + getShortNameOf(id: string) { + return this.idShortName.get(id); + } + + nameOrShortNameIs(value: string): TIndexSchema { + if (value !== undefined && value !== '') { + const matches = this.identities.get(value); + if (matches) { + this.indexSchema.filter(matches); + } + else { + return this.equals(value); + } + } + return this.indexSchema; + } + + /** deserializes an object graph back into this key */ + override deserialize(content: any) { + super.deserialize(content); + this.doneInsertion(); + } +} + +/** An key for string values. Does not support 'word' searches */ +export class SemverKey> extends Key { + compare(a: SemVer, b: SemVer): number { + return a.compare(b); + } + coerce(value: SemVer | string): SemVer { + if (typeof value === 'string') { + return new SemVer(value); + } + return value; + } + protected override addWord(each: SemVer, n: number) { + // no parts + } + + rangeMatch(value: Range | string) { + + // This could be faster if we stored a reverse lookup + // array that had the id for each key, but .. I don't + // think the perf will suffer much doing it this way. + + const set = new Set(); + const range = new Range(value); + + for (const node of this.values.entries()) { + for (const id of node[1]) { + + if (!this.indexSchema.selectedElements || this.indexSchema.selectedElements.has(id)) { + // it's currently in the keep list. + if (range.test(node[0])) { + set.add(id); + } + } + } + } + + this.indexSchema.filter(set.values()); + return this.indexSchema; + } + + override serialize() { + const result = super.serialize(); + result.words = undefined; + + return result; + } +} + +/** + * Base class for a custom IndexSchema + * + * @param TGraph - the object kind to be indexing + * @param TSelf - the child class that is being constructed. + */ +export abstract class IndexSchema> { + /** the collection of keys in this IndexSchema */ + readonly mapOfKeyObjects = new Map>(); + + /** + * the selected element ids. + * + * if this is `undefined`, the whole set is currently selected + */ + selectedElements?: Set; + + /** + * filter the selected elements down to an intersetction of the {selectedelements} ∩ {idsToKeep} + * + * @param idsToKeep the element ids to intersect with. + */ + filter(idsToKeep: Iterable) { + if (this.selectedElements) { + const selected = new Set(); + for (const each of idsToKeep) { + if (this.selectedElements.has(each)) { + selected.add(each); + } + } + this.selectedElements = selected; + } else { + this.selectedElements = new Set(idsToKeep); + } + } + + /** + * Serializes this IndexSchema to a persistable object graph. + */ + serialize() { + const result = { + }; + for (const [key, impl] of this.mapOfKeyObjects.entries()) { + result[key] = impl.serialize(); + } + return result; + } + + /** + * Deserializes a persistable object graph into the IndexSchema. + * + * replaces any existing data in the IndexSchema. + * @param content the persistable object graph. + */ + deserialize(content: any) { + for (const [key, impl] of this.mapOfKeyObjects.entries()) { + impl.deserialize(content[key]); + } + } + + /** + * returns the selected + */ + get items(): Array { + return this.selectedElements ? [...this.selectedElements].map(each => this.index.indexOfTargets[each]) : this.index.indexOfTargets; + } + + /** @internal */ + constructor(public index: Index) { + } +} + diff --git a/ce/ce/registries/registries.ts b/ce/ce/registries/registries.ts new file mode 100644 index 0000000000..5ae327cb28 --- /dev/null +++ b/ce/ce/registries/registries.ts @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { fail } from 'assert'; +import { Artifact } from '../artifacts/artifact'; +import { Registry, SearchCriteria } from '../artifacts/registry'; +import { i } from '../i18n'; +import { Session } from '../session'; +import { linq } from '../util/linq'; +import { Uri } from '../util/uri'; + +export class Registries implements Iterable<[Registry, Array]> { + protected registries: Map = new Map(); + + get registryNames() { + return this.registries.keys(); + } + + constructor(protected session: Session) { + + } + + [Symbol.iterator](): Iterator<[Registry, Array]> { + return linq.entries(this.registries).groupBy(([name, registry]) => registry, ([name, registry]) => name).entries(); + } + + getRegistryName(registry: Registry): string { + for (const [name, reg] of this.registries) { + if (reg === registry && name.indexOf('://') === -1) { + return name; + } + } + // ask the default registry? + return this.session.defaultRegistry.getRegistryName(registry); + } + + getRegistry(id: string | Uri): Registry | undefined { + return this.registries.get(id.toString()); + } + + has(registryName?: string) { + // only check for registries names not locations. + if (registryName && registryName.indexOf('://') === -1) { + for (const [name] of this.registries) { + if (name === registryName && name.indexOf('://') === -1) { + return true; + } + } + } + return false; + } + + add(registry: Registry, name?: string): Registry { + const location = registry.location; + + // check if this is already recorded (by uri) + let r = this.registries.get(location.toString()); + if (r && r !== registry) { + throw new Error(`Registry with location ${location.toString()} already loaded in this context`); + } + + // check if this is already recorded (by common name) + if (name) { + r = this.registries.get(name); + if (r && r !== registry) { + throw new Error(`Registry with a different name ${name} already loaded in this context`); + } + this.registries.set(name, registry); + } + + // record it by uri + this.registries.set(location.toString(), registry); + + return registry; + } + + async search(criteria?: SearchCriteria): Promise]>> { + const [source, name] = this.session.parseName(criteria?.idOrShortName || ''); + const registry = this.getRegistryWithNameOrLocation(source); + + return registry.search(this, { ...criteria, idOrShortName: name }); + } + + /** + * returns an artifact for the strongly-named artifact id/version. + * + * @param idOrShortName the identity of the artifact. If the string has no ':' at the front, default source is assumed. + * @param version the version of the artifact + */ + async getArtifact(idOrShortName: string, version: string | undefined): Promise<[Registry, string, Artifact] | undefined> { + const artifacts = await this.search({ idOrShortName, version }); + + switch (artifacts.length) { + case 0: + // did not match a name or short name. + return undefined; // nothing matched. + + case 1: { + // found the artifact. awesome. + const [registry, artifactId, all] = artifacts[0]; + return [registry, artifactId, all[0]]; + } + + default: { + // multiple matches. + // we can't return a single artifact, we're going to have to throw. + fail(i`Artifact identity '${idOrShortName}' matched more than one result (${[...artifacts.map(each => each[1])].join(',')}).`); + } + } + } + + /** returns a registry given the name or uri */ + getRegistryWithNameOrLocation(registryNameOrUri: string) { + // check the default registry first + let result = this.session.defaultRegistry.getRegistry(registryNameOrUri); + if (result) { + return result; + } + + result = this.getRegistry(registryNameOrUri); + if (!result) { + throw new Error(i`Unknown registry '${registryNameOrUri}'`); + } + return result; + } +} + diff --git a/ce/ce/registries/standard-registry.ts b/ce/ce/registries/standard-registry.ts new file mode 100644 index 0000000000..0c3b0b2a70 --- /dev/null +++ b/ce/ce/registries/standard-registry.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { MetadataFile } from '../amf/metadata-file'; +import { Session } from '../session'; +import { Uri } from '../util/uri'; + +export const THIS_IS_NOT_A_MANIFEST_ITS_AN_INDEX_STRING = '# MANIFEST-INDEX'; + +export async function isIndexFile(uri: Uri): Promise { + try { + return (await uri.isFile()) && (await uri.readUTF8()).startsWith(THIS_IS_NOT_A_MANIFEST_ITS_AN_INDEX_STRING); + } catch { + return false; + } +} +export async function isMetadataFile(uri: Uri, session: Session): Promise { + if (await uri.isFile()) { + try { + return (await MetadataFile.parseMetadata(uri, session))?.info?.exists(); + } catch { + // nope. no worries. + } + } + return false; +} + diff --git a/ce/ce/session.ts b/ce/ce/session.ts new file mode 100644 index 0000000000..6a2fe4c7d4 --- /dev/null +++ b/ce/ce/session.ts @@ -0,0 +1,516 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { delimiter } from 'path'; +import { MetadataFile } from './amf/metadata-file'; +import { Activation } from './artifacts/activation'; +import { Artifact, InstalledArtifact } from './artifacts/artifact'; +import { Registry } from './artifacts/registry'; +import { defaultConfig, globalConfigurationFile, postscriptVarible, profileNames, registryIndexFile, undo, vcpkgDownloadFolder } from './constants'; +import { FileSystem, FileType } from './fs/filesystem'; +import { HttpsFileSystem } from './fs/http-filesystem'; +import { LocalFileSystem } from './fs/local-filesystem'; +import { schemeOf, UnifiedFileSystem } from './fs/unified-filesystem'; +import { VsixLocalFilesystem } from './fs/vsix-local-filesystem'; +import { i } from './i18n'; +import { installGit } from './installers/git'; +import { installNuGet } from './installers/nuget'; +import { installUnTar } from './installers/untar'; +import { installUnZip } from './installers/unzip'; +import { InstallEvents, InstallOptions } from './interfaces/events'; +import { Installer } from './interfaces/metadata/installers/Installer'; +import { AggregateRegistry } from './registries/aggregate-registry'; +import { LocalRegistry } from './registries/LocalRegistry'; +import { Registries } from './registries/registries'; +import { RemoteRegistry } from './registries/RemoteRegistry'; +import { isIndexFile, isMetadataFile } from './registries/standard-registry'; +import { Channels, Stopwatch } from './util/channels'; +import { Dictionary, entries } from './util/linq'; +import { Queue } from './util/promise'; +import { isFilePath, Uri } from './util/uri'; +import { isYAML } from './yaml/yaml'; + +/** The definition for an installer tool function */ +type InstallerTool = ( + session: Session, + activation: Activation, + name: string, + targetLocation: Uri, + install: T, + events: Partial, + options: Partial +) => Promise + + +export type Context = { [key: string]: Array | undefined; } & { + readonly os: string; + readonly arch: string; + readonly windows: boolean; + readonly osx: boolean; + readonly linux: boolean; + readonly freebsd: boolean; + readonly x64: boolean; + readonly x86: boolean; + readonly arm: boolean; + readonly arm64: boolean; +} + +interface BackupFile { + environment: Dictionary; + activation: Activation; +} + +/** + * The Session class is used to hold a reference to the + * message channels, + * the filesystems, + * and any other 'global' data that should be kept. + * + */ +export class Session { + /** @internal */ + readonly stopwatch = new Stopwatch(); + readonly fileSystem: FileSystem; + readonly channels: Channels; + readonly homeFolder: Uri; + readonly tmpFolder: Uri; + readonly installFolder: Uri; + readonly registryFolder: Uri; + + readonly globalConfig: Uri; + readonly cache: Uri; + currentDirectory: Uri; + configuration!: MetadataFile; + + /** register installer functions here */ + private installers = new Map([ + ['nuget', installNuGet], + ['unzip', installUnZip], + ['untar', installUnTar], + ['git', installGit] + ]); + + readonly defaultRegistry: AggregateRegistry; + private readonly registries = new Registries(this); + + telemetryEnabled = false; + + constructor(currentDirectory: string, public readonly context: Context, public readonly settings: Dictionary, public readonly environment: NodeJS.ProcessEnv) { + this.fileSystem = new UnifiedFileSystem(this). + register('file', new LocalFileSystem(this)). + register('vsix', new VsixLocalFilesystem(this)). + register(['https'], new HttpsFileSystem(this) + ); + + this.channels = new Channels(this); + + this.setupLogging(); + + this.homeFolder = this.fileSystem.file(settings['homeFolder']!); + this.cache = this.environment[vcpkgDownloadFolder] ? this.parseUri(this.environment[vcpkgDownloadFolder]!) : this.homeFolder.join('downloads'); + this.globalConfig = this.homeFolder.join(globalConfigurationFile); + + this.tmpFolder = this.homeFolder.join('tmp'); + this.installFolder = this.homeFolder.join('artifacts'); + + this.registryFolder = this.homeFolder.join('registries'); + + this.currentDirectory = this.fileSystem.file(currentDirectory); + + // add built in registries + this.defaultRegistry = new AggregateRegistry(this); + } + + parseUri(uriOrPath: string | Uri): Uri { + return (typeof uriOrPath === 'string') ? isFilePath(uriOrPath) ? this.fileSystem.file(uriOrPath) : this.fileSystem.parse(uriOrPath) : uriOrPath; + } + + async parseLocation(location?: string): Promise { + if (location) { + const scheme = schemeOf(location); + // file uri or drive letter + if (scheme) { + if (scheme.toLowerCase() !== 'file' && scheme.length !== 1) { + // anything else with a colon isn't a legal path in any way. + return undefined; + } + // must be a file path of some kind. + const uri = this.parseUri(location); + return await uri.exists() ? uri : undefined; + } + + // is it an absolute path? + if (location.startsWith('/') || location.startsWith('\\')) { + const uri = this.fileSystem.file(location); + return await uri.exists() ? uri : undefined; + } + + // is it a path relative to the current directory? + const uri = this.currentDirectory.join(location); + return await uri.exists() ? uri : undefined; + } + return undefined; + } + + + loadRegistry(registryLocation: Uri | string | undefined, registryKind = 'artifact'): Registry | undefined { + if (registryLocation) { + const r = this.registries.getRegistry(registryLocation.toString()); + + if (r) { + return r; + } + + // not already loaded + registryLocation = this.parseUri(registryLocation); + + switch (registryKind) { + + case 'artifact': + switch (registryLocation.scheme) { + case 'https': + return this.registries.add(new RemoteRegistry(this, registryLocation)); + + case 'file': + return this.registries.add(new LocalRegistry(this, registryLocation)); + + default: + throw new Error(i`Unsupported registry scheme '${registryLocation.scheme}'`); + } + } + throw new Error(i`Unsupported registry kind '${registryKind}'`); + } + + return undefined; + } + + async isLocalRegistry(location: Uri | string): Promise { + location = this.parseUri(location); + + if (location.scheme === 'file') { + if (await isIndexFile(location)) { + return true; + } + + if (await location.isDirectory()) { + const index = location.join(registryIndexFile); + if (await isIndexFile(index)) { + return true; + } + const s = this; + let result = false; + const q = new Queue(); + + // still could be a folder of artifact files + // eslint-disable-next-line no-inner-declarations + async function process(folder: Uri) { + for (const [entry, type] of await folder.readDirectory()) { + if (result) { + return; + } + + if (type & FileType.Directory) { + await process(entry); + continue; + } + + if (type & FileType.File && isYAML(entry.path)) { + void q.enqueue(async () => { result = result || await isMetadataFile(entry, s); }); + } + } + } + await process(location); + await q.done; + return result; // whatever we guess, we'll use + } + return false; + } + + return false; + } + + async isRemoteRegistry(location: Uri | string): Promise { + return this.parseUri(location).scheme === 'https'; + } + + parseName(id: string): [string, string] { + const parts = id.split(':'); + switch (parts.length) { + case 0: + throw new Error(i`Invalid artifact id '${id}'`); + case 1: + return ['default', id]; + } + return <[string, string]>parts; + } + + get vcpkgInstalled(): Promise { + return this.homeFolder.exists('.vcpkg-root'); + } + + async saveConfig() { + await this.configuration.save(this.globalConfig); + } + + #postscriptFile?: Uri; + get postscriptFile() { + return this.#postscriptFile || (this.#postscriptFile = this.environment[postscriptVarible] ? this.fileSystem.file(this.environment[postscriptVarible]!) : undefined); + } + + async init() { + // load global configuration + if (!await this.fileSystem.isDirectory(this.homeFolder)) { + // let's create the folder + try { + await this.fileSystem.createDirectory(this.homeFolder); + } catch (error: any) { + // if this throws, let it + this.channels.debug(error?.message); + } + // check if it got made, because at an absolute minimum, we need a folder, so failing this is catastrophic. + strict.ok(await this.fileSystem.isDirectory(this.homeFolder), i`Fatal: The root folder '${this.homeFolder.fsPath}' can not be created`); + } + + if (!await this.fileSystem.isFile(this.globalConfig)) { + try { + await this.globalConfig.writeUTF8(defaultConfig); + } catch { + // if this throws, let it + } + // check if it got made, because at an absolute minimum, we need the config file, so failing this is catastrophic. + strict.ok(await this.fileSystem.isFile(this.globalConfig), i`Fatal: The global configuration file '${this.globalConfig.fsPath}' can not be created`); + } + + // got past the checks, let's load the configuration. + this.configuration = await MetadataFile.parseMetadata(this.globalConfig, this); + this.channels.debug(`Loaded global configuration file '${this.globalConfig.fsPath}'`); + + // load the registries + for (const [name, regDef] of this.configuration.registries) { + const loc = regDef.location.get(0); + if (loc) { + const uri = this.parseUri(loc); + const reg = this.loadRegistry(uri, regDef.registryKind); + if (reg) { + this.channels.debug(`Loaded global manifest ${name} => ${uri.formatted}`); + this.defaultRegistry.add(reg, name); + } + } + } + + if (this.context['sendmetrics']) { + // is it forced to be on? + this.telemetryEnabled = true; + } else { + // otherwise, check for the file that turns it off. + if (await this.vcpkgInstalled) { + this.telemetryEnabled = ! await this.homeFolder.exists('vcpkg.disable-metrics'); + } + } + + return this; + } + + async findProjectProfile(startLocation = this.currentDirectory, search = true): Promise { + let location = startLocation; + for (const loc of profileNames) { + const path = location.join(loc); + if (await this.fileSystem.isFile(path)) { + return path; + } + } + location = location.join('..'); + if (search) { + return (location.toString() === startLocation.toString()) ? undefined : this.findProjectProfile(location); + } + return undefined; + } + + #postscript = new Dictionary(); + addPostscript(variableName: string, value: string) { + this.#postscript[variableName] = value; + } + + async deactivate() { + // get the deactivation information + const lastEnv = this.environment[undo]; + + // remove the variable first. + delete this.environment[undo]; + this.addPostscript(undo, ''); + + if (lastEnv) { + const fileUri = this.parseUri(lastEnv); + if (await fileUri.exists()) { + const contents = await fileUri.readUTF8(); + await fileUri.delete(); + + if (contents) { + try { + const original = JSON.parse(contents, (k, v) => this.deserializer(k, v)); + + // reset the environment variables + // and queue them up in the postscript + for (const [variable, value] of entries(original.environment)) { + if (value) { + this.environment[variable] = value; + this.addPostscript(variable, value); + } else { + delete this.environment[variable]; + this.addPostscript(variable, ''); + } + } + + // in the paths, let's remove all the entries + for (const [variable, uris] of original.activation.paths.entries()) { + let pathLikeVariable = this.environment[variable]; + if (pathLikeVariable) { + for (const uri of uris) { + pathLikeVariable = pathLikeVariable.replace(uri.fsPath, ''); + } + const rx = new RegExp(`${delimiter}+`, 'g'); + pathLikeVariable = pathLikeVariable.replace(rx, delimiter).replace(/^;|;$/g, ''); + // persist that. + this.environment[variable] = pathLikeVariable; + this.addPostscript(variable, pathLikeVariable); + } + } + } catch { + // file not valid, bail. + } + } + } + } + } + + async setActivationInPostscript(activation: Activation, backupEnvironment = true) { + + // capture any variables that we set. + const contents = { environment: {}, activation }; + + // build PATH style variable for the environment + for (const [variable, values] of activation.Paths) { + + if (values.length) { + // add the new values first; existing values are added after. + const s = new Set(values.map(each => each.fsPath)); + const originalVariable = this.environment[variable] || ''; + if (originalVariable) { + for (const p of originalVariable.split(delimiter)) { + if (p) { + s.add(p); + } + } + } + contents.environment[variable] = originalVariable; + this.addPostscript(variable, [...s.values()].join(delimiter)); + } + // for path activations, we undo specific entries, so we don't store the variable here (in case the path is modified after) + } + + for (const [variable, value] of activation.Variables) { + this.addPostscript(variable, value); + contents.environment[variable] = this.environment[variable] || ''; // track the original value + } + + // for now. + if (activation.defines.size > 0) { + this.addPostscript('DEFINES', activation.Defines.map(([define, value]) => `${define}=${value}`).join(' ')); + } + + if (backupEnvironment) { + // create the environment backup file + const backupFile = this.tmpFolder.join(`previous-environment-${Date.now().toFixed()}.json`); + + await backupFile.writeUTF8(JSON.stringify(contents, (k, v) => this.serializer(k, v), 2)); + this.addPostscript(undo, backupFile.toString()); + } + } + + async writePostscript() { + let content = ''; + const psf = this.postscriptFile; + if (psf) { + switch (psf?.fsPath.substr(-3)) { + case 'ps1': + // update environment variables. (powershell) + content += [...entries(this.#postscript)].map((k, v) => { return `$\{ENV:${k[0]}}="${k[1]}"`; }).join('\n'); + break; + + case 'cmd': + // update environment variables. (cmd) + content += [...entries(this.#postscript)].map((k) => { return `set ${k[0]}=${k[1]}`; }).join('\r\n'); + break; + + case '.sh': + // update environment variables. (posix)' + content += [...entries(this.#postscript)].map((k, v) => { + return k[1] ? `export ${k[0]}="${k[1]}"` : `unset ${k[0]}`; + }).join('\n'); + } + + if (content) { + await psf.writeUTF8(content); + } + } + } + + setupLogging() { + // at this point, we can subscribe to the events in the export * from './lib/version';FileSystem and Channels + // and do what we need to do (record, store, etc.) + // + // (We'll defer actually this until we get to #23: Create Bug Report) + // + // this.FileSystem.on('deleted', (uri) => { console.debug(uri) }) + } + + async getInstalledArtifacts() { + const result = new Array<{ folder: Uri, id: string, artifact: Artifact }>(); + if (! await this.installFolder.exists()) { + return result; + } + for (const [folder, stat] of await this.installFolder.readDirectory(undefined, { recursive: true })) { + try { + const metadata = await MetadataFile.parseMetadata(folder.join('artifact.yaml'), this); + result.push({ + folder, + id: metadata.info.id, + artifact: await new InstalledArtifact(this, metadata).init(this) + }); + } catch { + // not a valid install. + } + } + return result; + } + + /** returns an installer function (or undefined) for a given installerkind */ + artifactInstaller(installInfo: Installer) { + return this.installers.get(installInfo.installerKind); + } + + async openManifest(manifestFile: Uri): Promise { + return await MetadataFile.parseConfiguration(manifestFile.fsPath, await manifestFile.readUTF8(), this); + } + + serializer(key: any, value: any) { + if (value instanceof Map) { + return { dataType: 'Map', value: Array.from(value.entries()) }; + } + return value; + } + + deserializer(key: any, value: any) { + if (typeof value === 'object' && value !== null) { + switch (value.dataType) { + case 'Map': + return new Map(value.value); + } + if (value.scheme && value.path) { + return this.fileSystem.from(value); + } + } + return value; + } +} diff --git a/ce/ce/tsconfig.json b/ce/ce/tsconfig.json new file mode 100644 index 0000000000..274fee9ec6 --- /dev/null +++ b/ce/ce/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../common/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "types": [ + "node" + ], + "inlineSourceMap": true + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + ".scripts/**", + "**/dist/**", + "test/scenarios/**", + "resources", + "node_modules/**", + "**/*.d.ts" + ] +} \ No newline at end of file diff --git a/ce/ce/util/channels.ts b/ce/ce/util/channels.ts new file mode 100644 index 0000000000..8e5b59322c --- /dev/null +++ b/ce/ce/util/channels.ts @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { EventEmitter } from 'ee-ts'; +import { Session } from '../session'; + +/** Event defintions for channel events */ +export interface ChannelEvents { + warning(text: string, context: any, msec: number): void; + error(text: string, context: any, msec: number): void; + message(text: string, context: any, msec: number): void; + debug(text: string, context: any, msec: number): void; +} + +/** + * @internal + * + * Tracks timing of events +*/ +export class Stopwatch { + start: number; + last: number; + constructor() { + this.last = this.start = process.uptime() * 1000; + } + get time() { + const now = process.uptime() * 1000; + const result = Math.floor(now - this.last); + this.last = now; + return result; + } + get total() { + const now = process.uptime() * 1000; + return Math.floor(now - this.start); + } +} + +/** Exposes a set of events that are used to communicate with the user + * + * Warning, Error, Message, Debug + */ +export class Channels extends EventEmitter { + /** @internal */ + readonly stopwatch: Stopwatch; + + warning(text: string, context?: any) { + this.emit('warning', text, context, this.stopwatch.total); + } + error(text: string, context?: any) { + this.emit('error', text, context, this.stopwatch.total); + } + message(text: string, context?: any) { + this.emit('message', text, context, this.stopwatch.total); + } + debug(text: string, context?: any) { + this.emit('debug', text, context, this.stopwatch.total); + } + constructor(session: Session) { + super(); + this.stopwatch = session.stopwatch; + } +} diff --git a/ce/ce/util/checks.ts b/ce/ce/util/checks.ts new file mode 100644 index 0000000000..55ae74fffc --- /dev/null +++ b/ce/ce/util/checks.ts @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isScalar, isSeq, YAMLMap } from 'yaml'; +import { i } from '../i18n'; +import { ErrorKind } from '../interfaces/error-kind'; +import { ValidationError } from '../interfaces/validation-error'; + +/** @internal */ +export function isPrimitive(value: any): value is (string | number | boolean) { + switch (typeof value) { + case 'string': + case 'number': + case 'boolean': + return true; + } + return false; +} + +/** @internal */ +export function isNullish(value: any): value is null | undefined { + return value === null || value === undefined || value === ''; +} + +/** @internal */ +export function isIterable(source: any): source is Iterable { + return !!source && typeof (source) !== 'string' && !!source[Symbol.iterator]; +} + +export function* checkOptionalString(parent: YAMLMap, range: [number, number, number], name: string): Iterable { + switch (typeof parent.get(name)) { + case 'string': + case 'undefined': + break; + default: + yield { message: i`${name} must be a string`, range: range, category: ErrorKind.IncorrectType }; + } +} + +export function* checkOptionalBool(parent: YAMLMap, range: [number, number, number], name: string): Iterable { + switch (typeof parent.get(name)) { + case 'boolean': + case 'undefined': + break; + default: + yield { message: i`${name} must be a bool`, range: range, category: ErrorKind.IncorrectType }; + } +} + +function checkOptionalArrayOfStringsImpl(parent: YAMLMap, range: [number, number, number], name: string): boolean { + const val = parent.get(name); + if (isSeq(val)) { + for (const entry of val.items) { + if (!isScalar(entry) || typeof entry.value !== 'string') { + return true; + } + } + } else if (typeof val !== 'undefined') { + return true; + } + + return false; +} + +export function* checkOptionalArrayOfStrings(parent: YAMLMap, range: [number, number, number], name: string): Iterable { + if (checkOptionalArrayOfStringsImpl(parent, range, name)) { + yield { message: i`${name} must be an array of strings, or unset`, range: range, category: ErrorKind.IncorrectType }; + } +} diff --git a/ce/ce/util/credentials.ts b/ce/ce/util/credentials.ts new file mode 100644 index 0000000000..eb16a7eb52 --- /dev/null +++ b/ce/ce/util/credentials.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export interface Credentials { + githubToken?: string +} diff --git a/ce/ce/util/evaluator.ts b/ce/ce/util/evaluator.ts new file mode 100644 index 0000000000..1dabc70d4f --- /dev/null +++ b/ce/ce/util/evaluator.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { delimiter } from 'path'; +import { createSandbox } from '../util/safeEval'; +import { isPrimitive } from './checks'; + +/** sandboxed eval function for evaluating expressions */ +const safeEval: (code: string, context?: any) => T = createSandbox(); + +function proxifyObject(obj: Record): any { + return new Proxy(obj, { + get(target, prop) { + if (typeof prop === 'string') { + let result = target[prop]; + // check for a direct match first + if (!result) { + // go thru the properties and check for a case-insensitive match + for (const each of Object.keys(target)) { + if (each.toLowerCase() === prop.toLowerCase()) { + result = target[each]; + break; + } + } + } + if (result) { + if (Array.isArray(result)) { + return result; + } + if (typeof result === 'object') { + return proxifyObject(result); + } + if (isPrimitive(result)) { + return result; + } + } + return undefined; + } + }, + }); + +} + +export class Evaluator { + private activation: any; + private host: any; + + constructor(private artifactData: Record, host: Record, activation: Record) { + this.host = proxifyObject(host); + this.activation = proxifyObject(activation); + + } + + evaluate(text: string | undefined): string | undefined { + if (!text || text.indexOf('$') === -1) { + // quick exit if no expression or no variables + return text; + } + + // $$ -> escape for $ + text = text.replace(/\$\$/g, '\uffff'); + + // $0 ... $9 -> replace contents with the values from the artifact + text = text.replace(/\$([0-9])/g, (match, index) => this.artifactData[match] || match); + + // $ -> expression value + text = text.replace(/\$([a-zA-Z_.][a-zA-Z0-9_.]*)/g, (match, expression) => { + + if (expression.startsWith('host.')) { + // this is getting something from the host context (ie, environment variable) + return safeEval(expression.substr(5), this.host) || match; + } + + // otherwise, assume it is a property on the activation object + return safeEval(expression, this.activation) || match; + }); + + // ${ ...} in non-verify mode, the contents are just returned + text = text.replace(/\$\{(.*?)\}/g, '$1'); + + // restore escaped $ + text = text.replace(/\uffff/g, '$'); + + return text; + } + + expandPaths(value: string, delim = delimiter): Array { + let n = undefined; + + const parts = value.split(/(\$[a-zA-Z0-9.]+?)/g).filter(each => each).map((part, i) => { + + const value = this.evaluate(part) || ''; + + if (value.indexOf(delim) !== -1) { + n = i; + } + + return value; + }); + + if (n === undefined) { + // if the value didn't have a path separator, then just return the value + return [parts.join('')]; + } + + const front = parts.slice(0, n).join(''); + const back = parts.slice(n + 1).join(''); + + return parts[n].split(delim).filter(each => each).map(each => `${front}${each}${back}`); + } + + async evaluateAndVerify(expression: string | undefined): Promise { + return ''; + } +} \ No newline at end of file diff --git a/ce/ce/util/events.ts b/ce/ce/util/events.ts new file mode 100644 index 0000000000..17e260b818 --- /dev/null +++ b/ce/ce/util/events.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { EventEmitter as eventemitter } from 'ee-ts'; +import { EventEmitter } from 'events'; +import { Stream } from 'stream'; +import { promisify } from 'util'; + +/** + * Creates a promise that resolves after a delay + * + * @param delayMS the length of time to delay in milliseconds. + */ +export function delay(delayMS: number): Promise { + return new Promise(res => setTimeout(res, delayMS)); +} + +export type Emitter = Pick; + +/** + * This can proxy event emitters to other sources. + * Used with an intersect() call to get a promise that has events. +*/ +export class EventForwarder implements Emitter { + #emitters = new Array(); + #subscriptions = new Array<[string, any]>(); + + /** + * @internal + * + * registers the actual event emitter with the forwarder. + * + * if events were subscribed to before the emitter is registered, we're going + * to forward on those subscriptions now. + */ + register(emitter: UnionOfEmiters) { + for (const [event, listener] of this.#subscriptions) { + emitter.on(event, listener); + } + this.#emitters.push(emitter); + } + + /** + * Lets our forwarder pass on events to subscribe to + * + * @remarks we're going to cache these subscriptions, since if the consumer starts subscribing before we + * actually register the emitter, we'll have to subscribe at registration time. + * + * @param event the event to subscribe to + * @param listener the callback for the listener + */ + on(event: any, listener: any) { + this.#subscriptions.push([event, listener]); + for (const emitter of this.#emitters) { + emitter.on(event, listener); + } + return this; + } + + off(event: any, listener: any) { + for (const emitter of this.#emitters) { + emitter.off(event, listener); + } + return this; + } +} +/** + * creates a awaitable promise for a given event. + * @param eventEmitter the event emitter + * @param event the event name + */ +export function async(eventEmitter: EventEmitter, event: string | symbol) { + return promisify(eventEmitter.once)(event); +} + +export function completed(stream: Stream): Promise { + return new Promise((resolve, reject) => { + stream.once('end', resolve); + stream.once('error', reject); + }); +} + +const ignore = new Set([ + 'constructor', + '__defineGetter__', + '__defineSetter__', + 'hasOwnProperty', + '__lookupGetter__', + '__lookupSetter__', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toString', + 'valueOf', + 'toLocaleString' +]); + +function getMethods(obj: T) { + const properties = new Set(); + let current = obj; + do { + Object.getOwnPropertyNames(current).map(item => properties.add(item)); + } while ((current = Object.getPrototypeOf(current))); + return [...properties].filter(item => (!ignore.has(item)) && typeof (obj)[item] === 'function'); +} + +export class ExtendedEmitter extends eventemitter { + subscribe(listener?: Partial) { + if (listener) { + for (const each of getMethods(listener)) { + this.on(each, (listener)[each]); + } + } + } + unsubscribe(listener?: Partial) { + if (listener) { + for (const each of getMethods(listener)) { + this.off(each, (listener)[each]); + } + } + } +} \ No newline at end of file diff --git a/ce/ce/util/exceptions.ts b/ce/ce/util/exceptions.ts new file mode 100644 index 0000000000..4920627b7b --- /dev/null +++ b/ce/ce/util/exceptions.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../i18n'; +import { Uri } from './uri'; + +export class Failed extends Error { + fatal = true; +} + +export class RemoteFileUnavailable extends Error { + constructor(public uri: Array) { + super(); + } +} + +export class TargetFileCollision extends Error { + constructor(public uri: Uri, message: string) { + super(message); + } +} + +export class MultipleInstallsMatched extends Error { + constructor(public queries: Array) { + super(i`Matched more than one install block [${queries.join(',')}]`); + } +} + diff --git a/ce/ce/util/exec-cmd.ts b/ce/ce/util/exec-cmd.ts new file mode 100644 index 0000000000..eee1332bce --- /dev/null +++ b/ce/ce/util/exec-cmd.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ChildProcess, ProcessEnvOptions, spawn, SpawnOptions } from 'child_process'; + +export interface ExecOptions extends SpawnOptions { + onCreate?(cp: ChildProcess): void; + onStdOutData?(chunk: any): void; + onStdErrData?(chunk: any): void; +} + +export interface ExecResult { + stdout: string; + stderr: string; + + /** + * Union of stdout and stderr. + */ + log: string; + error: Error | null; + code: number | null; +} + + +export function cmdlineToArray(text: string, result: Array = [], matcher = /[^\s"]+|"([^"]*)"/gi, count = 0): Array { + text = text.replace(/\\"/g, '\ufffe'); + const match = matcher.exec(text); + return match + ? cmdlineToArray( + text, + result, + matcher, + result.push(match[1] ? match[1].replace(/\ufffe/g, '\\"') : match[0].replace(/\ufffe/g, '\\"')), + ) + : result; +} + +export function execute(command: string, cmdlineargs: Array, options: ExecOptions = {}): Promise { + return new Promise((resolve, reject) => { + const cp = spawn(command, cmdlineargs.filter(each => each), { ...options, stdio: 'pipe' }); + if (options.onCreate) { + options.onCreate(cp); + } + + options.onStdOutData ? cp.stdout.on('data', options.onStdOutData) : cp; + options.onStdErrData ? cp.stderr.on('data', options.onStdErrData) : cp; + + let err = ''; + let out = ''; + let all = ''; + cp.stderr.on('data', (chunk) => { + err += chunk; + all += chunk; + }); + cp.stdout.on('data', (chunk) => { + out += chunk; + all += chunk; + }); + + cp.on('error', (err) => { + reject(err); + }); + + cp.on('close', (code, signal) => + resolve({ + stdout: out, + stderr: err, + log: all, + error: code ? new Error('Process Failed.') : null, + code, + }), + ); + }); +} + +/** + * Method that wraps spawn with for ease of calling. It is wrapped with a promise that must be awaited. + * This version calls spawn with a shell and allows for chaining of commands. In this version, commands and + * arguments must both be given in the command string. + * @param commands String of commands with parameters, possibly chained together with '&&' or '||'. + * @param options Options providing callbacks for various scenarios. + * @param environmentOptions Environment options for passing environment variables into the spawn. + * @returns + */ +export const execute_shell = ( + command: string, + options: ExecOptions = {}, + environmentOptions?: ProcessEnvOptions +): Promise => { + return new Promise((resolve, reject) => { + const cp = spawn(command, environmentOptions ? { ...options, ...environmentOptions, stdio: 'pipe', shell: true } : { ...options, stdio: 'pipe', shell: true }); + if (options.onCreate) { + options.onCreate(cp); + } + + options.onStdOutData ? cp.stdout.on('data', options.onStdOutData) : cp; + options.onStdErrData ? cp.stderr.on('data', options.onStdErrData) : cp; + + let err = ''; + let out = ''; + let all = ''; + cp.stderr.on('data', (chunk) => { + err += chunk; + all += chunk; + }); + cp.stdout.on('data', (chunk) => { + out += chunk; + all += chunk; + }); + + cp.on('error', (err) => { + reject(err); + }); + cp.on('close', (code, signal) => + resolve({ + stdout: out, + stderr: err, + log: all, + error: code ? new Error('Process Failed.') : null, + code, + }), + ); + }); +}; \ No newline at end of file diff --git a/ce/ce/util/hash.ts b/ce/ce/util/hash.ts new file mode 100644 index 0000000000..fa27b0774b --- /dev/null +++ b/ce/ce/util/hash.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { fail } from 'assert'; +import { createHash } from 'crypto'; +import { Readable } from 'stream'; +import { ProgressTrackingStream } from '../fs/streams'; +import { Uri } from './uri'; + +// sha256, sha512, sha384 +export type Algorithm = 'sha256' | 'sha384' | 'sha512' + +export async function hash(stream: Readable, uri: Uri, size: number, algorithm: 'sha256' | 'sha384' | 'sha512' = 'sha256', events: Partial) { + stream = await stream; + + try { + const p = new ProgressTrackingStream(0, size); + p.on('progress', (filePercentage) => events.verifying?.(uri.fsPath, filePercentage)); + + for await (const chunk of stream.pipe(p).pipe(createHash(algorithm)).setEncoding('hex')) { + // it should be done reading here + return chunk; + } + } finally { + stream.destroy(); + } + fail('Should have returned a chunk from the pipe'); +} + +export interface VerifyEvents { + verifying(file: string, percent: number): void; +} + +export interface Hash { + value?: string; + algorithm?: 'sha256' | 'sha384' | 'sha512' +} diff --git a/ce/ce/util/intersect.ts b/ce/ce/util/intersect.ts new file mode 100644 index 0000000000..4f842a59f4 --- /dev/null +++ b/ce/ce/util/intersect.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Creates an intersection object from two source objects. + * + * Typescript nicely supports defining intersection types (ie, Foo & Bar ) + * But if you have two seperate *instances*, and you want to use them as the implementation + * of that intersection, the language doesn't solve that for you. + * + * This function creates a strongly typed proxy type around the two objects, + * and returns members for the intersection of them. + * + * This works well for properties and member functions the same. + * + * Members in the primary object will take precedence over members in the secondary object if names conflict. + * + * This can also be used to "add" arbitrary members to an existing type (without mutating the original object) + * + * @example + * const combined = intersect( new Foo(), { test: () => { console.debug('testing'); } }); + * combined.test(); // writes out 'testing' to console + * + * @param primary primary object - members from this will have precedence. + * @param secondary secondary object - members from this will be used if primary does not have a member + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export function intersect(primary: T, secondary: T2, filters = ['constructor']): T & T2 { + // eslint-disable-next-line keyword-spacing + return new Proxy({ primary, secondary }, { + // member get proxy handler + get(target: { primary: T, secondary: T2 }, property: string | symbol, receiver: any) { + // check for properties on the objects first + const propertyName = property.toString(); + + // provide custom JON impl. + if (propertyName === 'toJSON') { + return () => { + const allKeys = this.ownKeys(); + const o = {}; + for (const i of allKeys) { + const v = this.get(target, i); + if (typeof v !== 'function') { + o[i] = v; + } + } + return o; + }; + } + + const pv = (target.primary)[property]; + const sv = (target.secondary)[property]; + + if (pv !== undefined) { + if (typeof pv === 'function') { + return pv.bind(primary); + } + return pv; + } + + if (sv !== undefined) { + if (typeof sv === 'function') { + return sv.bind(secondary); + } + return sv; + } + + return undefined; + }, + + // member set proxy handler + set(target: { primary: T, secondary: T2 }, property: string | symbol, value: any) { + const propertyName = property.toString(); + + if (Object.getOwnPropertyNames(target.primary).indexOf(propertyName) > -1) { + return (target.primary)[property] = value; + } + if (Object.getOwnPropertyNames(target.secondary).indexOf(propertyName) > -1) { + return (target.secondary)[property] = value; + } + return undefined; + }, + ownKeys(target: { primary: T, secondary: T2 }): ArrayLike { + return [...new Set([ + ...Object.getOwnPropertyNames(Object.getPrototypeOf(primary)), + ...Object.getOwnPropertyNames(primary), + ...Object.getOwnPropertyNames(Object.getPrototypeOf(secondary)), + ...Object.getOwnPropertyNames(secondary)].filter(each => filters.indexOf(each) === -1))]; + } + + }); +} diff --git a/ce/ce/util/linq.ts b/ce/ce/util/linq.ts new file mode 100644 index 0000000000..8c9ba01c2d --- /dev/null +++ b/ce/ce/util/linq.ts @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export interface Dictionary { + [key: string]: T; +} + +export class Dictionary implements Dictionary { +} + +export function ToDictionary(keys: Array, each: (index: string) => T) { + const result = new Dictionary(); + keys.map((v, i, a) => result[v] = each(v)); + return result; +} + +export type IndexOf = T extends Map ? T : T extends Array ? number : string; + +/** performs a truthy check on the value, and calls onTrue when the condition is true,and onFalse when it's not */ +export function when(value: T, onTrue: (value: NonNullable) => void, onFalse: () => void = () => { /* */ }) { + return value ? onTrue(>value) : onFalse(); +} + +export interface IterableWithLinq extends Iterable { + linq: IterableWithLinq; + any(predicate?: (each: T) => boolean): boolean; + all(predicate: (each: T) => boolean): boolean; + bifurcate(predicate: (each: T) => boolean): Array>; + concat(more: Iterable): IterableWithLinq; + distinct(selector?: (each: T) => any): IterableWithLinq; + duplicates(selector?: (each: T) => any): IterableWithLinq; + first(predicate?: (each: T) => boolean): T | undefined; + selectNonNullable(selector: (each: T) => V): IterableWithLinq>; + select(selector: (each: T) => V): IterableWithLinq; + selectMany(selector: (each: T) => Iterable): IterableWithLinq; + where(predicate: (each: T) => boolean): IterableWithLinq; + forEach(action: (each: T) => void): void; + aggregate(accumulator: (current: T | A, next: T) => A, seed?: T | A, resultAction?: (result?: T | A) => A | R): T | A | R | undefined; + toArray(): Array; + toObject(selector: (each: T) => [V, U]): Record; + results(): Promise; + toDictionary(keySelector: (each: T) => string, selector: (each: T) => TValue): Dictionary; + toMap(keySelector: (each: T) => TKey, selector: (each: T) => TValue): Map; + groupBy(keySelector: (each: T) => TKey, selector: (each: T) => TValue): Map>; + + /** + * Gets or sets the length of the iterable. This is a number one higher than the highest element defined in an array. + */ + count(): number; + + /** + * Adds all the elements of an array separated by the specified separator string. + * @param separator A string used to separate one element of an array from the next in the resulting String. If omitted, the array elements are separated with a comma. + */ + join(separator?: string): string; + +} + +/* eslint-disable */ + +function linqify(iterable: Iterable | IterableIterator): IterableWithLinq { + if ((iterable)['linq'] === iterable) { + return >iterable; + } + const r = { + [Symbol.iterator]: iterable[Symbol.iterator].bind(iterable), + all: all.bind(iterable), + any: any.bind(iterable), + bifurcate: bifurcate.bind(iterable), + concat: concat.bind(iterable), + distinct: distinct.bind(iterable), + duplicates: duplicates.bind(iterable), + first: first.bind(iterable), + select: select.bind(iterable), + selectMany: selectMany.bind(iterable), + selectNonNullable: selectNonNullable.bind(iterable), + toArray: toArray.bind(iterable), + toObject: toObject.bind(iterable), + where: where.bind(iterable), + forEach: forEach.bind(iterable), + aggregate: aggregate.bind(iterable), + join: join.bind(iterable), + count: len.bind(iterable), + results: results.bind(iterable), + toDictionary: toDictionary.bind(iterable), + toMap: toMap.bind(iterable), + groupBy: groupBy.bind(iterable), + }; + r.linq = r; + return r; +} + +function len(this: Iterable): number { + return length(this); +} + +export function keys(source: Map | null | undefined): Iterable +export function keys>(source: Dictionary | null | undefined): Iterable +export function keys>(source: Array | null | undefined): Iterable +export function keys(source: any | undefined | null): Iterable +export function keys(source: any): Iterable { + if (source) { + if (Array.isArray(source)) { + return >>(>source).keys(); + } + + if (source instanceof Map) { + return >(>source).keys(); + } + + if (source instanceof Set) { + throw new Error('Unable to iterate keys on a Set'); + } + + return >>Object.keys(source); + } + // undefined/null + return []; +} + + + +/** returns an IterableWithLinq<> for keys in the collection */ +function _keys(source: Map | null | undefined): IterableWithLinq +function _keys>(source: Dictionary | null | undefined): IterableWithLinq +function _keys>(source: Array | null | undefined): IterableWithLinq +function _keys(source: any | undefined | null): IterableWithLinq +function _keys(source: any): IterableWithLinq { + //export function keys | Dictionary | Map)>(source: TSrc & (Array | Dictionary | Map) | null | undefined): IterableWithLinq> { + if (source) { + if (Array.isArray(source)) { + return >>linqify((>source).keys()); + } + + if (source instanceof Map) { + return >linqify((>source).keys()); + } + + if (source instanceof Set) { + throw new Error('Unable to iterate keys on a Set'); + } + + return >>linqify((Object.keys(source))); + } + // undefined/null + return linqify([]); +} +function isIterable(source: any): source is Iterable { + return !!source && !!source[Symbol.iterator]; +} + +export function values | Dictionary | Map)>(source: (Iterable | Array | Dictionary | Map | Set) | null | undefined): Iterable { + if (source) { + // map + if (source instanceof Map || source instanceof Set) { + return source.values(); + } + + // any iterable source + if (isIterable(source)) { + return source; + } + + // dictionary (object keys) + return Object.values(source); + } + + // null/undefined + return []; +} +export const linq = { + values: _values, + entries: _entries, + keys: _keys, + find: _find, + startsWith: _startsWith, +}; + +/** returns an IterableWithLinq<> for values in the collection + * + * @note - null/undefined/empty values are considered 'empty' +*/ +function _values(source: (Array | Dictionary | Map | Set | Iterable) | null | undefined): IterableWithLinq { + return (source) ? linqify(values(source)) : linqify([]); +} + +export function entries | Dictionary | Map | undefined | null)>(source: TSrc & (Array | Dictionary | Map) | null | undefined): Iterable<[IndexOf, T]> { + if (source) { + if (Array.isArray(source)) { + return , T]>>source.entries(); + } + + if (source instanceof Map) { + return , T]>>source.entries(); + } + + if (source instanceof Set) { + throw new Error('Unable to iterate items on a Set (use values)'); + } + + return , T]>>Object.entries(source); + } + // undefined/null + return []; +} + +/** returns an IterableWithLinq<{key,value}> for the source */ +function _entries | Dictionary | Map | undefined | null)>(source: TSrc & (Array | Dictionary | Map) | null | undefined): IterableWithLinq<[IndexOf, T]> { + return linqify(source ? entries(source) : []) +} + +/** returns the first value where the key equals the match value (case-insensitive) */ +function _find | Dictionary | Map | undefined | null)>(source: TSrc & (Array | Dictionary | Map) | null | undefined, match: string): T | undefined { + return _entries(source).first(([key,]) => key.toString().localeCompare(match, undefined, { sensitivity: 'base' }) === 0)?.[1]; +} + +/** returns the first value where the key starts with the match value (case-insensitive) */ +function _startsWith | Dictionary | Map | undefined | null)>(source: TSrc & (Array | Dictionary | Map) | null | undefined, match: string): T | undefined { + match = match.toLowerCase(); + return _entries(source).first(([key,]) => key.toString().toLowerCase().startsWith(match))?.[1]; +} + + +export function length(source?: string | Iterable | Dictionary | Array | Map | Set): number { + if (source) { + if (Array.isArray(source) || typeof (source) === 'string') { + return source.length; + } + if (source instanceof Map || source instanceof Set) { + return source.size; + } + if (isIterable(source)) { + return [...source].length; + } + return source ? Object.values(source).length : 0; + } + return 0; +} + +function toDictionary(this: Iterable, keySelector: (each: TElement) => string, selector: (each: TElement) => TValue): Dictionary { + const result = new Dictionary(); + for (const each of this) { + result[keySelector(each)] = selector(each); + } + return result; +} + +function toMap(this: Iterable, keySelector: (each: TElement) => TKey, selector: (each: TElement) => TValue): Map { + const result = new Map(); + for (const each of this) { + result.set(keySelector(each), selector(each)); + } + return result; +} + +function groupBy(this: Iterable, keySelector: (each: TElement) => TKey, selector: (each: TElement) => TValue): Map { + const result = new ManyMap(); + for (const each of this) { + result.push(keySelector(each), selector(each)); + } + return result; +} + +function any(this: Iterable, predicate?: (each: T) => boolean): boolean { + for (const each of this) { + if (!predicate || predicate(each)) { + return true; + } + } + return false; +} + +function all(this: Iterable, predicate: (each: T) => boolean): boolean { + for (const each of this) { + if (!predicate(each)) { + return false; + } + } + return true; +} + +function concat(this: Iterable, more: Iterable): IterableWithLinq { + return linqify(function* (this: Iterable) { + for (const each of this) { + yield each; + } + for (const each of more) { + yield each; + } + }.bind(this)()); +} + +function select(this: Iterable, selector: (each: T) => V): IterableWithLinq { + return linqify(function* (this: Iterable) { + for (const each of this) { + yield selector(each); + } + }.bind(this)()); +} + +function selectMany(this: Iterable, selector: (each: T) => Iterable): IterableWithLinq { + return linqify(function* (this: Iterable) { + for (const each of this) { + yield* selector(each); + } + }.bind(this)()); +} + +function where(this: Iterable, predicate: (each: T) => boolean): IterableWithLinq { + return linqify(function* (this: Iterable) { + for (const each of this) { + if (predicate(each)) { + yield each; + } + } + }.bind(this)()); +} + +function forEach(this: Iterable, action: (each: T) => void) { + for (const each of this) { + action(each); + } +} + +function aggregate(this: Iterable, accumulator: (current: T | A, next: T) => A, seed?: T | A, resultAction?: (result?: T | A) => A | R): T | A | R | undefined { + let result: T | A | undefined = seed; + for (const each of this) { + if (result === undefined) { + result = each; + continue; + } + result = accumulator(result, each); + } + return resultAction !== undefined ? resultAction(result) : result; +} + +function selectNonNullable(this: Iterable, selector: (each: T) => V): IterableWithLinq> { + return linqify(function* (this: Iterable) { + for (const each of this) { + const value = selector(each); + if (value) { + yield >value; + } + } + }.bind(this)()); +} + +function nonNullable(this: Iterable): IterableWithLinq> { + return linqify(function* (this: Iterable) { + for (const each of this) { + if (each) { + yield >each; + } + } + }.bind(this)()); +} + +function first(this: Iterable, predicate?: (each: T) => boolean): T | undefined { + for (const each of this) { + if (!predicate || predicate(each)) { + return each; + } + } + return undefined; +} + +function toArray(this: Iterable): Array { + return [...this]; +} + +function toObject(this: Iterable, selector: (each: T) => [string, V]): Record { + const result = >{}; + for (const each of this) { + const [key, value] = selector(each); + result[key] = value; + } + return result; +} + +async function results(this: Iterable): Promise { + await Promise.all([...this]); +} + + +function join(this: Iterable, separator: string): string { + return [...this].join(separator); +} + +function bifurcate(this: Iterable, predicate: (each: T) => boolean): Array> { + const result = [new Array(), new Array()]; + for (const each of this) { + result[predicate(each) ? 0 : 1].push(each); + } + return result; +} + +function distinct(this: Iterable, selector?: (each: T) => any): IterableWithLinq { + const hash = new Dictionary(); + return linqify(function* (this: Iterable) { + + if (!selector) { + selector = i => i; + } + for (const each of this) { + const k = JSON.stringify(selector(each)); + if (!hash[k]) { + hash[k] = true; + yield each; + } + } + }.bind(this)()); +} + +function duplicates(this: Iterable, selector?: (each: T) => any): IterableWithLinq { + const hash = new Dictionary(); + return linqify(function* (this: Iterable) { + + if (!selector) { + selector = i => i; + } + for (const each of this) { + const k = JSON.stringify(selector(each)); + if (hash[k] === undefined) { + hash[k] = false; + } else { + if (hash[k] === false) { + hash[k] = true; + yield each; + } + } + } + }.bind(this)()); +} + +/** A Map of Key: Array */ +export class ManyMap extends Map> { + /** + * Push the value into the array at key + * @param key the unique key in the map + * @param value the value to push to the collection at 'key' + */ + push(key: K, value: V) { + this.getOrDefault(key, []).push(value); + } +} + +export function countWhere(from: Iterable, predicate: (each: T) => Promise): Promise +export function countWhere(from: Iterable, predicate: (each: T) => boolean): number +export function countWhere(from: Iterable, predicate: (e: T) => boolean | Promise) { + let v = 0; + const all = []; + for (const each of from) { + const test = predicate(each); + if (test.then) { + all.push(test.then((antecedent: any) => { + if (antecedent) { + v++; + } + })); + continue; + } + if (test) { + v++; + } + } + if (all.length) { + return Promise.all(all).then(() => v); + } + return v; +} \ No newline at end of file diff --git a/ce/ce/util/manual-promise.ts b/ce/ce/util/manual-promise.ts new file mode 100644 index 0000000000..c52b2daab0 --- /dev/null +++ b/ce/ce/util/manual-promise.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** +* A manually (or externally) controlled asynchronous Promise implementation +*/ +export class ManualPromise implements Promise { + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | null | undefined, onrejected?: ((reason: any) => TResult2 | PromiseLike) | null | undefined): Promise { + return this.p.then(onfulfilled, onrejected); + } + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | null | undefined): Promise { + return this.p.catch(onrejected); + } + finally(onfinally?: (() => void) | null | undefined): Promise { + return this.p.finally(onfinally); + } + + readonly [Symbol.toStringTag]: 'Promise'; + private p: Promise; + + /** + * A method to manually resolve the Promise. + */ + public resolve: (value?: T | PromiseLike | undefined) => void = (v) => { /* */ }; + + /** + * A method to manually reject the Promise + */ + public reject: (e: any) => void = (e) => { /* */ }; + + private state: 'pending' | 'resolved' | 'rejected' = 'pending'; + + /** + * Returns true of the Promise has been Resolved or Rejected + */ + public get isCompleted(): boolean { + return this.state !== 'pending'; + } + + /** + * Returns true if the Promise has been Resolved. + */ + public get isResolved(): boolean { + return this.state === 'resolved'; + } + + /** + * Returns true if the Promise has been Rejected. + */ + public get isRejected(): boolean { + return this.state === 'rejected'; + } + + public constructor() { + this.p = new Promise((r, j) => { + this.resolve = (v: T | PromiseLike | undefined) => { this.state = 'resolved'; r(v); }; + this.reject = (e: any) => { this.state = 'rejected'; j(e); }; + }); + } +} + +export class LazyPromise extends ManualPromise { + public constructor(private action: () => Promise) { + super(); + } + + execute() { + this.action().then(v => this.resolve(v), e => this.reject(e)); + return this; + } +} diff --git a/ce/ce/util/percentage-scaler.ts b/ce/ce/util/percentage-scaler.ts new file mode 100644 index 0000000000..40c384487a --- /dev/null +++ b/ce/ce/util/percentage-scaler.ts @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; + +export class PercentageScaler { + private readonly scaledDomainMax : number; + private readonly scaledPercentMax : number; + + private static clamp(test: number, min: number, max:number) : number { + if (test < min) { return min; } + if (test > max) { return max; } + return test; + } + + constructor(public readonly lowestDomain: number, public readonly highestDomain: number, + public readonly lowestPercentage = 0, public readonly highestPercentage = 100) { + strict.ok(lowestDomain <= highestDomain); + strict.ok(lowestPercentage <= highestPercentage); + this.scaledDomainMax = highestDomain - lowestDomain; + this.scaledPercentMax = highestPercentage - lowestPercentage; + } + + scalePosition(domain: number) : number { + if (this.scaledDomainMax === 0 || this.scaledPercentMax === 0) { + return this.highestPercentage; + } + const domainClamped = PercentageScaler.clamp(domain, this.lowestDomain, this.highestDomain); + const domainScaled = domainClamped - this.lowestDomain; + const domainProportion = domainScaled / this.scaledDomainMax; + const partialPercent = this.scaledPercentMax * domainProportion; + const percentage = this.lowestPercentage + partialPercent; + return Math.round(percentage * 10) / 10; + } +} diff --git a/ce/ce/util/promise.ts b/ce/ce/util/promise.ts new file mode 100644 index 0000000000..339e21b96c --- /dev/null +++ b/ce/ce/util/promise.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { LazyPromise, ManualPromise } from './manual-promise'; + +/** a precrafted failed Promise */ +const waiting = Promise.reject(0xDEFACED); +waiting.catch(() => { /** */ }); + +/** + * Does a Promise.any(), and accept the one that first matches the predicate, or if all resolve, and none match, the first. + * + * @remarks WARNING - this requires Node 15+ or node 14.12+ with --harmony + * @param from + * @param predicate + */ +export async function anyWhere(from: Iterable>, predicate: (value: T) => boolean) { + let unfulfilled = new Array>(); + const failed = new Array>(); + const completed = new Array(); + + // wait for something to succeed. if nothing suceeds, then this will throw. + const first = await Promise.any(from); + let success: T | undefined; + + // eslint-disable-next-line no-constant-condition + while (true) { + + // + for (const each of from) { + // if we had a winner, return now. + await Promise.any([each, waiting]).then(antecedent => { + if (predicate(antecedent)) { + success = antecedent; + return antecedent; + } + completed.push(antecedent); + return undefined; + }).catch(r => { + if (r === 0xDEFACED) { + // it's not done yet. + unfulfilled.push(each); + } else { + // oh, it returned and it was a failure. + failed.push(each); + } + return undefined; + }); + } + // we found one that passes muster! + if (success) { + return success; + } + + if (unfulfilled.length) { + // something completed successfully, but nothing passed the predicate yet. + // so hope remains eternal, lets rerun whats left with the unfulfilled. + from = unfulfilled; + unfulfilled = []; + continue; + } + + // they all finished + // but nothing hit the happy path. + break; + } + + // if we get here, then we're + // everything completed, but nothing passed the predicate + // give them the first to suceed + return first; +} + + +export class Queue { + private total = 0; + private active = 0; + private queue = new Array>(); + private whenZero: ManualPromise | undefined; + private rejections = new Array(); + + constructor(private maxConcurency = 8) { + } + + get count() { + return this.total; + } + + get done() { + return this.zero(); + } + + /** Will block until the queue hits the zero mark */ + private async zero(): Promise { + if (this.active) { + this.whenZero = this.whenZero || new ManualPromise(); + await this.whenZero; + } + if (this.rejections.length > 0) { + throw new AggregateError(this.rejections); + } + this.whenZero = undefined; + return this.total; + } + + private next() { + (--this.active) || this.whenZero?.resolve(0); + if (this.queue.length) { + this.queue.pop()?.execute().catch(async (e) => { this.rejections.push(e); throw e; }).finally(() => this.next()); + } + } + + /** + * Queues up actions for throttling the number of concurrent async tasks running at a given time. + * + * If the process has reached max concurrency, the action is deferred until the last item + * The last item + * @param action + */ + async enqueue(action: () => Promise): Promise { + strict.ok(!this.whenZero, 'items may not be added to the queue while it is being awaited'); + + this.active++; + this.total++; + + if (this.queue.length || this.active >= this.maxConcurency) { + const result = new LazyPromise(action); + this.queue.push(result); + return result; + } + + return action().catch(async (e) => { this.rejections.push(e); throw e; }).finally(() => this.next()); + } + + enqueueMany(array: Array, fn: (v: S) => Promise) { + for (const each of array) { + void this.enqueue(() => fn(each)); + } + return this; + } + +} + diff --git a/ce/ce/util/safeEval.ts b/ce/ce/util/safeEval.ts new file mode 100644 index 0000000000..13d0e1428f --- /dev/null +++ b/ce/ce/util/safeEval.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as vm from 'vm'; + +/** + * Creates a reusable safe-eval sandbox to execute code in. + */ +export function createSandbox(): (code: string, context?: any) => T { + const sandbox = vm.createContext({}); + return (code: string, context?: any) => { + const response = 'SAFE_EVAL_' + Math.floor(Math.random() * 1000000); + sandbox[response] = {}; + if (context) { + for (const key of Object.keys(context)) { + sandbox[key] = context[key]; + } + vm.runInContext(`try { ${response} = ${code} } catch (e) { ${response} = undefined }`, sandbox); + for (const key of Object.keys(context)) { + delete sandbox[key]; + } + } else { + vm.runInContext(`${response} = ${code}`, sandbox); + } + return sandbox[response]; + }; +} diff --git a/ce/ce/util/text.ts b/ce/ce/util/text.ts new file mode 100644 index 0000000000..3b3b01849b --- /dev/null +++ b/ce/ce/util/text.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TextDecoder } from 'util'; + +const decoder = new TextDecoder('utf-8'); + +export function decode(input?: NodeJS.ArrayBufferView | ArrayBuffer | null | undefined) { + return decoder.decode(input); +} +export function encode(content: string): Uint8Array { + return Buffer.from(content, 'utf-8'); +} + +export function equalsIgnoreCase(s1: string | undefined, s2: string | undefined): boolean { + return s1 === s2 || !!s1 && !!s2 && s1.localeCompare(s2, undefined, { sensitivity: 'base' }) === 0; +} diff --git a/ce/ce/util/uri.ts b/ce/ce/util/uri.ts new file mode 100644 index 0000000000..86ad2a393e --- /dev/null +++ b/ce/ce/util/uri.ts @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strict } from 'assert'; +import { dirname, join, relative } from 'path'; +import { Readable, Writable } from 'stream'; +import { URL } from 'url'; +import { URI } from 'vscode-uri'; +import { UriComponents } from 'vscode-uri/lib/umd/uri'; +import { FileStat, FileSystem, FileType, ReadHandle, WriteStreamOptions } from '../fs/filesystem'; +import { AcquireEvents } from '../interfaces/events'; +import { Algorithm, Hash, hash } from './hash'; +import { decode, encode } from './text'; + +/** + * This class is intended to be a drop-in replacement for the vscode uri + * class, but has a filesystem associated with it. + * + * By associating the filesystem with the URI, we can allow for file URIs + * to be scoped to a given filesystem (ie, a zip could be a filesystem ) + * + * Uniform Resource Identifier (URI) https://tools.ietf.org/html/rfc3986. + * This class is a simple parser which creates the basic component parts + * (https://tools.ietf.org/html/rfc3986#section-3) with minimal validation + * and encoding. + * + * + * ```txt + * foo://example.com:8042/over/there?name=ferret#nose + * \_/ \______________/\_________/ \_________/ \__/ + * | | | | | + * scheme authority path query fragment + * | _____________________|__ + * / \ / \ + * urn:example:animal:ferret:nose + * ``` + * + */ +export class Uri implements URI { + protected constructor(public readonly fileSystem: FileSystem, protected readonly uri: URI) { + + } + + static readonly invalid = new Uri(undefined, URI.parse('invalid:')); + + static isInvalid(uri?: Uri) { + return uri === undefined || Uri.invalid === uri; + } + /** + * scheme is the 'https' part of 'https://www.msft.com/some/path?query#fragment'. + * The part before the first colon. + */ + get scheme() { return this.uri.scheme; } + + /** + * authority is the 'www.msft.com' part of 'https://www.msft.com/some/path?query#fragment'. + * The part between the first double slashes and the next slash. + */ + get authority() { return this.uri.authority; } + + /** + * path is the '/some/path' part of 'https://www.msft.com/some/path?query#fragment'. + */ + get path() { return this.uri.path; } + + /** + * query is the 'query' part of 'https://www.msft.com/some/path?query#fragment'. + */ + get query() { return this.uri.query; } + + /** + * fragment is the 'fragment' part of 'https://www.msft.com/some/path?query#fragment'. + */ + get fragment() { return this.uri.fragment; } + + /** + * Creates a new Uri from a string, e.g. `https://www.msft.com/some/path`, + * `file:///usr/home`, or `scheme:with/path`. + * + * @param value A string which represents an URI (see `URI#toString`). + */ + static parse(fileSystem: FileSystem, value: string, _strict?: boolean): Uri { + return new Uri(fileSystem, URI.parse(value, _strict)); + } + + /** + * Creates a new Uri from a string, and replaces 'vsix' schemes with file:// instead. + * + * @param value A string which represents a URI which may be a VSIX uri. + */ + static parseFilterVsix(fileSystem: FileSystem, value: string, _strict?: boolean, vsixBaseUri?: Uri): Uri { + const parsed = URI.parse(value, _strict); + if (vsixBaseUri && parsed.scheme === 'vsix') { + return vsixBaseUri.join(parsed.path); + } + + return new Uri(fileSystem, parsed); + } + + /** + * Creates a new URI from a file system path, e.g. `c:\my\files`, + * `/usr/home`, or `\\server\share\some\path`. + * + * The *difference* between `URI#parse` and `URI#file` is that the latter treats the argument + * as path, not as stringified-uri. E.g. `URI.file(path)` is **not the same as** + * `URI.parse('file://' + path)` because the path might contain characters that are + * interpreted (# and ?). See the following sample: + * ```ts +const good = URI.file('/coding/c#/project1'); +good.scheme === 'file'; +good.path === '/coding/c#/project1'; +good.fragment === ''; +const bad = URI.parse('file://' + '/coding/c#/project1'); +bad.scheme === 'file'; +bad.path === '/coding/c'; // path is now broken +bad.fragment === '/project1'; +``` + * + * @param path A file system path (see `URI#fsPath`) + */ + static file(fileSystem: FileSystem, path: string): Uri { + return new Uri(fileSystem, URI.file(path)); + } + + /** construct an Uri from the various parts */ + static from(fileSystem: FileSystem, components: { + scheme: string; + authority?: string; + path?: string; + query?: string; + fragment?: string; + }): Uri { + return new Uri(fileSystem, URI.from(components)); + } + + /** + * Join all arguments together and normalize the resulting Uri. + * + * Also ensures that slashes are all forward. + * */ + join(...paths: Array) { + return new Uri(this.fileSystem, this.with({ path: join(this.path, ...paths).replace(/\\/g, '/') })); + } + + relative(target: Uri): string { + strict.ok(target.authority === this.authority, `Uris '${target.toString()}' and '${this.toString()}' are not of the same base`); + return relative(this.path, target.path).replace(/\\/g, '/'); + } + + /** returns true if the uri represents a file:// resource. */ + get isLocal(): boolean { + return this.scheme === 'file' || this.scheme === 'vsix'; + } + + get isHttps(): boolean { + return this.scheme === 'https'; + } + /** + * Returns a string representing the corresponding file system path of this URI. + * Will handle UNC paths, normalizes windows drive letters to lower-case, and uses the + * platform specific path separator. + * + * * Will *not* validate the path for invalid characters and semantics. + * * Will *not* look at the scheme of this URI. + * * The result shall *not* be used for display purposes but for accessing a file on disk. + * + * + * The *difference* to `URI#path` is the use of the platform specific separator and the handling + * of UNC paths. See the below sample of a file-uri with an authority (UNC path). + * + * ```ts + const u = URI.parse('file://server/c$/folder/file.txt') + u.authority === 'server' + u.path === '/shares/c$/file.txt' + u.fsPath === '\\server\c$\folder\file.txt' + ``` + * + * Using `URI#path` to read a file (using fs-apis) would not be enough because parts of the path, + * namely the server name, would be missing. Therefore `URI#fsPath` exists - it's sugar to ease working + * with URIs that represent files on disk (`file` scheme). + */ + get fsPath(): string { + return this.uri.fsPath; + } + + /** Duplicates the current Uri, changing out any parts */ + with(change: { scheme?: string | undefined; authority?: string | null | undefined; path?: string | null | undefined; query?: string | null | undefined; fragment?: string | null | undefined; }): URI { + return new Uri(this.fileSystem, this.uri.with(change)); + } + + /** + * Creates a string representation for this URI. It's guaranteed that calling + * `URI.parse` with the result of this function creates an URI which is equal + * to this URI. + * + * * The result shall *not* be used for display purposes but for externalization or transport. + * * The result will be encoded using the percentage encoding and encoding happens mostly + * ignore the scheme-specific encoding rules. + * + * @param skipEncoding Do not encode the result, default is `false` + */ + toString(skipEncoding?: boolean): string { + return this.uri.toString(skipEncoding); + } + + get formatted(): string { + return this.scheme === 'file' ? this.uri.fsPath : this.uri.toString(); + } + + /** returns a JSON object with the components of the Uri */ + toJSON(): UriComponents { + return this.uri.toJSON(); + } + + toUrl(): URL { + return new URL(this.uri.toString()); + } + + /* Act on this uri */ + protected resolve(uriOrRelativePath?: Uri | string) { + return typeof uriOrRelativePath === 'string' ? this.join(uriOrRelativePath) : uriOrRelativePath ?? this; + } + + stat(uri?: Uri | string): Promise { + uri = this.resolve(uri); + return uri.fileSystem.stat(uri); + } + + readDirectory(uri?: Uri | string, options?: { recursive?: boolean }): Promise> { + uri = this.resolve(uri); + return uri.fileSystem.readDirectory(uri, options); + } + + async createDirectory(uri?: Uri | string): Promise { + uri = this.resolve(uri); + await uri.fileSystem.createDirectory(uri); + return uri; + } + + readFile(uri?: Uri | string): Promise { + uri = this.resolve(uri); + return uri.fileSystem.readFile(uri); + } + + async readUTF8(uri?: Uri | string): Promise { + return decode(await this.readFile(uri)); + } + + openFile(uri?: Uri | string): Promise { + uri = this.resolve(uri); + return uri.fileSystem.openFile(uri); + } + + readStream(start = 0, end = Infinity): Promise { + return this.fileSystem.readStream(this, { start, end }); + } + + async readBlock(start = 0, end = Infinity): Promise { + const stream = await this.fileSystem.readStream(this, { start, end }); + + let block = Buffer.alloc(0); + for await (const chunk of stream) { + block = Buffer.concat([block, chunk]); + } + return block; + } + + async writeFile(content: Uint8Array): Promise { + await this.fileSystem.writeFile(this, content); + return this; + } + + writeUTF8(content: string): Promise { + return this.writeFile(encode(content)); + } + + writeStream(options?: WriteStreamOptions): Promise { + return this.fileSystem.writeStream(this, options); + } + + delete(options?: { recursive?: boolean, useTrash?: boolean }): Promise { + return this.fileSystem.delete(this, options); + } + + exists(uri?: Uri | string): Promise { + uri = this.resolve(uri); + return uri.fileSystem.exists(uri); + } + + isFile(uri?: Uri | string): Promise { + uri = this.resolve(uri); + return uri.fileSystem.isFile(uri); + } + + isSymlink(uri?: Uri | string): Promise { + uri = this.resolve(uri); + return uri.fileSystem.isSymlink(uri); + } + + isDirectory(uri?: Uri | string): Promise { + uri = this.resolve(uri); + return uri.fileSystem.isDirectory(uri); + } + + async size(uri?: Uri | string): Promise { + return (await this.stat(uri)).size; + } + + async hash(algorithm?: Algorithm): Promise { + if (algorithm) { + + return await hash(await this.fileSystem.readStream(this), this, await this.size(), algorithm, {}); + } + return undefined; + } + + async hashValid(events: Partial, matchOptions?: Hash) { + if (matchOptions?.algorithm && await this.exists()) { + return matchOptions.value?.toLowerCase() === await hash(await this.readStream(), this, await this.size(), matchOptions.algorithm, events); + } + return false; + } + + get parent(): Uri { + return new Uri(this.fileSystem, this.with({ + path: dirname(this.path) + })); + } +} + +export function isFilePath(uriOrPath?: Uri | string): boolean { + if (uriOrPath) { + if (uriOrPath instanceof Uri) { + return uriOrPath.scheme === 'file'; + } + if (uriOrPath.startsWith('file:')) { + return true; + } + return !!(/^[/\\.]|^[a-zA-Z]:/g.exec((uriOrPath || '').toString())); + } + return false; +} + diff --git a/ce/ce/util/xml.ts b/ce/ce/util/xml.ts new file mode 100644 index 0000000000..4c7e7f307b --- /dev/null +++ b/ce/ce/util/xml.ts @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { XMLBuilder, XmlBuilderOptionsOptional } from 'fast-xml-parser'; + +const defaultOptions = { + attributeNamePrefix: '$', + textNodeName: '#text', + ignoreAttributes: false, + ignoreNameSpace: false, + allowBooleanAttributes: false, + parseNodeValue: false, + parseAttributeValue: true, + trimValues: true, + cdataTagName: '__cdata', + cdataPositionChar: '\\c', + parseTrueNumberOnly: false, + arrayMode: false, + format: true, +}; + +export function toXml(content: Record, options: XmlBuilderOptionsOptional = {}) { + return `\n${new XMLBuilder({ ...defaultOptions, ...options }).build(content)}`.trim(); +} + +export function toXmlFragment(content: Record, options: XmlBuilderOptionsOptional = {}) { + return new XMLBuilder({ ...defaultOptions, ...options }).build(content); +} + diff --git a/ce/ce/version.ts b/ce/ce/version.ts new file mode 100644 index 0000000000..6ca9706bdb --- /dev/null +++ b/ce/ce/version.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** export a constant with the version of this library. */ +// eslint-disable-next-line @typescript-eslint/no-var-requires +export const Version: string = require('../package.json').version; diff --git a/ce/ce/willow/template-amf.ts b/ce/ce/willow/template-amf.ts new file mode 100644 index 0000000000..e39c88100f --- /dev/null +++ b/ce/ce/willow/template-amf.ts @@ -0,0 +1,288 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert } from 'console'; +import { isMap, isPair, isScalar, isSeq, LineCounter, Pair, parseDocument, YAMLMap, YAMLSeq } from 'yaml'; +import { i } from '../i18n'; +import { Session } from '../session'; +import { FlatVsManPayload, VsManDatabase } from './willow'; + +function lookupVsixVersion(session: Session, vsManLookup: VsManDatabase, id: string): string | undefined { + const adoptedVersionSource = vsManLookup.get(id); + if (!adoptedVersionSource) { + session.channels.error(i`template required id (${id}) not present in the Visual Studio manifest.`); + return undefined; + } + + assert(adoptedVersionSource.length >= 1); + const adoptedVersion = adoptedVersionSource[0].version; + if (adoptedVersionSource.length != 1) { + session.channels.warning(i`template required id version (${id}) which names an ID with more than one package in the Visual Studio manifest; choosing first version '${adoptedVersion}'.`); + } + + return adoptedVersion; +} + +function getYamlMapEntryKey(target: any): string | undefined { + if (isScalar(target)) { + if (typeof target.value === 'string') { + return target.value; + } + } else if (typeof target === 'string') { + return target; + } + + return undefined; +} + +// Replaces adopt-vsix-version-from-id in in the "info" block of the AMF denoted by inputRootMap, +// if any, with a real version derived from vsManLookup. +// Returns whether unrecoverable errors occurred. +function replaceAdoptVsixVersionFromId(session: Session, inputPath: string, + inputRootMap: YAMLMap, vsManLookup: VsManDatabase): boolean { + const inputInfoMap = inputRootMap.get('info'); + if (!isMap(inputInfoMap)) { + session.channels.error(i`${inputPath} is missing an 'info' map.`); + return true; + } + + const inputInfoMapCount = inputInfoMap.items.length; + for (let idx = 0; idx < inputInfoMapCount; ++idx) { + const inputInfoEntry = inputInfoMap.items[idx]; + if (!isPair(inputInfoEntry)) { + continue; + } + + if (getYamlMapEntryKey(inputInfoEntry.key) !== 'adopt-vsix-version-from-id') { + continue; + } + + if (!isScalar(inputInfoEntry.value) || typeof inputInfoEntry.value.value !== 'string') { + session.channels.error(i`${inputPath} use of adopt-vsix-version-from-id was not an id string`); + return true; + } + + const adoptedVersion = lookupVsixVersion(session, vsManLookup, inputInfoEntry.value.value); + if (typeof adoptedVersion !== 'string') { + return true; + } + + inputInfoMap.items[idx] = new Pair('version', adoptedVersion); + } + + return false; +} + +function transformVsixMapToUnzipMaps(session: Session, inputPath: string, + maybeVsixMap: YAMLMap, vsManLookup: VsManDatabase): Array | undefined { + const vsixId = maybeVsixMap.get('vsix'); + if (typeof vsixId !== 'string') { + return [maybeVsixMap]; // make no changes + } + + const strip = maybeVsixMap.get('strip'); + const vsixSource = vsManLookup.get(vsixId); + if (!vsixSource || vsixSource.length === 0 || !vsixSource[0]) { + session.channels.error(i`${inputPath} use of install vsix, named ID ${vsixId} was not present in the Visual Studio manifest.`); + return undefined; + } + + if (vsixSource.length === 1) { + const asUnzip = new YAMLMap(); + const soleSource = vsixSource[0]; + asUnzip.add(new Pair('unzip', [soleSource.localPath, soleSource.url])); + asUnzip.add(new Pair('sha256', soleSource.sha256)); + if (strip) { + asUnzip.add(new Pair('strip', strip)); + } + + return [asUnzip]; + } + + const langMap = new Map(); + for (const langVsix of vsixSource) { + const language = langVsix.language; + if (!language) { + session.channels.error(i`${inputPath} use of install vsix, named ID ${vsixId} had multiple packages in the Visual Studio manifest, but not all packages had language set.`); + return undefined; + } + + if (langMap.has(language)) { + session.channels.error(i`${inputPath} use of install vsix, named ID ${vsixId} had multiple matches for the same language.`); + return undefined; + } + + langMap.set(language, langVsix); + } + + const result = new Array(); + for (const entry of langMap) { + const asUnzip = new YAMLMap(); + asUnzip.add(new Pair('unzip', [entry[1].localPath, entry[1].url])); + asUnzip.add(new Pair('sha256', entry[1].sha256)); + asUnzip.add(new Pair('lang', entry[0])); + if (strip) { + asUnzip.add(new Pair('strip', strip)); + } + + result.push(asUnzip); + } + + return result; +} + +function templateAmfProcessInstallCandidate(session: Session, inputPath: string, + vsManLookup: VsManDatabase, installParent: YAMLMap): boolean { + const replacement = new Array(); + const installNode = installParent.get('install'); + if (isMap(installNode)) { + const candidates = transformVsixMapToUnzipMaps(session, inputPath, installNode, vsManLookup); + if (!candidates) { + return false; + } + + for (const candidate of candidates) { + replacement.push(candidate); + } + } else if (isSeq(installNode)) { + for (const item of installNode.items) { + if (!isMap(item)) { + return true; + } + + const candidates = transformVsixMapToUnzipMaps(session, inputPath, item, vsManLookup); + if (!candidates) { + return false; + } + + for (const candidate of candidates) { + replacement.push(candidate); + } + } + } else { + return false; + } + + if (replacement.length === 1) { + installParent.set('install', replacement[0]); + } else { + const inserted = new YAMLSeq(); + for (const entry of replacement) { + inserted.add(entry); + } + + installParent.set('install', inserted); + } + + return false; +} + +function templateAmfApplyVsixRequireVersion(session: Session, inputPath: string, vsManLookup: VsManDatabase, + vsixVersionRequireParent: YAMLMap): boolean { + const parentItems = vsixVersionRequireParent.items; + for (let idx = 0; idx < parentItems.length; ++idx) { + const thisItem = parentItems[idx]; + const key = getYamlMapEntryKey(thisItem.key); + if (key !== 'vsixVersionRequire') { + continue; + } + + const vsixVersionRequire = thisItem.value; + if (!isMap(vsixVersionRequire)) { + session.channels.error(i`vsixVersionRequire must be a map in ${inputPath}.`); + return true; + } + + const replacements = new YAMLMap(); + for (const vsixVersionRequireEntry of vsixVersionRequire.items) { + const key = vsixVersionRequireEntry.key; + const value = vsixVersionRequireEntry.value; + if (!isScalar(key) || !isScalar(value) + || typeof key.value !== 'string' || typeof value.value !== 'string') { + session.channels.error(i`vsixVersionRequire entry did not have the expected form in ${inputPath}.`); + return true; + } + + const targetName = key.value; + const adoptedVersion = lookupVsixVersion(session, vsManLookup, value.value); + if (typeof adoptedVersion !== 'string') { + return true; + } + + if (replacements.has(targetName)) { + session.channels.warning(i`vsixVersionRequire contained duplicate key ${targetName}; choosing the second.`); + } + + replacements.set(targetName, adoptedVersion); + } + + parentItems[idx] = new Pair('requires', replacements); + } + + return false; +} + +export function templateAmfApplyVsManifestInformation( + session: Session, inputPath: string, inputContent: string, vsManLookup: VsManDatabase): string | undefined { + const genericErrorMessage = i`Failed to interpret ${inputPath} as an AMF template.`; + const lc = new LineCounter(); + const inputDom = parseDocument(inputContent, { prettyErrors: false, lineCounter: lc, strict: true }); + if (inputDom.errors.length !== 0) { + session.channels.error(i`Failed to parse ${inputPath} as a YAML document: ${JSON.stringify(inputDom.errors)}`); + return undefined; + } + + if (inputDom.warnings.length !== 0) { + session.channels.warning(i`YAML warnings when parsing ${inputPath}: ${JSON.stringify(inputDom.warnings)}`); + } + + if (!isMap(inputDom.contents)) { + session.channels.error(genericErrorMessage); + return undefined; + } + + const inputRootMap = inputDom.contents; + + // replace any adopt-vsix-version-from-id nodes with the real versions + if (replaceAdoptVsixVersionFromId(session, inputPath, inputRootMap, vsManLookup)) { + return undefined; + } + + // replace any installs with "vsix" sources inside with unzip sources + + // replace any vsixVersionRequire s with require s + if (templateAmfProcessInstallCandidate(session, inputPath, vsManLookup, inputRootMap)) { + return undefined; + } + + if (templateAmfApplyVsixRequireVersion(session, inputPath, vsManLookup, inputRootMap)) { + return undefined; + } + + for (const demand of inputRootMap.items) { + if (!isPair(demand)) { + session.channels.error(genericErrorMessage); + return undefined; + } + + const demandKey = getYamlMapEntryKey(demand.key); + if (typeof demandKey !== 'string') { + session.channels.error(genericErrorMessage); + return undefined; + } + + const demandContents = demand.value; + if (demandKey === 'info' || demandKey == 'contacts' || !isMap(demandContents)) { + continue; + } + + if (templateAmfProcessInstallCandidate(session, inputPath, vsManLookup, demandContents)) { + return undefined; + } + + if (templateAmfApplyVsixRequireVersion(session, inputPath, vsManLookup, demandContents)) { + return undefined; + } + } + + return inputDom.toString(); +} diff --git a/ce/ce/willow/willow.ts b/ce/ce/willow/willow.ts new file mode 100644 index 0000000000..c3518cbbb6 --- /dev/null +++ b/ce/ce/willow/willow.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '../i18n'; + +export interface VsPayload { + readonly fileName: string; + readonly sha256: string; + readonly size: number; + readonly url: string; +} + +export interface ChManDef { + readonly payloads: Array; + readonly type: string; + readonly version: string; +} + +export interface FlatChManPayload extends VsPayload { + readonly version: string; +} + +export interface FlatVsManPayload extends VsPayload { + readonly id: string; + readonly version: string; + readonly language: string | undefined; + readonly installSize: number; + readonly localPath: string; +} + +function throwUnexpectedManifest(): never { + throw new Error(i`Unexpected format for Visual Studio manifest in channel.`); +} + +function isPayload(candidatePayload: any) { + return typeof candidatePayload.fileName === 'string' + && typeof candidatePayload.sha256 === 'string' + && candidatePayload.sha256.length === 64 + && typeof candidatePayload.size === 'number' + && typeof candidatePayload.url === 'string'; +} + +function isMinimalPackage(candidatePackage: any) { + return typeof candidatePackage.id === 'string' + && typeof candidatePackage.version === 'string' + && candidatePackage.type === 'Vsix' + && Array.isArray(candidatePackage.payloads) + && candidatePackage.payloads.length === 1 + && isPayload(candidatePackage.payloads[0]); +} + +function packageHasInstallSize(candidatePackage: any) { + return typeof (candidatePackage.installSizes) === 'object' + && typeof (candidatePackage.installSizes.targetDrive) === 'number'; +} + +function packageGetPointerLikeSingleDependencyId(candidatePackage: any): string | undefined { + if (packageHasInstallSize(candidatePackage) + || typeof candidatePackage.dependencies !== 'object') { + return undefined; + } + + let theDependency = undefined; + for (const key in candidatePackage.dependencies) { + if (theDependency) { + return undefined; + } + + if (key !== '') { + theDependency = key; + } + } + + return theDependency; +} + +export function parseVsManFromChannel(channelManifestContent: string): FlatChManPayload { + const chMan = JSON.parse(channelManifestContent); + const channelItemsRaw = chMan?.channelItems; + if (chMan.manifestVersion !== '1.1' || !Array.isArray(channelItemsRaw)) { + throw new Error(i`Unexpected Visual Studio channel manifest version.`); + } + + let vsManItem: ChManDef | undefined = undefined; + for (const channelItem of channelItemsRaw) { + if (channelItem?.id === 'Microsoft.VisualStudio.Manifests.VisualStudio') { + if (vsManItem === undefined) { + vsManItem = channelItem; + } else { + throwUnexpectedManifest(); + } + } + } + + if (!vsManItem) { + throwUnexpectedManifest(); + } + + if (!Array.isArray(vsManItem.payloads) + || vsManItem.type !== 'Manifest' + || typeof vsManItem.version !== 'string' + || vsManItem.payloads.length !== 1 + || !isPayload(vsManItem.payloads[0])) { + throwUnexpectedManifest(); + } + + return { ...vsManItem.payloads[0], version: vsManItem.version }; +} + +function flattenVsManPackage(rawJson: any): FlatVsManPayload | undefined { + if (!packageHasInstallSize(rawJson)) { + return undefined; + } + + let language: string | undefined; + if (typeof (rawJson.language) === 'string') { + language = rawJson.language; + } + + const thePayload = rawJson.payloads[0]; + const theId = rawJson.id; + const theVersion = rawJson.version; + let chip: string | undefined; + if (typeof (rawJson.chip) === 'string') { + chip = rawJson.chip; + } + + let localPath = `vsix:///${theId},version=${theVersion}`; + if (chip) { + localPath += `,chip=${chip}`; + } + + if (language) { + localPath += `,language=${language}`; + } + + localPath += '/payload.vsix'; + + return { + fileName: thePayload.fileName, + sha256: thePayload.sha256, + size: thePayload.size, + url: thePayload.url, + id: theId, + version: theVersion, + language: language, + installSize: rawJson.installSizes.targetDrive, + localPath: localPath + }; +} + +function maybeHydratePointerLikePackages(lookup: Map>, originalId: string) { + const originalArray = lookup.get(originalId); + if (!originalArray || originalArray.length === 0) { + return; + } + + const targetId = packageGetPointerLikeSingleDependencyId(originalArray[0]); + if (targetId && targetId !== originalId) { + maybeHydratePointerLikePackages(lookup, targetId); + const targetArray = lookup.get(targetId); + if (targetArray && targetArray.length == originalArray.length) { + lookup.set(originalId, targetArray); + } + } +} + +function escapeRegex(str: string) { + return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +class NumbersEntry { + public readonly numbers: Array; // for example, [10, 29] + constructor(public readonly rawStr: string) // for example, 10.0029 + { + this.numbers = Array.from(rawStr.split('.').select(Number.parseInt)); + } +} + +function cmpNumbersEntry(a: NumbersEntry, b: NumbersEntry) { + for (let idx = 0; ; ++idx) { + if (a.numbers.length <= idx) { + if (b.numbers.length <= idx) { + return 0; + } + + return -1; + } else if (b.numbers.length <= idx) { + return 1; + } + + const l = a.numbers[idx]; + const r = b.numbers[idx]; + if (l < r) { + return -1; + } + + if (r < l) { + return 1; + } + } +} + +function maxElement(subject: Array, cmp: (arg0: T, arg1: T) => number): T | undefined { + if (subject.length === 0) { + return undefined; + } + + let highest = subject[0]; + for (let idx = 1; idx < subject.length; ++idx) { + if (cmp(subject[idx], highest) > 0) { + highest = subject[idx]; + } + } + + return highest; +} + +export function resolveVsManId(ids: Iterable, candidateId: string): string { + const needle = '$(SxSVersion)'; + const loc = candidateId.indexOf(needle); + if (loc >= 0) { + if (candidateId.indexOf(needle, loc + 1) >= 0) { + throw new Error(i`${candidateId} can only contain ${needle} once`); + } + + // find all the ids which match the expected form + const matchingIds = new Array(); + const prefix = candidateId.slice(0, loc); + const suffix = candidateId.slice(loc + needle.length); + const regStr = '^' + escapeRegex(prefix) + '((?:\\d+\\.)*\\d+)' + escapeRegex(suffix) + '$'; + const reg = new RegExp(regStr); + for (const id of ids) { + const match = reg.exec(id); + if (match) { + matchingIds.push(new NumbersEntry(match[1])); + } + } + + const highest = maxElement(matchingIds, cmpNumbersEntry); + if (highest) { + return prefix + highest.rawStr + suffix; + } + } + + return candidateId; +} + +export class VsManDatabase { + private readonly idLookup = new Map>(); + + constructor(vsManContent: string) { + const vsMan = JSON.parse(vsManContent); + if (!Array.isArray(vsMan.packages)) { + throwUnexpectedManifest(); + } + + const lookup = new Map>(); + for (const p of vsMan.packages) { + if (!isMinimalPackage(p)) { + continue; + } + + const id = p.id; + const existing = lookup.get(id); + if (existing) { + existing.push(p); + } else { + lookup.set(id, [p]); + } + } + + for (const id of Array.from(lookup.keys())) { + maybeHydratePointerLikePackages(lookup, id); + } + + for (const entry of lookup.entries()) { + const newArr = new Array(); + for (const p of entry[1]) { + const flattened = flattenVsManPackage(p); + if (flattened) { + newArr.push(flattened); + } + } + + if (newArr.length != 0) { + this.idLookup.set(entry[0], newArr); + } + } + } + + get(id: string): Array | undefined { + return this.idLookup.get(resolveVsManId(this.idLookup.keys(), id)); + } + + get size() { + return this.idLookup.size; + } +} diff --git a/ce/ce/yaml/BaseMap.ts b/ce/ce/yaml/BaseMap.ts new file mode 100644 index 0000000000..87d547138c --- /dev/null +++ b/ce/ce/yaml/BaseMap.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isMap, isScalar, isSeq } from 'yaml'; +import { Entity } from './Entity'; +import { ScalarSequence } from './ScalarSequence'; +import { EntityFactory, Node, Primitive, Yaml, YAMLSequence } from './yaml-types'; + + +export /** @internal */ abstract class BaseMap extends Entity { + get keys(): Array { + return this.exists() ? this.node.items.map(each => this.asString(each.key)!) : []; + } + + get length(): number { + return this.exists() ? this.node.items.length : 0; + } + + getEntity = Yaml>(key: string, factory: EntityFactory): TEntity | undefined { + if (this.exists()) { + const v = this.node.get(key, true); + if (v) { + return new factory(v, this, key); + } + } + return undefined; + } + + getSequence(key: string, factory: EntityFactory | (new (node: Node, parent?: Yaml, key?: string) => ScalarSequence)) { + if (this.exists()) { + const v = this.node.get(key, true); + if (isSeq(v)) { + return new factory(v); + } + } + return undefined; + } + + getValue(key: string): Primitive | undefined { + if (this.exists()) { + const v = this.node.get(key, true); + if (isScalar(v)) { + return this.asPrimitive(v.value); + } + } + return undefined; + } + + delete(key: string) { + let result = false; + if (this.node) { + result = this.node.delete(key); + } + this.dispose(); + return result; + } + + clear() { + if (isMap(this.node) || isSeq(this.node)) { + this.node.items.length = 0; + } + this.dispose(true); + } +} diff --git a/ce/ce/yaml/Coerce.ts b/ce/ce/yaml/Coerce.ts new file mode 100644 index 0000000000..4111641bb0 --- /dev/null +++ b/ce/ce/yaml/Coerce.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isScalar } from 'yaml'; +import { Primitive } from './yaml-types'; + + +export /** @internal */ class Coerce { + static String(value: any): string | undefined { + if (isScalar(value)) { + value = value.value; + } + return typeof value === 'string' ? value : undefined; + } + static Number(value: any): number | undefined { + if (isScalar(value)) { + value = value.value; + } + return typeof value === 'number' ? value : undefined; + } + static Boolean(value: any): boolean | undefined { + if (isScalar(value)) { + value = value.value; + } + return typeof value === 'boolean' ? value : undefined; + } + static Primitive(value: any): Primitive | undefined { + if (isScalar(value)) { + value = value.value; + } + switch (typeof value) { + case 'boolean': + case 'number': + case 'string': + return value; + } + return undefined; + } +} diff --git a/ce/ce/yaml/CustomScalarMap.ts b/ce/ce/yaml/CustomScalarMap.ts new file mode 100644 index 0000000000..5786e9033f --- /dev/null +++ b/ce/ce/yaml/CustomScalarMap.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isScalar, Scalar } from 'yaml'; +import { BaseMap } from './BaseMap'; +import { EntityFactory, Yaml, YAMLDictionary } from './yaml-types'; + + +export /** @internal */ class CustomScalarMap> extends BaseMap { + protected constructor(protected factory: EntityFactory, node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(node, parent, key); + } + + add(key: string): TElement { + this.assert(true); + this.node.set(key, ''); + return this.get(key)!; + } + + + *[Symbol.iterator](): Iterator<[string, TElement]> { + if (this.node) { + for (const { key, value } of this.node.items) { + if (isScalar(value)) { + yield [key, new this.factory(value, this, key)]; + } + } + } + } + + get(key: string): TElement | undefined { + if (this.node) { + const v = this.node.get(key, true); + if (isScalar(v)) { + return new this.factory(v, this, key); + } + } + return undefined; + } + + set(key: string, value: TElement) { + if (value === undefined || value === null) { + throw new Error('Cannot set undefined or null to a map'); + } + + if (value.empty) { + throw new Error('Cannot set an empty entity to a map'); + } + + this.assert(true); // if we don't have a node at the moment, we need to create one. + + this.node.set(key, new Scalar(value)); + } +} diff --git a/ce/ce/yaml/Entity.ts b/ce/ce/yaml/Entity.ts new file mode 100644 index 0000000000..f5db200696 --- /dev/null +++ b/ce/ce/yaml/Entity.ts @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isMap, isScalar, isSeq } from 'yaml'; +import { i } from '../i18n'; +import { ErrorKind } from '../interfaces/error-kind'; +import { ValidationError } from '../interfaces/validation-error'; +import { isNullish } from '../util/checks'; +import { Node, Primitive, Yaml, YAMLDictionary } from './yaml-types'; + +/** An object that is backed by a YamlMAP node */ + +export /** @internal */ class Entity extends Yaml { + /**@internal*/ static override create(): YAMLDictionary { + return new YAMLDictionary(); + } + + protected setMember(name: string, value: Primitive | undefined): void { + this.assert(true); + + if (isNullish(value)) { + this.node.delete(name); + return; + } + + this.node.set(name, value); + } + + protected getMember(name: string): Primitive | undefined { + + return this.exists() ? this.node?.get(name, false) : undefined; + } + + override /** @internal */ *validate(): Iterable { + if (this.node && !isMap(this.node)) { + yield { message: i`Incorrect type for '${this.key}' - should be an object`, range: this.sourcePosition(), category: ErrorKind.IncorrectType }; + } + } + + has(key: string, kind?: 'sequence' | 'entity' | 'scalar'): boolean { + if (this.node) { + switch (kind) { + case 'sequence': + return isSeq(this.node.get(key)); + case 'entity': + return isMap(this.node.get(key)); + case 'scalar': + return isScalar(this.node.get(key)); + default: + return this.node.has(key); + } + } + return false; + } + + kind(key: string): 'sequence' | 'entity' | 'scalar' | 'string' | 'number' | 'boolean' | 'undefined' | undefined { + if (this.node) { + const v = this.node.get(key, true); + if (v === undefined) { + return 'undefined'; + } + + if (isSeq(v)) { + return 'sequence'; + } else if (isMap(v)) { + return 'entity'; + } else if (isScalar(v)) { + if (typeof v.value === 'string') { + return 'string'; + } else if (typeof v.value === 'number') { + return 'number'; + } else if (typeof v.value === 'boolean') { + return 'boolean'; + } + } + } + return undefined; + } + + is(key: string, kind: 'sequence' | 'entity' | 'scalar' | 'string' | 'number' | 'boolean'): boolean | undefined { + if (this.node) { + const v = this.node.get(key, true); + if (v === undefined) { + return undefined; + } + + switch (kind) { + case 'sequence': + return isSeq(v); + case 'entity': + return isMap(v); + case 'scalar': + return isScalar(v); + case 'string': + return isScalar(v) && typeof v.value === 'string'; + case 'number': + return isScalar(v) && typeof v.value === 'number'; + case 'boolean': + return isScalar(v) && typeof v.value === 'boolean'; + } + } + return false; + } +} diff --git a/ce/ce/yaml/EntityMap.ts b/ce/ce/yaml/EntityMap.ts new file mode 100644 index 0000000000..a1dbaf827c --- /dev/null +++ b/ce/ce/yaml/EntityMap.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Dictionary } from '../interfaces/collections'; +import { BaseMap } from './BaseMap'; +import { EntityFactory, Node, Yaml, YAMLDictionary } from './yaml-types'; + + +export /** @internal */ abstract class EntityMap> extends BaseMap implements Dictionary, Iterable<[string, TElement]> { + protected constructor(protected factory: EntityFactory, node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(node, parent, key); + } + + get values(): Iterable { + return this.exists() ? this.node.items.map(each => new this.factory(each.value)) : []; + } + + *[Symbol.iterator](): Iterator<[string, TElement]> { + if (this.node) { + for (const each of this.node.items) { + const k = this.asString(each.key); + if (k) { + yield [k, new this.factory(each.value)]; + } + } + } + } + + add(key: string): TElement { + if (this.has(key)) { + return this.get(key)!; + } + this.assert(true); + const child = this.factory.create(); + this.set(key, child); + return new this.factory(this.factory.create(), this, key); + } + + get(key: string): TElement | undefined { + return this.getEntity(key, this.factory); + } + + set(key: string, value: TElement) { + if (value === undefined || value === null) { + throw new Error('Cannot set undefined or null to a map'); + } + + if (value.empty) { + throw new Error('Cannot set an empty entity to a map'); + } + + // if we don't have a node at the moment, we need to create one. + this.assert(true); + + this.node.set(key, value.node); + } +} diff --git a/ce/ce/yaml/EntitySequence.ts b/ce/ce/yaml/EntitySequence.ts new file mode 100644 index 0000000000..c26a054b1f --- /dev/null +++ b/ce/ce/yaml/EntitySequence.ts @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isMap, isScalar, isSeq } from 'yaml'; +import { EntityFactory, Yaml, YAMLDictionary, YAMLSequence } from './yaml-types'; + +/** + * EntitySequence is expressed as either a single entity or a sequence of entities. + */ + +export /** @internal */ class EntitySequence> extends Yaml { + protected constructor(protected factory: EntityFactory, node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(node, parent, key); + } + + static override create(): YAMLDictionary { + return new YAMLDictionary(); + } + + get length(): number { + if (this.node) { + if (isSeq(this.node)) { + return this.node.items.length; + } + if (isMap(this.node)) { + return 1; + } + } + return 0; + } + + add(value: TElement) { + if (value === undefined || value === null) { + throw new Error('Cannot add undefined or null to a sequence'); + } + + if (value.empty) { + throw new Error('Cannot add an empty entity to a sequence'); + } + + if (!this.node) { + // if we don't have a node at the moment, we need to create one. + this.assert(true, value.node); + return; + } + + if (isMap(this.node)) { + // this is currently a single item. + // we need to convert it to a sequence + const n = this.node; + const seq = new YAMLSequence(); + seq.add(n); + this.node = seq; + + // fall thru to the sequnce add + } + + if (isSeq(this.node)) { + this.node.add(value.node); + return; + } + } + + get(index: number): TElement | undefined { + if (isSeq(this.node)) { + return this.node.items[index]; + } + + if (isScalar(this.node) && index === 0) { + return this.node.value; + } + + return undefined; + } + + *[Symbol.iterator](): Iterator { + if (isScalar(this.node)) { + return yield new this.factory(this.node); + } + yield* EntitySequence.generator(this); + } + + clear() { + if (isSeq(this.node)) { + // just make sure the collection is emptied first + this.node.items.length = 0; + } + this.dispose(true); + } + + protected static *generator>(sequence: EntitySequence) { + if (isSeq(sequence.node)) { + for (const item of sequence.node.items) { + yield new sequence.factory(item); + } + } + } +} diff --git a/ce/ce/yaml/Flags.ts b/ce/ce/yaml/Flags.ts new file mode 100644 index 0000000000..177a165c78 --- /dev/null +++ b/ce/ce/yaml/Flags.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Scalar } from 'yaml'; +import { Yaml, YAMLSequence } from './yaml-types'; + + +export /** @internal */ class Flags extends Yaml { + + has(flag: string) { + if (this.node) { + return this.node.items.some(each => each.value === flag); + } + return false; + } + + set(flag: string, value: boolean) { + this.assert(true); + if (value) { + this.node.add(new Scalar(flag)); + } else { + this.node.delete(flag); + } + } +} diff --git a/ce/ce/yaml/ScalarMap.ts b/ce/ce/yaml/ScalarMap.ts new file mode 100644 index 0000000000..7afb8db248 --- /dev/null +++ b/ce/ce/yaml/ScalarMap.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isScalar } from 'yaml'; +import { BaseMap } from './BaseMap'; +import { Primitive } from './yaml-types'; + + +export /** @internal */ class ScalarMap extends BaseMap { + get(key: string): TElement | undefined { + return this.getValue(key); + } + + set(key: string, value: TElement) { + this.assert(true); + this.node.set(key, value); + } + + + add(key: string): TElement { + this.assert(true); + this.node.set(key, ''); + return this.getValue(key); + } + + *[Symbol.iterator](): Iterator<[string, TElement]> { + if (this.node) { + for (const { key, value } of this.node.items) { + const v = isScalar(value) ? this.asPrimitive(value) : undefined; + if (v) { + yield [key, value]; + } + } + } + } +} diff --git a/ce/ce/yaml/ScalarSequence.ts b/ce/ce/yaml/ScalarSequence.ts new file mode 100644 index 0000000000..434e036f77 --- /dev/null +++ b/ce/ce/yaml/ScalarSequence.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isScalar, isSeq, Scalar, YAMLSeq } from 'yaml'; +import { Primitive, Yaml, YAMLScalar, YAMLSequence } from './yaml-types'; + +/** + * ScalarSequence is expressed as either a single scalar value or a sequence of scalar values. + */ + +export /** @internal */ class ScalarSequence extends Yaml | Scalar> { + static override create(): YAMLScalar { + return new YAMLScalar(''); + } + + get length(): number { + if (this.node) { + if (isSeq(this.node)) { + return this.node.items.length; + } + if (isScalar(this.node)) { + return 1; + } + } + return 0; + } + + has(value: TElement) { + for (const each of this) { + if (value === each) { + return true; + } + } + return false; + } + + add(value: TElement) { + if (value === undefined || value === null) { + throw new Error('Cannot add undefined or null to a sequence'); + } + + // check if the value is already in the set + if (this.has(value)) { + return; + } + + if (!this.node) { + // if we don't have a node at the moment, we need to create one. + this.assert(true); + (this.node).value = value; + return; + } + + if (isScalar(this.node)) { + // this is currently a single item. + // we need to convert it to a sequence + const n = this.node; + const seq = new YAMLSequence(); + seq.add(n); + this.dispose(true); + this.assert(true, seq); + // fall thru to the sequnce add + } + + if (isSeq(this.node)) { + this.node.add((new Scalar(value))); + } + } + + delete(value: TElement) { + if (isSeq(this.node)) { + for (let i = 0; i < this.node.items.length; i++) { + if (value === this.asPrimitive(this.node.items[i])) { + this.node.items.splice(i, 1); + return true; + } + } + } + if (isScalar(this.node) && this.node.value === value) { + this.dispose(true); + return true; + } + return false; + } + + get(index: number): TElement | undefined { + if (isSeq(this.node)) { + return this.node.items[index]; + } + + if (isScalar(this.node) && index === 0) { + return this.node.value; + } + + return undefined; + } + + *[Symbol.iterator](): Iterator { + if (isScalar(this.node)) { + return yield this.asPrimitive(this.node.value); + } + if (isSeq(this.node)) { + for (const each of this.node.items.values()) { + const v = this.asPrimitive(each); + if (v) { + yield v; + } + } + } + } + + clear() { + if (isSeq(this.node)) { + // just make sure the collection is emptied first + this.node.items.length = 0; + } + this.dispose(true); + } +} diff --git a/ce/ce/yaml/strings.ts b/ce/ce/yaml/strings.ts new file mode 100644 index 0000000000..abd41e38e0 --- /dev/null +++ b/ce/ce/yaml/strings.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Dictionary, Strings as IStrings } from '../interfaces/collections'; +import { EntityMap } from './EntityMap'; +import { ScalarSequence } from './ScalarSequence'; +import { Yaml, YAMLDictionary, YAMLScalar, YAMLSequence } from './yaml-types'; + + +export class Strings extends ScalarSequence implements IStrings { + constructor(node?: YAMLSequence | YAMLScalar, parent?: Yaml, key?: string) { + super(node, parent, key); + } +} + +export class StringsMap extends EntityMap> implements Dictionary { + constructor(node?: YAMLDictionary, parent?: Yaml, key?: string) { + super(Strings, node, parent, key); + } +} \ No newline at end of file diff --git a/ce/ce/yaml/yaml-types.ts b/ce/ce/yaml/yaml-types.ts new file mode 100644 index 0000000000..141318c055 --- /dev/null +++ b/ce/ce/yaml/yaml-types.ts @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isCollection, isMap, isScalar, isSeq, Scalar, YAMLMap, YAMLSeq } from 'yaml'; +import { ValidationError } from '../interfaces/validation-error'; +import { isNullish } from '../util/checks'; + +export class YAMLDictionary extends YAMLMap { } +export class YAMLSequence extends YAMLSeq { } +export class YAMLScalar extends Scalar { } +export type Primitive = string | number | boolean; +export type Node = YAMLDictionary | YAMLSequence | YAMLScalar; +export type Range = [number, number, number]; + +export /** @internal */ abstract class Yaml { + constructor(/** @internal */ node?: ThisType, protected parent?: Yaml, protected key?: string) { + this.node = node; + if (!(>(this.constructor)).create) { + throw new Error(`class ${this.constructor.name} is missing implementation for create`); + } + } + + /** returns the current node as a JSON string */ + toString(): string { + return this.node?.toJSON() ?? ''; + } + + /** + * Coersion function to string + * + * This will pass the coercion up to the parent if it exists + * (or otherwise overridden in the subclass) + * + * Which allows for value overriding + */ + protected asString(value: any): string | undefined { + if (this.parent) { + return this.parent.asString(value); + } + + return value === undefined ? undefined : (isScalar(value) ? value.value : value).toString(); + } + + /** + * Coersion function to number + * + * This will pass the coercion up to the parent if it exists + * (or otherwise overridden in the subclass) + * + * Which allows for value overriding + */ + asNumber(value: any): number | undefined { + if (this.parent) { + return this.parent.asNumber(value); + } + + if (isScalar(value)) { + value = value.value; + } + return typeof value === 'number' ? value : undefined; + } + + /** + * Coersion function to boolean + * + * This will pass the coercion up to the parent if it exists + * (or otherwise overridden in the subclass) + * + * Which allows for value overriding + */ + asBoolean(value: any): boolean | undefined { + if (this.parent) { + return this.parent.asBoolean(value); + } + + if (isScalar(value)) { + value = value.value; + } + return typeof value === 'boolean' ? value : undefined; + } + + /** + * Coersion function to any primitive + * + * This will pass the coercion up to the parent if it exists + * (or otherwise overridden in the subclass) + * + * Which allows for value overriding + */ + asPrimitive(value: any): Primitive | undefined { + if (this.parent) { + return this.parent.asPrimitive(value); + } + + if (isScalar(value)) { + value = value.value; + } + switch (typeof value) { + case 'boolean': + case 'number': + case 'string': + return value; + } + return undefined; + } + + + get root(): Yaml { + return this.parent ? this.parent.root : this; + } + + protected createNode(): ThisType { + return (>this.constructor).create(); + } + + /**@internal*/ static create() { + throw new Error('creator not Not implemented on base class.'); + } + + private _node: ThisType | undefined; + + get node(): ThisType | undefined { + if (this._node) { + return this._node; + } + + if (this.key && this.parent && isMap(this.parent?.node)) { + this._node = this.parent.node.get(this.key, true); + } + + return this._node; + } + + set node(n: ThisType | undefined) { + this._node = n; + } + + sourcePosition(key?: string | number): Range | undefined { + if (!this.node) { + return undefined; + } + if (key !== undefined) { + if ((isMap(this.node) || isSeq(this.node))) { + const node = this.node.get(key); + if (node) { + return node.range || undefined; + } + return undefined; + } + if (isScalar(this.node)) { + throw new Error('Scalar does not have a key to get a source position'); + } + } + return this.node?.range || undefined; + } + + /** will dispose of this object if it is empty (or forced) */ + dispose(force = false, deleteFromParent = true) { + if ((this.empty || force) && this.node) { + if (deleteFromParent) { + this.parent?.deleteChild(this); + } + this.node = undefined; + } + } + + /** if this node has any data, this should return false */ + get empty(): boolean { + if (isCollection(this.node)) { + return !(this.node?.items.length); + } + return !isNullish(this.node?.value); + } + + /** @internal */ exists(): this is Yaml & { node: ThisType } { + if (this.node) { + return true; + } + // well, if we're lazy and haven't instantiated it yet, check if it's created. + if (this.key && this.parent && isMap(this.parent.node)) { + this.node = this.parent.node.get(this.key); + if (this.node) { + return true; + } + } + return false; + } + /** @internal */ assert(recreateIfDisposed = false, node = this.node): asserts this is Yaml & { node: ThisType } { + if (this.node && this.node === node) { + return; // quick and fast + } + + if (recreateIfDisposed) { + // ensure that this node is the node we're supposed to be. + this.node = node; + + if (this.parent) { + // ensure that the parent is not disposed + (this.parent).assert(true); + + if (isMap(this.parent.node)) { + if (this.key) { + // we have a parent, and the key, we can add the node. + // let's just check if there is one first + this.node = this.node || this.parent.node.get(this.key) || this.createNode(); + this.parent.node.set(this.key, this.node); + return; + } + // the parent is a map, but we don't have a key, so we can't add the node. + throw new Error('Parent is a map, but we don\'t have a key'); + } + + if (isSeq(this.parent.node)) { + this.node = this.node || this.parent.node.get(this.key) || this.createNode(); + this.parent.node.add(this.node); + return; + } + + throw new Error('YAML parent is not a container.'); + } + } + throw new Error('YAML node is undefined'); + } + + protected deleteChild(child: Yaml) { + if (!child.node) { + // this child is already disposed + return; + } + + this.assert(); + + if (isMap(this.node)) { + if (child.key) { + this.node.delete(child.key); + child.dispose(true, false); + this.dispose(); // clean up if this is empty + return; + } + } + + if (isCollection(this.node)) { + // child is in some kind of collection. + // we should be able to find the child's index and remove it. + for (let i = 0; i < this.node.items.length; i++) { + if (this.node.items[i].value === child.node) { + this.node.delete(this.node.items[i].key); + child.dispose(true, false); + this.dispose(); // clean up if this is empty + return; + } + } + + // if we get here, we didn't find the child. + // but, it's not in the object, so we're good I guess + throw new Error('Child Node not found trying to delete'); + // return; + } + + throw new Error('this node does not have children.'); + } + + *validate(): Iterable { + // shh. + } +} + +export /** @internal */ interface EntityFactory> extends NodeFactory { + /**@internal*/ new(node: TNode, parent?: Yaml, key?: string): TEntity; +} + +export /** @internal */ interface NodeFactory extends Function { + /**@internal*/ create(): TNode; +} + diff --git a/ce/ce/yaml/yaml.ts b/ce/ce/yaml/yaml.ts new file mode 100644 index 0000000000..9f4ea54080 --- /dev/null +++ b/ce/ce/yaml/yaml.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Document, Node, Pair, parseDocument, Scalar, stringify, visit, YAMLMap, YAMLSeq } from 'yaml'; +import { StringOrStrings } from '../interfaces/metadata/metadata-format'; + +/** @internal */ +export const createNode = (v: any, b = true) => parseDocument('', { prettyErrors: false }).createNode(v, {}); + +/** @internal */ +export function getOrCreateMap(node: Document.Parsed | YAMLMap, name: string): YAMLMap { + let m = node.get(name); + if (m) { + return m; + } + + node.set(name, m = new YAMLMap()); + return m; +} + +export function getStrings(node: Document.Parsed | YAMLMap, name: string): Array { + const r = node.get(name); + if (r) { + if (r instanceof YAMLSeq) { + return r.items.map((each: any) => each.value); + } + if (typeof r === 'string') { + return [r]; + } + } + return []; +} + +export function setStrings(node: Document.Parsed | YAMLMap, name: string, value: StringOrStrings) { + if (Array.isArray(value)) { + switch (value.length) { + case 0: + return node.set(name, undefined); + case 1: + return node.set(name, value[0]); + } + return node.set(name, createNode(value, true)); + } + node.set(name, value); +} + +export function getPair(from: YAMLMap, name: string): Pair | undefined { + return from.items.find((each: any) => (each.key).value === name); +} + +export function serialize(value: any) { + const document = new Document(value); + visit(document, { + Seq: (k, n, p) => { + // set arrays to [ ... ] instead of one value per line. + n.flow = true; + } + }); + return document.toString(); +} + +export function isYAML(path: string) { + path = path.toLowerCase(); + return path.endsWith('.yml') || path.endsWith('.yaml') || path.endsWith('.json'); +} + +export function toYAML(content: string) { + if (content.charAt(0) === '{') { + // the content is in JSON format. + // let's force it to YAML + try { + return stringify(JSON.parse(content)).toString(); + } catch (e: any) { + // no worries. + } + } + return content; +} diff --git a/ce/common/.default-eslintrc.yaml b/ce/common/.default-eslintrc.yaml new file mode 100644 index 0000000000..929a09d4f9 --- /dev/null +++ b/ce/common/.default-eslintrc.yaml @@ -0,0 +1,100 @@ +--- +parser: "@typescript-eslint/parser" +plugins: +- "@typescript-eslint" +- "notice" + +env: + es6: true + node: true +extends: +- eslint:recommended +- plugin:@typescript-eslint/recommended +globals: + Atomics: readonly + SharedArrayBuffer: readonly +parserOptions: + ecmaVersion: 2018 + sourceType: module + warnOnUnsupportedTypeScriptVersion : false + project: tsconfig.json +rules: + + "no-trailing-spaces" : "error" + "space-in-parens": "error" + "no-cond-assign" : 'off' + "keyword-spacing": + - 'error' + - overrides: + this: + before: false + "@typescript-eslint/explicit-module-boundary-types" : 'off' + "@typescript-eslint/no-non-null-assertion": 'off' + "@typescript-eslint/no-use-before-define" : 'off' + "@typescript-eslint/no-this-alias" : 'off' + "@typescript-eslint/interface-name-prefix": 'off' + "@typescript-eslint/explicit-function-return-type": 'off' + "@typescript-eslint/no-explicit-any": 'off' + "@typescript-eslint/no-empty-interface": 'off' + "@typescript-eslint/no-namespace": 'off' + "@typescript-eslint/explicit-member-accessibility": 'off' + "@typescript-eslint/no-unused-vars": 'off' + "@typescript-eslint/no-parameter-properties": 'off' + "@typescript-eslint/no-angle-bracket-type-assertion" : 'off' + "@typescript-eslint/no-floating-promises": 'error' + "require-atomic-updates" : 'off' + "@typescript-eslint/consistent-type-assertions" : + - error + - assertionStyle: 'angle-bracket' + "@typescript-eslint/array-type": + - error + - default: generic + indent: + - warn + - 2 + - SwitchCase : 1 + ObjectExpression: first + + + "@typescript-eslint/indent": + - 0 + - 2 + no-undef: 'off' + no-unused-vars: 'off' + linebreak-style: + - 'error' + - unix + quotes: + - error + - single + semi: + - error + - always + no-multiple-empty-lines: + - error + - max: 2 + maxBOF: 0 + maxEOF: 1 + + "notice/notice": + - "error" + - { "templateFile": "../common/header.txt" } + + ## BEGIN SECTION + ## The rules in this section cover security compliance item "Verify banned APIs are not used". + no-eval: "error" + # Disable the base rule as required to use @typescript/eslint/no-implied-eval. For details: + # https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-implied-eval.md#how-to-use + no-implied-eval: "off" + "@typescript-eslint/no-implied-eval": "error" + "no-restricted-syntax": + - "error" + - selector: "CallExpression[callee.name='execUnsafeLocalFunction']" + message: "execUnsafeLocalFunction is banned" + - selector: "CallExpression[callee.property.name='execUnsafeLocalFunction']" + message: "execUnsafeLocalFunction is banned" + - selector: "CallExpression[callee.name='setInnerHTMLUnsafe']" + message: "setInnerHTMLUnsafe is banned" + - selector: "CallExpression[callee.property.name='setInnerHTMLUnsafe']" + message: "setInnerHTMLUnsafe is banned" + ## END SECTION \ No newline at end of file diff --git a/ce/common/config/rush/.npmrc b/ce/common/config/rush/.npmrc new file mode 100644 index 0000000000..b902e270cc --- /dev/null +++ b/ce/common/config/rush/.npmrc @@ -0,0 +1,22 @@ +# Rush uses this file to configure the NPM package registry during installation. It is applicable +# to PNPM, NPM, and Yarn package managers. It is used by operations such as "rush install", +# "rush update", and the "install-run.js" scripts. +# +# NOTE: The "rush publish" command uses .npmrc-publish instead. +# +# Before invoking the package manager, Rush will copy this file to the folder where installation +# is performed. The copied file will omit any config lines that reference environment variables +# that are undefined in that session; this avoids problems that would otherwise result due to +# a missing variable being replaced by an empty string. +# +# * * * SECURITY WARNING * * * +# +# It is NOT recommended to store authentication tokens in a text file on a lab machine, because +# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, +# for example if the machine loses power. A safer practice is to pass the token via an +# environment variable, which can be referenced from .npmrc using ${} expansion. For example: +# +# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} +# +registry=https://registry.npmjs.org/ +always-auth=false diff --git a/ce/common/config/rush/.npmrc-publish b/ce/common/config/rush/.npmrc-publish new file mode 100644 index 0000000000..7ab44c18d6 --- /dev/null +++ b/ce/common/config/rush/.npmrc-publish @@ -0,0 +1,20 @@ +# This config file is very similar to common/config/rush/.npmrc, except that .npmrc-publish +# is used by the "rush publish" command, as publishing often involves different credentials +# and registries than other operations. +# +# Before invoking the package manager, Rush will copy this file to "common/temp/publish-home/.npmrc" +# and then temporarily map that folder as the "home directory" for the current user account. +# This enables the same settings to apply for each project folder that gets published. The copied file +# will omit any config lines that reference environment variables that are undefined in that session; +# this avoids problems that would otherwise result due to a missing variable being replaced by +# an empty string. +# +# * * * SECURITY WARNING * * * +# +# It is NOT recommended to store authentication tokens in a text file on a lab machine, because +# other unrelated processes may be able to read the file. Also, the file may persist indefinitely, +# for example if the machine loses power. A safer practice is to pass the token via an +# environment variable, which can be referenced from .npmrc using ${} expansion. For example: +# +# //registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN} +# diff --git a/ce/common/config/rush/command-line.json b/ce/common/config/rush/command-line.json new file mode 100644 index 0000000000..faf80be5ae --- /dev/null +++ b/ce/common/config/rush/command-line.json @@ -0,0 +1,162 @@ +/** + * This configuration file defines custom commands for the "rush" command-line. + * For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/command-line.schema.json", + /** + * Custom "commands" introduce new verbs for the command-line. To see the help for these + * example commands, try "rush --help", "rush my-bulk-command --help", or + * "rush my-global-command --help". + */ + "commands": [ + { + "commandKind": "global", + "name": "set-versions", + "summary": "set the patch version in the package.json files", + "shellCommand": "node .scripts/set-versions.js" + }, + { + "commandKind": "global", + "name": "reset-versions", + "summary": "reset the patch version in the package.json files to .0", + "shellCommand": "node .scripts/set-versions.js --reset" + }, + { + "commandKind": "global", + "name": "sync-versions", + "summary": "sync versions of sibling projects", + "shellCommand": "node .scripts/sync-versions.js" + }, + { + "commandKind": "global", + "name": "watch", + "summary": "run npm watch on all projects", + "shellCommand": "node .scripts/watch.js" + }, + { + "commandKind": "global", + "name": "clean", + "summary": "run npm clean on all projects", + "shellCommand": "node .scripts/npm-run.js clean" + }, + { + "commandKind": "global", + "name": "test", + "summary": "run all npm test", + "shellCommand": "node .scripts/npm-run.js test" + }, + { + "commandKind": "global", + "name": "test-ci", + "summary": "run all npm test-ci (build then run test)", + "shellCommand": "node .scripts/npm-run.js test-ci" + }, + { + "commandKind": "global", + "name": "fix", + "summary": "run all npm fix", + "shellCommand": "node .scripts/npm-run.js eslint-fix" + }, + { + "commandKind": "global", + "name": "lint", + "summary": "run all npm lint", + "shellCommand": "node .scripts/npm-run.js eslint" + } + ], + /** + * Custom "parameters" introduce new parameters for specified Rush command-line commands. + * For example, you might define a "--production" parameter for the "rush build" command. + */ + "parameters": [ + // { + // /** + // * (Required) Determines the type of custom parameter. + // * A "flag" is a custom command-line parameter whose presence acts as an on/off switch. + // */ + // "parameterKind": "flag", + // + // /** + // * (Required) The long name of the parameter. It must be lower-case and use dash delimiters. + // */ + // "longName": "--my-flag", + // + // /** + // * An optional alternative short name for the parameter. It must be a dash followed by a single + // * lower-case or upper-case letter, which is case-sensitive. + // * + // * NOTE: The Rush developers recommend that automation scripts should always use the long name + // * to improve readability. The short name is only intended as a convenience for humans. + // * The alphabet letters run out quickly, and are difficult to memorize, so *only* use + // * a short name if you expect the parameter to be needed very often in everyday operations. + // */ + // "shortName": "-m", + // + // /** + // * (Required) A long description to be shown in the command-line help. + // * + // * Whenever you introduce commands/parameters, taking a little time to write meaningful + // * documentation can make a big difference for the developer experience in your repo. + // */ + // "description": "A custom flag parameter that is passed to the scripts that are invoked when building projects", + // + // /** + // * (Required) A list of custom commands and/or built-in Rush commands that this parameter may + // * be used with. The parameter will be appended to the shell command that Rush invokes. + // */ + // "associatedCommands": [ "build", "rebuild" ] + // }, + // + // { + // /** + // * (Required) Determines the type of custom parameter. + // * A "flag" is a custom command-line parameter whose presence acts as an on/off switch. + // */ + // "parameterKind": "choice", + // "longName": "--my-choice", + // "description": "A custom choice parameter for the \"my-global-command\" custom command", + // + // "associatedCommands": [ "my-global-command" ], + // + // /** + // * Normally if a parameter is omitted from the command line, it will not be passed + // * to the shell command. this value will be inserted by default. Whereas if a "defaultValue" + // * is defined, the parameter will always be passed to the shell command, and will use the + // * default value if unspecified. The value must be one of the defined alternatives. + // */ + // "defaultValue": "vanilla", + // + // /** + // * (Required) A list of alternative argument values that can be chosen for this parameter. + // */ + // "alternatives": [ + // { + // /** + // * A token that is one of the alternatives that can be used with the choice parameter, + // * e.g. "vanilla" in "--flavor vanilla". + // */ + // "name": "vanilla", + // + // /** + // * A detailed description for the alternative that can be shown in the command-line help. + // * + // * Whenever you introduce commands/parameters, taking a little time to write meaningful + // * documentation can make a big difference for the developer experience in your repo. + // */ + // "description": "Use the vanilla flavor (the default)" + // }, + // + // { + // "name": "chocolate", + // "description": "Use the chocolate flavor" + // }, + // + // { + // "name": "strawberry", + // "description": "Use the strawberry flavor" + // } + // ] + // } + ] +} \ No newline at end of file diff --git a/ce/common/config/rush/common-versions.json b/ce/common/config/rush/common-versions.json new file mode 100644 index 0000000000..f71e8322a5 --- /dev/null +++ b/ce/common/config/rush/common-versions.json @@ -0,0 +1,62 @@ +/** + * This configuration file specifies NPM dependency version selections that affect all projects + * in a Rush repo. For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/common-versions.schema.json", + + /** + * A table that specifies a "preferred version" for a given NPM package. This feature is typically used + * to hold back an indirect dependency to a specific older version, or to reduce duplication of indirect dependencies. + * + * The "preferredVersions" value can be any SemVer range specifier (e.g. "~1.2.3"). Rush injects these values into + * the "dependencies" field of the top-level common/temp/package.json, which influences how the package manager + * will calculate versions. The specific effect depends on your package manager. Generally it will have no + * effect on an incompatible or already constrained SemVer range. If you are using PNPM, similar effects can be + * achieved using the pnpmfile.js hook. See the Rush documentation for more details. + * + * After modifying this field, it's recommended to run "rush update --full" so that the package manager + * will recalculate all version selections. + */ + "preferredVersions": { + /** + * When someone asks for "^1.0.0" make sure they get "1.2.3" when working in this repo, + * instead of the latest version. + */ + // "some-library": "1.2.3" + }, + + /** + * When set to true, for all projects in the repo, all dependencies will be automatically added as preferredVersions, + * except in cases where different projects specify different version ranges for a given dependency. For older + * package managers, this tended to reduce duplication of indirect dependencies. However, it can sometimes cause + * trouble for indirect dependencies with incompatible peerDependencies ranges. + * + * The default value is true. If you're encountering installation errors related to peer dependencies, + * it's recommended to set this to false. + * + * After modifying this field, it's recommended to run "rush update --full" so that the package manager + * will recalculate all version selections. + */ + // "implicitlyPreferredVersions": false, + + /** + * The "rush check" command can be used to enforce that every project in the repo must specify + * the same SemVer range for a given dependency. However, sometimes exceptions are needed. + * The allowedAlternativeVersions table allows you to list other SemVer ranges that will be + * accepted by "rush check" for a given dependency. + * + * IMPORTANT: THIS TABLE IS FOR *ADDITIONAL* VERSION RANGES THAT ARE ALTERNATIVES TO THE + * USUAL VERSION (WHICH IS INFERRED BY LOOKING AT ALL PROJECTS IN THE REPO). + * This design avoids unnecessary churn in this file. + */ + "allowedAlternativeVersions": { + /** + * For example, allow some projects to use an older TypeScript compiler + * (in addition to whatever "usual" version is being used by other projects in the repo): + */ + // "typescript": [ + // "~2.4.0" + // ] + } +} diff --git a/ce/common/config/rush/deploy.json b/ce/common/config/rush/deploy.json new file mode 100644 index 0000000000..805bd4bd03 --- /dev/null +++ b/ce/common/config/rush/deploy.json @@ -0,0 +1,111 @@ +/** + * This configuration file defines a deployment scenario for use with the "rush deploy" command. + * The default scenario file path is "deploy.json"; additional files use the naming pattern + * "deploy-.json". For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/deploy-scenario.schema.json", + /** + * The "rush deploy" command prepares a deployment folder, starting from the main project and collecting + * all of its dependencies (both NPM packages and other Rush projects). The main project is specified + * using the "--project" parameter. The "deploymentProjectNames" setting lists the allowable choices for + * the "--project" parameter; this documents the intended deployments for your monorepo and helps validate + * that "rush deploy" is invoked correctly. If there is only one item in the "deploymentProjectNames" array, + * then "--project" can be omitted. The names should be complete package names as declared in rush.json. + * + * If the main project should include other unrelated Rush projects, add it to the "projectSettings" section, + * and then specify those projects in the "additionalProjectsToInclude" list. + */ + "deploymentProjectNames": [ + "@microsoft/vcpkg-ce" + ], + /** + * When deploying a local Rush project, the package.json "devDependencies" are normally excluded. + * If you want to include them, set "includeDevDependencies" to true. + * + * The default value is false. + */ + "includeDevDependencies": false, + /** + * When deploying a local Rush project, normally the .npmignore filter is applied so that Rush only copies + * files that would be packaged by "npm pack". Setting "includeNpmIgnoreFiles" to true will disable this + * filtering so that all files are copied (with a few trivial exceptions such as the "node_modules" folder). + * + * The default value is false. + */ + // "includeNpmIgnoreFiles": true, + /** + * To improve backwards compatibility with legacy packages, the PNPM package manager installs extra links in the + * node_modules folder that enable packages to import undeclared dependencies. In some cases this workaround may + * double the number of links created. If your deployment does not require this workaround, you can set + * "omitPnpmWorkaroundLinks" to true to avoid creating the extra links. + * + * The default value is false. + */ + // "omitPnpmWorkaroundLinks": true, + /** + * Specify how links (symbolic links, hard links, and/or NTFS junctions) will be created in the deployed folder: + * + * - "default": Create the links while copying the files; this is the default behavior. + * - "script": A Node.js script called "create-links.js" will be written. When executed, this script will + * create the links described in the "deploy-metadata.json" output file. + * - "none": Do nothing; some other tool may create the links later. + */ + "linkCreation": "script", + /** + * If this path is specified, then after "rush deploy", recursively copy the files from this folder to + * the deployment target folder (common/deploy). This can be used to provide additional configuration files + * or scripts needed by the server when deploying. The path is resolved relative to the repository root. + */ + "folderToCopy": "assets/", + /** + * Customize how Rush projects are processed during deployment. + */ + "projectSettings": [ + { + // /** + // * The full package name of the project, which must be declared in rush.json. + // */ + "projectName": "@microsoft/vcpkg-ce", + // + // /** + // * A list of additional local Rush projects to be deployed with this project (beyond the package.json + // * dependencies). Specify full package names, which must be declared in rush.json. + // */ + // "additionalProjectsToInclude": [ + // // "@my-scope/my-project2" + // ], + // + // /** + // * When deploying a project, the included dependencies are normally determined automatically based on + // * package.json fields such as "dependencies", "peerDependencies", and "optionalDependencies", + // * subject to other deployment settings such as "includeDevDependencies". However, in cases where + // * that information is not accurate, you can use "additionalDependenciesToInclude" to add more + // * packages to the list. + // * + // * The list can include any package name that is installed by Rush and resolvable via Node.js module + // * resolution; however, if it resolves to a local Rush project, the "additionalProjectsToInclude" + // * field will not be recursively applied. + // */ + // "additionalDependenciesToInclude": [ + // // "@rushstack/node-core-library" + // ], + // + // /** + // * This setting prevents specific dependencies from being deployed. It only filters dependencies that + // * are explicitly declared in package.json for this project. It does not affect dependencies added + // * via "additionalProjectsToInclude" or "additionalDependenciesToInclude", nor does it affect indirect + // * dependencies. + // * + // * The "*" wildcard may be used to match zero or more characters. For example, if your project already + // * bundles its own dependencies, specify "dependenciesToExclude": [ "*" ] to exclude all package.json + // * dependencies. + // */ + "dependenciesToExclude": [ + "@types/*", + "typescript", + "translate-strings" + ] + } + ] +} \ No newline at end of file diff --git a/ce/common/config/rush/experiments.json b/ce/common/config/rush/experiments.json new file mode 100644 index 0000000000..abbbba9471 --- /dev/null +++ b/ce/common/config/rush/experiments.json @@ -0,0 +1,36 @@ +/** + * This configuration file allows repo maintainers to enable and disable experimental + * Rush features. For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/experiments.schema.json", + + /** + * Rush 5.14.0 improved incremental builds to ignore spurious changes in the pnpm-lock.json file. + * This optimization is enabled by default. If you encounter a problem where "rush build" is neglecting + * to build some projects, please open a GitHub issue. As a workaround you can uncomment this line + * to temporarily restore the old behavior where everything must be rebuilt whenever pnpm-lock.json + * is modified. + */ + // "legacyIncrementalBuildDependencyDetection": true, + + /** + * By default, rush passes --no-prefer-frozen-lockfile to 'pnpm install'. + * Set this option to true to pass '--frozen-lockfile' instead. + */ + // "usePnpmFrozenLockfileForRushInstall": true, + + /** + * If true, the chmod field in temporary project tar headers will not be normalized. + * This normalization can help ensure consistent tarball integrity across platforms. + */ + // "noChmodFieldInTarHeaderNormalization": true, + + /** + * If true, the build cache feature is enabled. To use this feature, a common/config/rush/build-cache.json + * file must be created with configuration options. + * + * See https://github.com/microsoft/rushstack/issues/2393 for details about this experimental feature. + */ + // "buildCache": true +} diff --git a/ce/common/config/rush/pnpm-lock.yaml b/ce/common/config/rush/pnpm-lock.yaml new file mode 100644 index 0000000000..aebec62bb5 --- /dev/null +++ b/ce/common/config/rush/pnpm-lock.yaml @@ -0,0 +1,2818 @@ +lockfileVersion: 5.3 + +specifiers: + '@rush-temp/tar-stream': file:./projects/tar-stream.tgz + '@rush-temp/vcpkg-ce': file:./projects/vcpkg-ce.tgz + '@rush-temp/vcpkg-ce.test': file:./projects/vcpkg-ce.test.tgz + '@snyk/nuget-semver': 1.3.0 + '@types/cli-progress': 3.9.2 + '@types/marked': 4.0.2 + '@types/marked-terminal': 3.1.3 + '@types/micromatch': 4.0.2 + '@types/mocha': 9.1.0 + '@types/node': 17.0.15 + '@types/semver': 7.3.9 + '@types/tar-stream': ~2.2.0 + '@typescript-eslint/eslint-plugin': 5.10.2 + '@typescript-eslint/parser': 5.10.2 + applicationinsights: 2.2.1 + chalk: 4.1.2 + cli-progress: 3.10.0 + ee-ts: 2.0.0-rc.6 + end-of-stream: ^1.4.1 + eslint: 8.8.0 + eslint-plugin-notice: 0.9.10 + fast-glob: 3.2.11 + fast-xml-parser: 4.0.3 + fs-constants: ^1.0.0 + got: 11.8.3 + inherits: ^2.0.3 + marked: 4.0.12 + marked-terminal: 5.1.1 + micromatch: 4.0.4 + mocha: '9.2' + sed-lite: 0.8.4 + semver: 7.3.5 + shx: 0.3.4 + sorted-btree: 1.6.0 + source-map-support: 0.5.21 + tape: ^4.9.2 + translate-strings: 1.0.11 + txtgen: 2.2.8 + typescript: 4.5.5 + unbzip2-stream: 1.4.3 + vscode-uri: 3.0.3 + yaml: 2.0.0-10 + +dependencies: + '@rush-temp/tar-stream': file:projects/tar-stream.tgz + '@rush-temp/vcpkg-ce': file:projects/vcpkg-ce.tgz + '@rush-temp/vcpkg-ce.test': file:projects/vcpkg-ce.test.tgz + '@snyk/nuget-semver': 1.3.0 + '@types/cli-progress': 3.9.2 + '@types/marked': 4.0.2 + '@types/marked-terminal': 3.1.3 + '@types/micromatch': 4.0.2 + '@types/mocha': 9.1.0 + '@types/node': 17.0.15 + '@types/semver': 7.3.9 + '@types/tar-stream': 2.2.2 + '@typescript-eslint/eslint-plugin': 5.10.2_2595c2126aec4d4b6e944b931dabb4c2 + '@typescript-eslint/parser': 5.10.2_eslint@8.8.0+typescript@4.5.5 + applicationinsights: 2.2.1 + chalk: 4.1.2 + cli-progress: 3.10.0 + ee-ts: 2.0.0-rc.6_typescript@4.5.5 + end-of-stream: 1.4.4 + eslint: 8.8.0 + eslint-plugin-notice: 0.9.10_eslint@8.8.0 + fast-glob: 3.2.11 + fast-xml-parser: 4.0.3 + fs-constants: 1.0.0 + got: 11.8.3 + inherits: 2.0.4 + marked: 4.0.12 + marked-terminal: 5.1.1_marked@4.0.12 + micromatch: 4.0.4 + mocha: 9.2.0 + sed-lite: 0.8.4 + semver: 7.3.5 + shx: 0.3.4 + sorted-btree: 1.6.0 + source-map-support: 0.5.21 + tape: 4.15.0 + translate-strings: 1.0.11 + txtgen: 2.2.8 + typescript: 4.5.5 + unbzip2-stream: 1.4.3 + vscode-uri: 3.0.3 + yaml: 2.0.0-10 + +packages: + + /@azure/abort-controller/1.0.4: + resolution: {integrity: sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==} + engines: {node: '>=8.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@azure/cognitiveservices-translatortext/1.0.1: + resolution: {integrity: sha512-ByOcyqLKFlpgHhkEw44dV9DecmI+r+FZIh72eFwGx8NZLr0erSkknkUJwS/reZgejG0nGevfqM6Sow8Zat29Xw==} + dependencies: + '@azure/ms-rest-js': 2.6.0 + tslib: 1.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /@azure/core-asynciterator-polyfill/1.0.0: + resolution: {integrity: sha512-kmv8CGrPfN9SwMwrkiBK9VTQYxdFQEGe0BmQk+M8io56P9KNzpAxcWE/1fxJj7uouwN4kXF0BHW8DNlgx+wtCg==} + dev: false + + /@azure/core-auth/1.3.2: + resolution: {integrity: sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA==} + engines: {node: '>=12.0.0'} + dependencies: + '@azure/abort-controller': 1.0.4 + tslib: 2.3.1 + dev: false + + /@azure/core-http/2.2.4: + resolution: {integrity: sha512-QmmJmexXKtPyc3/rsZR/YTLDvMatzbzAypJmLzvlfxgz/SkgnqV/D4f6F2LsK6tBj1qhyp8BoXiOebiej0zz3A==} + engines: {node: '>=12.0.0'} + dependencies: + '@azure/abort-controller': 1.0.4 + '@azure/core-asynciterator-polyfill': 1.0.0 + '@azure/core-auth': 1.3.2 + '@azure/core-tracing': 1.0.0-preview.13 + '@azure/logger': 1.0.3 + '@types/node-fetch': 2.5.12 + '@types/tunnel': 0.0.3 + form-data: 4.0.0 + node-fetch: 2.6.7 + process: 0.11.10 + tough-cookie: 4.0.0 + tslib: 2.3.1 + tunnel: 0.0.6 + uuid: 8.3.2 + xml2js: 0.4.23 + transitivePeerDependencies: + - encoding + dev: false + + /@azure/core-tracing/1.0.0-preview.13: + resolution: {integrity: sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==} + engines: {node: '>=12.0.0'} + dependencies: + '@opentelemetry/api': 1.0.4 + tslib: 2.3.1 + dev: false + + /@azure/logger/1.0.3: + resolution: {integrity: sha512-aK4s3Xxjrx3daZr3VylxejK3vG5ExXck5WOHDJ8in/k9AqlfIyFMMT1uG7u8mNjX+QRILTIn0/Xgschfh/dQ9g==} + engines: {node: '>=12.0.0'} + dependencies: + tslib: 2.3.1 + dev: false + + /@azure/ms-rest-azure-js/2.1.0: + resolution: {integrity: sha512-CjZjB8apvXl5h97Ck6SbeeCmU0sk56YPozPtTyGudPp1RGoHXNjFNtoOvwOG76EdpmMpxbK10DqcygI16Lu60Q==} + dependencies: + '@azure/core-auth': 1.3.2 + '@azure/ms-rest-js': 2.6.0 + tslib: 1.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /@azure/ms-rest-js/2.6.0: + resolution: {integrity: sha512-4C5FCtvEzWudblB+h92/TYYPiq7tuElX8icVYToxOdggnYqeec4Se14mjse5miInKtZahiFHdl8lZA/jziEc5g==} + dependencies: + '@azure/core-auth': 1.3.2 + abort-controller: 3.0.0 + form-data: 2.5.1 + node-fetch: 2.6.7 + tough-cookie: 3.0.1 + tslib: 1.14.1 + tunnel: 0.0.6 + uuid: 8.3.2 + xml2js: 0.4.23 + transitivePeerDependencies: + - encoding + dev: false + + /@dsherret/to-absolute-glob/2.0.2: + resolution: {integrity: sha1-H2R13IvZdM6gei2vOGSzF7HdMyw=} + engines: {node: '>=0.10.0'} + dependencies: + is-absolute: 1.0.0 + is-negated-glob: 1.0.0 + dev: false + + /@eslint/eslintrc/1.0.5: + resolution: {integrity: sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.3 + espree: 9.3.0 + globals: 13.12.1 + ignore: 4.0.6 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.0.4 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@humanwhocodes/config-array/0.9.3: + resolution: {integrity: sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.3 + minimatch: 3.0.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@humanwhocodes/object-schema/1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: false + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: false + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: false + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.13.0 + dev: false + + /@opentelemetry/api/1.0.4: + resolution: {integrity: sha512-BuJuXRSJNQ3QoKA6GWWDyuLpOUck+9hAXNMCnrloc1aWVoy6Xq6t9PUV08aBZ4Lutqq2LEHM486bpZqoViScog==} + engines: {node: '>=8.0.0'} + dev: false + + /@opentelemetry/core/1.0.1_@opentelemetry+api@1.0.4: + resolution: {integrity: sha512-90nQ2X6b/8X+xjcLDBYKooAcOsIlwLRYm+1VsxcX5cHl6V4CSVmDpBreQSDH/A21SqROzapk6813008SatmPpQ==} + engines: {node: '>=8.5.0'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.1.0' + dependencies: + '@opentelemetry/api': 1.0.4 + '@opentelemetry/semantic-conventions': 1.0.1 + dev: false + + /@opentelemetry/resources/1.0.1_@opentelemetry+api@1.0.4: + resolution: {integrity: sha512-p8DevOaAEepPucUtImR4cZKHOE2L1jgQAtkdZporV+XnxPA/HqCHPEESyUVuo4f5M0NUlL6k5Pba75KwNJlTRg==} + engines: {node: '>=8.0.0'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.1.0' + dependencies: + '@opentelemetry/api': 1.0.4 + '@opentelemetry/core': 1.0.1_@opentelemetry+api@1.0.4 + '@opentelemetry/semantic-conventions': 1.0.1 + dev: false + + /@opentelemetry/sdk-trace-base/1.0.1_@opentelemetry+api@1.0.4: + resolution: {integrity: sha512-JVSAepTpW7dnqfV7XFN0zHj1jXGNd5OcvIGQl76buogqffdgJdgJWQNrOuUJaus56zrOtlzqFH+YtMA9RGEg8w==} + engines: {node: '>=8.0.0'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.1.0' + dependencies: + '@opentelemetry/api': 1.0.4 + '@opentelemetry/core': 1.0.1_@opentelemetry+api@1.0.4 + '@opentelemetry/resources': 1.0.1_@opentelemetry+api@1.0.4 + '@opentelemetry/semantic-conventions': 1.0.1 + dev: false + + /@opentelemetry/semantic-conventions/1.0.1: + resolution: {integrity: sha512-7XU1sfQ8uCVcXLxtAHA8r3qaLJ2oq7sKtEwzZhzuEXqYmjW+n+J4yM3kNo0HQo3Xp1eUe47UM6Wy6yuAvIyllg==} + engines: {node: '>=8.0.0'} + dev: false + + /@sindresorhus/is/4.4.0: + resolution: {integrity: sha512-QppPM/8l3Mawvh4rn9CNEYIU9bxpXUCRMaX9yUpvBk1nMKusLKpfXGDEKExKaPhLzcn3lzil7pR6rnJ11HgeRQ==} + engines: {node: '>=10'} + dev: false + + /@snyk/nuget-semver/1.3.0: + resolution: {integrity: sha512-1CL4BzQKFPwml+BBefKuM0v9UfsFOgSKzrTfYpUkiSNkUVsMxXK37LlT3HtG7zGpMxXiG+XXfPopo/96Z0wfNg==} + engines: {node: '>=6'} + dev: false + + /@szmarczak/http-timer/4.0.6: + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + + /@ts-morph/common/0.7.5: + resolution: {integrity: sha512-nlFunSKAsFWI0Ol/uPxJcpVqXxTGNuaWXTmoQDhcnwj1UM4QmBSUVWzqoQ0OzUlqo4sV1gobfFBkMHuZVemMAQ==} + dependencies: + '@dsherret/to-absolute-glob': 2.0.2 + fast-glob: 3.2.11 + is-negated-glob: 1.0.0 + mkdirp: 1.0.4 + multimatch: 5.0.0 + typescript: 4.1.6 + dev: false + + /@types/braces/3.0.1: + resolution: {integrity: sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==} + dev: false + + /@types/cacheable-request/6.0.2: + resolution: {integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==} + dependencies: + '@types/http-cache-semantics': 4.0.1 + '@types/keyv': 3.1.3 + '@types/node': 17.0.15 + '@types/responselike': 1.0.0 + dev: false + + /@types/cli-progress/3.9.2: + resolution: {integrity: sha512-VO5/X5Ij+oVgEVjg5u0IXVe3JQSKJX+Ev8C5x+0hPy0AuWyW+bF8tbajR7cPFnDGhs7pidztcac+ccrDtk5teA==} + dependencies: + '@types/node': 17.0.15 + dev: false + + /@types/http-cache-semantics/4.0.1: + resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} + dev: false + + /@types/json-schema/7.0.9: + resolution: {integrity: sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==} + dev: false + + /@types/keyv/3.1.3: + resolution: {integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg==} + dependencies: + '@types/node': 17.0.15 + dev: false + + /@types/marked-terminal/3.1.3: + resolution: {integrity: sha512-dKgOLKlI5zFb2jTbRcyQqbdrHxeU74DCOkVIZtsoB2sc1ctXZ1iB2uxG2jjAuzoLdvwHP065ijN6Q8HecWdWYg==} + dependencies: + '@types/marked': 3.0.3 + chalk: 2.4.2 + dev: false + + /@types/marked/3.0.3: + resolution: {integrity: sha512-ZgAr847Wl68W+B0sWH7F4fDPxTzerLnRuUXjUpp1n4NjGSs8hgPAjAp7NQIXblG34MXTrf5wWkAK8PVJ2LIlVg==} + dev: false + + /@types/marked/4.0.2: + resolution: {integrity: sha512-auNrZ/c0w6wsM9DccwVxWHssrMDezHUAXNesdp2RQrCVCyrQbOiSq7yqdJKrUQQpw9VTm7CGYJH2A/YG7jjrjQ==} + dev: false + + /@types/micromatch/4.0.2: + resolution: {integrity: sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA==} + dependencies: + '@types/braces': 3.0.1 + dev: false + + /@types/minimatch/3.0.5: + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + dev: false + + /@types/mocha/9.1.0: + resolution: {integrity: sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==} + dev: false + + /@types/node-fetch/2.5.12: + resolution: {integrity: sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==} + dependencies: + '@types/node': 17.0.15 + form-data: 3.0.1 + dev: false + + /@types/node/14.14.22: + resolution: {integrity: sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw==} + dev: false + + /@types/node/17.0.15: + resolution: {integrity: sha512-zWt4SDDv1S9WRBNxLFxFRHxdD9tvH8f5/kg5/IaLFdnSNXsDY4eL3Q3XXN+VxUnWIhyVFDwcsmAprvwXoM/ClA==} + dev: false + + /@types/responselike/1.0.0: + resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} + dependencies: + '@types/node': 17.0.15 + dev: false + + /@types/semver/7.3.9: + resolution: {integrity: sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ==} + dev: false + + /@types/tar-stream/2.2.2: + resolution: {integrity: sha512-1AX+Yt3icFuU6kxwmPakaiGrJUwG44MpuiqPg4dSolRFk6jmvs4b3IbUol9wKDLIgU76gevn3EwE8y/DkSJCZQ==} + dependencies: + '@types/node': 14.14.22 + dev: false + + /@types/tunnel/0.0.3: + resolution: {integrity: sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==} + dependencies: + '@types/node': 17.0.15 + dev: false + + /@typescript-eslint/eslint-plugin/5.10.2_2595c2126aec4d4b6e944b931dabb4c2: + resolution: {integrity: sha512-4W/9lLuE+v27O/oe7hXJKjNtBLnZE8tQAFpapdxwSVHqtmIoPB1gph3+ahNwVuNL37BX7YQHyGF9Xv6XCnIX2Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.10.2_eslint@8.8.0+typescript@4.5.5 + '@typescript-eslint/scope-manager': 5.10.2 + '@typescript-eslint/type-utils': 5.10.2_eslint@8.8.0+typescript@4.5.5 + '@typescript-eslint/utils': 5.10.2_eslint@8.8.0+typescript@4.5.5 + debug: 4.3.3 + eslint: 8.8.0 + functional-red-black-tree: 1.0.1 + ignore: 5.2.0 + regexpp: 3.2.0 + semver: 7.3.5 + tsutils: 3.21.0_typescript@4.5.5 + typescript: 4.5.5 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/parser/5.10.2_eslint@8.8.0+typescript@4.5.5: + resolution: {integrity: sha512-JaNYGkaQVhP6HNF+lkdOr2cAs2wdSZBoalE22uYWq8IEv/OVH0RksSGydk+sW8cLoSeYmC+OHvRyv2i4AQ7Czg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.10.2 + '@typescript-eslint/types': 5.10.2 + '@typescript-eslint/typescript-estree': 5.10.2_typescript@4.5.5 + debug: 4.3.3 + eslint: 8.8.0 + typescript: 4.5.5 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/scope-manager/5.10.2: + resolution: {integrity: sha512-39Tm6f4RoZoVUWBYr3ekS75TYgpr5Y+X0xLZxXqcZNDWZdJdYbKd3q2IR4V9y5NxxiPu/jxJ8XP7EgHiEQtFnw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.10.2 + '@typescript-eslint/visitor-keys': 5.10.2 + dev: false + + /@typescript-eslint/type-utils/5.10.2_eslint@8.8.0+typescript@4.5.5: + resolution: {integrity: sha512-uRKSvw/Ccs5FYEoXW04Z5VfzF2iiZcx8Fu7DGIB7RHozuP0VbKNzP1KfZkHBTM75pCpsWxIthEH1B33dmGBKHw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/utils': 5.10.2_eslint@8.8.0+typescript@4.5.5 + debug: 4.3.3 + eslint: 8.8.0 + tsutils: 3.21.0_typescript@4.5.5 + typescript: 4.5.5 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/types/5.10.2: + resolution: {integrity: sha512-Qfp0qk/5j2Rz3p3/WhWgu4S1JtMcPgFLnmAKAW061uXxKSa7VWKZsDXVaMXh2N60CX9h6YLaBoy9PJAfCOjk3w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /@typescript-eslint/typescript-estree/5.10.2_typescript@4.5.5: + resolution: {integrity: sha512-WHHw6a9vvZls6JkTgGljwCsMkv8wu8XU8WaYKeYhxhWXH/atZeiMW6uDFPLZOvzNOGmuSMvHtZKd6AuC8PrwKQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.10.2 + '@typescript-eslint/visitor-keys': 5.10.2 + debug: 4.3.3 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.3.5 + tsutils: 3.21.0_typescript@4.5.5 + typescript: 4.5.5 + transitivePeerDependencies: + - supports-color + dev: false + + /@typescript-eslint/utils/5.10.2_eslint@8.8.0+typescript@4.5.5: + resolution: {integrity: sha512-vuJaBeig1NnBRkf7q9tgMLREiYD7zsMrsN1DA3wcoMDvr3BTFiIpKjGiYZoKPllfEwN7spUjv7ZqD+JhbVjEPg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.9 + '@typescript-eslint/scope-manager': 5.10.2 + '@typescript-eslint/types': 5.10.2 + '@typescript-eslint/typescript-estree': 5.10.2_typescript@4.5.5 + eslint: 8.8.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0_eslint@8.8.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: false + + /@typescript-eslint/visitor-keys/5.10.2: + resolution: {integrity: sha512-zHIhYGGGrFJvvyfwHk5M08C5B5K4bewkm+rrvNTKk1/S15YHR+SA/QUF8ZWscXSfEaB8Nn2puZj+iHcoxVOD/Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.10.2 + eslint-visitor-keys: 3.2.0 + dev: false + + /@ungap/promise-all-settled/1.1.2: + resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} + dev: false + + /abort-controller/3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + + /acorn-jsx/5.3.2_acorn@8.7.0: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.7.0 + dev: false + + /acorn/8.7.0: + resolution: {integrity: sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: false + + /ajv/6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: false + + /ansi-colors/4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: false + + /ansi-escapes/5.0.0: + resolution: {integrity: sha512-5GFMVX8HqE/TB+FuBJGuO5XG0WrsA6ptUqoODaT/n9mmUaZFkqnBueB4leqGBCmrUHnCnC4PCZTCd0E7QQ83bA==} + engines: {node: '>=12'} + dependencies: + type-fest: 1.4.0 + dev: false + + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /ansi-styles/3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: false + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: false + + /ansicolors/0.3.2: + resolution: {integrity: sha1-ZlWX3oap/+Oqm/vmyuXG6kJrSXk=} + dev: false + + /anymatch/3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: false + + /applicationinsights/2.2.1: + resolution: {integrity: sha512-N6panMyjw6E6ayCgjFDBmL/NkaolgBgeX1iJ0jh50E6wrncVJBCa+I4IelwwOfJ4Dl9BWzOSLjp84wTiUyhNwg==} + engines: {node: '>=8.0.0'} + peerDependencies: + applicationinsights-native-metrics: '*' + peerDependenciesMeta: + applicationinsights-native-metrics: + optional: true + dependencies: + '@azure/core-http': 2.2.4 + '@opentelemetry/api': 1.0.4 + '@opentelemetry/core': 1.0.1_@opentelemetry+api@1.0.4 + '@opentelemetry/sdk-trace-base': 1.0.1_@opentelemetry+api@1.0.4 + '@opentelemetry/semantic-conventions': 1.0.1 + cls-hooked: 4.2.2 + continuation-local-storage: 3.2.1 + diagnostic-channel: 1.1.0 + diagnostic-channel-publishers: 1.0.4_diagnostic-channel@1.1.0 + transitivePeerDependencies: + - encoding + dev: false + + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: false + + /array-differ/3.0.0: + resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} + engines: {node: '>=8'} + dev: false + + /array-union/2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: false + + /arrify/2.0.1: + resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} + engines: {node: '>=8'} + dev: false + + /async-hook-jl/1.7.6: + resolution: {integrity: sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==} + engines: {node: ^4.7 || >=6.9 || >=7.3} + dependencies: + stack-chain: 1.3.7 + dev: false + + /async-listener/0.6.10: + resolution: {integrity: sha512-gpuo6xOyF4D5DE5WvyqZdPA3NGhiT6Qf07l7DCB0wwDEsLvDIbCr6j9S5aj5Ch96dLace5tXVzWBZkxU/c5ohw==} + engines: {node: <=0.11.8 || >0.11.10} + dependencies: + semver: 5.7.1 + shimmer: 1.2.1 + dev: false + + /asynckit/0.4.0: + resolution: {integrity: sha1-x57Zf380y48robyXkLzDZkdLS3k=} + dev: false + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: false + + /base64-js/1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: false + + /bl/4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: false + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: false + + /browser-stdout/1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: false + + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false + + /buffer/5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /cacheable-lookup/5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + dev: false + + /cacheable-request/7.0.2: + resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.2 + get-stream: 5.2.0 + http-cache-semantics: 4.1.0 + keyv: 4.1.0 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.0 + dev: false + + /call-bind/1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.1.1 + dev: false + + /callsites/3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: false + + /camelcase/6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: false + + /cardinal/2.1.1: + resolution: {integrity: sha1-fMEFXYItISlU0HsIXeolHMe8VQU=} + hasBin: true + dependencies: + ansicolors: 0.3.2 + redeyed: 2.1.1 + dev: false + + /chalk/2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: false + + /chalk/4.1.0: + resolution: {integrity: sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + + /chalk/5.0.0: + resolution: {integrity: sha512-/duVOqst+luxCQRKEo4bNxinsOQtMP80ZYm7mMqzuh5PociNL0PvmHFvREJ9ueYL2TxlHjBcmLCdmocx9Vg+IQ==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: false + + /chownr/2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + + /cli-progress/3.10.0: + resolution: {integrity: sha512-kLORQrhYCAtUPLZxqsAt2YJGOvRdt34+O6jl5cQGb7iF3dM55FQZlTR+rQyIK9JUcO9bBMwZsTlND+3dmFU2Cw==} + engines: {node: '>=4'} + dependencies: + string-width: 4.2.3 + dev: false + + /cli-table3/0.6.1: + resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} + engines: {node: 10.* || >= 12.*} + dependencies: + string-width: 4.2.3 + optionalDependencies: + colors: 1.4.0 + dev: false + + /cliui/7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + + /clone-response/1.0.2: + resolution: {integrity: sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=} + dependencies: + mimic-response: 1.0.1 + dev: false + + /cls-hooked/4.2.2: + resolution: {integrity: sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==} + engines: {node: ^4.7 || >=6.9 || >=7.3 || >=8.2.1} + dependencies: + async-hook-jl: 1.7.6 + emitter-listener: 1.1.2 + semver: 5.7.1 + dev: false + + /code-block-writer/10.1.1: + resolution: {integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==} + dev: false + + /color-convert/1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: false + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: false + + /color-name/1.1.3: + resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} + dev: false + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: false + + /colors/1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + requiresBuild: true + dev: false + optional: true + + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /concat-map/0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + dev: false + + /continuation-local-storage/3.2.1: + resolution: {integrity: sha512-jx44cconVqkCEEyLSKWwkvUXwO561jXMa3LPjTPsm5QR22PA0/mhe33FT4Xb5y74JDvt/Cq+5lm8S8rskLv9ZA==} + dependencies: + async-listener: 0.6.10 + emitter-listener: 1.1.2 + dev: false + + /cross-spawn/7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: false + + /debug/4.3.3: + resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /debug/4.3.3_supports-color@8.1.1: + resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: false + + /decamelize/4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: false + + /decompress-response/6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + + /deep-equal/1.1.1: + resolution: {integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==} + dependencies: + is-arguments: 1.1.1 + is-date-object: 1.0.5 + is-regex: 1.1.4 + object-is: 1.1.5 + object-keys: 1.1.1 + regexp.prototype.flags: 1.4.1 + dev: false + + /deep-is/0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: false + + /defer-to-connect/2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: false + + /define-properties/1.1.3: + resolution: {integrity: sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==} + engines: {node: '>= 0.4'} + dependencies: + object-keys: 1.1.1 + dev: false + + /defined/1.0.0: + resolution: {integrity: sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=} + dev: false + + /delayed-stream/1.0.0: + resolution: {integrity: sha1-3zrhmayt+31ECqrgsp4icrJOxhk=} + engines: {node: '>=0.4.0'} + dev: false + + /diagnostic-channel-publishers/1.0.4_diagnostic-channel@1.1.0: + resolution: {integrity: sha512-GDRAOrcNTPk4DhYzM2BauMnq7nKdFWmSFjWnEu8dT8Xf/ZXUbpORrqNAhIWsy2tqRjHG7QkmYjMUL4/EGSM2GA==} + peerDependencies: + diagnostic-channel: '*' + dependencies: + diagnostic-channel: 1.1.0 + dev: false + + /diagnostic-channel/1.1.0: + resolution: {integrity: sha512-fwujyMe1gj6rk6dYi9hMZm0c8Mz8NDMVl2LB4iaYh3+LIAThZC8RKFGXWG0IML2OxAit/ZFRgZhMkhQ3d/bobQ==} + dependencies: + semver: 5.7.1 + dev: false + + /diff/5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: false + + /dir-glob/3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: false + + /doctrine/3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: false + + /dotignore/0.1.2: + resolution: {integrity: sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==} + hasBin: true + dependencies: + minimatch: 3.0.4 + dev: false + + /ee-ts/2.0.0-rc.6_typescript@4.5.5: + resolution: {integrity: sha512-n52lYxRqYqJhw5b7iol1NHHaz/hShlwKby2bhMM+/QePMe3rpV8F5lL96j678uOHtYirEHUoYP9GhKKhQVk+hQ==} + engines: {node: '>=6'} + peerDependencies: + typescript: '>=3' + dependencies: + typescript: 4.5.5 + dev: false + + /emitter-listener/1.1.2: + resolution: {integrity: sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==} + dependencies: + shimmer: 1.2.1 + dev: false + + /emoji-regex/8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /end-of-stream/1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + + /es-abstract/1.19.1: + resolution: {integrity: sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + es-to-primitive: 1.2.1 + function-bind: 1.1.1 + get-intrinsic: 1.1.1 + get-symbol-description: 1.0.0 + has: 1.0.3 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + is-callable: 1.2.4 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.1 + is-string: 1.0.7 + is-weakref: 1.0.2 + object-inspect: 1.12.0 + object-keys: 1.1.1 + object.assign: 4.1.2 + string.prototype.trimend: 1.0.4 + string.prototype.trimstart: 1.0.4 + unbox-primitive: 1.0.1 + dev: false + + /es-to-primitive/1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.4 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: false + + /escalade/3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: false + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=} + engines: {node: '>=0.8.0'} + dev: false + + /escape-string-regexp/4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: false + + /eslint-plugin-notice/0.9.10_eslint@8.8.0: + resolution: {integrity: sha512-rF79EuqdJKu9hhTmwUkNeSvLmmq03m/NXq/NHwUENHbdJ0wtoyOjxZBhW4QCug8v5xYE6cGe3AWkGqSIe9KUbQ==} + peerDependencies: + eslint: '>=3.0.0' + dependencies: + eslint: 8.8.0 + find-root: 1.1.0 + lodash: 4.17.21 + metric-lcs: 0.1.2 + dev: false + + /eslint-scope/5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: false + + /eslint-scope/7.1.0: + resolution: {integrity: sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: false + + /eslint-utils/3.0.0_eslint@8.8.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.8.0 + eslint-visitor-keys: 2.1.0 + dev: false + + /eslint-visitor-keys/2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: false + + /eslint-visitor-keys/3.2.0: + resolution: {integrity: sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: false + + /eslint/8.8.0: + resolution: {integrity: sha512-H3KXAzQGBH1plhYS3okDix2ZthuYJlQQEGE5k0IKuEqUSiyu4AmxxlJ2MtTYeJ3xB4jDhcYCwGOg2TXYdnDXlQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint/eslintrc': 1.0.5 + '@humanwhocodes/config-array': 0.9.3 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.3 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.1.0 + eslint-utils: 3.0.0_eslint@8.8.0 + eslint-visitor-keys: 3.2.0 + espree: 9.3.0 + esquery: 1.4.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + functional-red-black-tree: 1.0.1 + glob-parent: 6.0.2 + globals: 13.12.1 + ignore: 5.2.0 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.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.0.4 + natural-compare: 1.4.0 + optionator: 0.9.1 + regexpp: 3.2.0 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + v8-compile-cache: 2.3.0 + transitivePeerDependencies: + - supports-color + dev: false + + /espree/9.3.0: + resolution: {integrity: sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.7.0 + acorn-jsx: 5.3.2_acorn@8.7.0 + eslint-visitor-keys: 3.2.0 + dev: false + + /esprima/4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /esquery/1.4.0: + resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: false + + /esrecurse/4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: false + + /estraverse/4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: false + + /estraverse/5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: false + + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: false + + /event-target-shim/5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: false + + /fast-glob/3.2.11: + resolution: {integrity: sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.4 + dev: false + + /fast-json-stable-stringify/2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: false + + /fast-levenshtein/2.0.6: + resolution: {integrity: sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=} + dev: false + + /fast-xml-parser/4.0.3: + resolution: {integrity: sha512-xhQbg3a/EYNHwK0cxIG1nZmVkHX/0tWihamn5pU4Mhd9KEVE2ga8ZJiqEUgB2sApElvAATOdMTLjgqIpvYDUkQ==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + + /fastq/1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + dependencies: + reusify: 1.0.4 + dev: false + + /file-entry-cache/6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: false + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: false + + /find-root/1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + dev: false + + /find-up/5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: false + + /flat-cache/3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.5 + rimraf: 3.0.2 + dev: false + + /flat/5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: false + + /flatted/3.2.5: + resolution: {integrity: sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==} + dev: false + + /for-each/0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.4 + dev: false + + /form-data/2.5.1: + resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.34 + dev: false + + /form-data/3.0.1: + resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.34 + dev: false + + /form-data/4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.34 + dev: false + + /fs-constants/1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + dev: false + + /fs-minipass/2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.1.6 + dev: false + + /fs.realpath/1.0.0: + resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} + dev: false + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: false + + /functional-red-black-tree/1.0.1: + resolution: {integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=} + dev: false + + /get-caller-file/2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false + + /get-intrinsic/1.1.1: + resolution: {integrity: sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + dev: false + + /get-stream/5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: false + + /get-symbol-description/1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.1 + dev: false + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: false + + /glob-parent/6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: false + + /glob/7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.0.4 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: false + + /globals/13.12.1: + resolution: {integrity: sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: false + + /globby/11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.11 + ignore: 5.2.0 + merge2: 1.4.1 + slash: 3.0.0 + dev: false + + /got/11.8.3: + resolution: {integrity: sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==} + engines: {node: '>=10.19.0'} + dependencies: + '@sindresorhus/is': 4.4.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.2 + '@types/responselike': 1.0.0 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.2 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.0 + dev: false + + /growl/1.10.5: + resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} + engines: {node: '>=4.x'} + dev: false + + /has-bigints/1.0.1: + resolution: {integrity: sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==} + dev: false + + /has-flag/3.0.0: + resolution: {integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0=} + engines: {node: '>=4'} + dev: false + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: false + + /has-symbols/1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-tostringtag/1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: false + + /he/1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + + /http-cache-semantics/4.1.0: + resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} + dev: false + + /http2-wrapper/1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: false + + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /ignore/4.0.6: + resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + engines: {node: '>= 4'} + dev: false + + /ignore/5.2.0: + resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} + engines: {node: '>= 4'} + dev: false + + /import-fresh/3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: false + + /imurmurhash/0.1.4: + resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=} + engines: {node: '>=0.8.19'} + dev: false + + /inflight/1.0.6: + resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: false + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /internal-slot/1.0.3: + resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.1.1 + has: 1.0.3 + side-channel: 1.0.4 + dev: false + + /interpret/1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: false + + /ip-regex/2.1.0: + resolution: {integrity: sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=} + engines: {node: '>=4'} + dev: false + + /is-absolute/1.0.0: + resolution: {integrity: sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==} + engines: {node: '>=0.10.0'} + dependencies: + is-relative: 1.0.0 + is-windows: 1.0.2 + dev: false + + /is-arguments/1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-bigint/1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.1 + dev: false + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: false + + /is-boolean-object/1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-callable/1.2.4: + resolution: {integrity: sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==} + engines: {node: '>= 0.4'} + dev: false + + /is-core-module/2.8.1: + resolution: {integrity: sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==} + dependencies: + has: 1.0.3 + dev: false + + /is-date-object/1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-extglob/2.1.1: + resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} + engines: {node: '>=0.10.0'} + dev: false + + /is-fullwidth-code-point/3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: false + + /is-negated-glob/1.0.0: + resolution: {integrity: sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=} + engines: {node: '>=0.10.0'} + dev: false + + /is-negative-zero/2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: false + + /is-number-object/1.0.6: + resolution: {integrity: sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: false + + /is-plain-obj/2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: false + + /is-regex/1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: false + + /is-relative/1.0.0: + resolution: {integrity: sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==} + engines: {node: '>=0.10.0'} + dependencies: + is-unc-path: 1.0.0 + dev: false + + /is-shared-array-buffer/1.0.1: + resolution: {integrity: sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==} + dev: false + + /is-string/1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: false + + /is-symbol/1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: false + + /is-unc-path/1.0.0: + resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==} + engines: {node: '>=0.10.0'} + dependencies: + unc-path-regex: 0.1.2 + dev: false + + /is-unicode-supported/0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: false + + /is-weakref/1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: false + + /is-windows/1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + dev: false + + /isexe/2.0.0: + resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} + dev: false + + /js-yaml/4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: false + + /json-buffer/3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: false + + /json-schema-traverse/0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: false + + /json-stable-stringify-without-jsonify/1.0.1: + resolution: {integrity: sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=} + dev: false + + /keyv/4.1.0: + resolution: {integrity: sha512-YsY3wr6HabE11/sscee+3nZ03XjvkrPWGouAmJFBdZoK92wiOlJCzI5/sDEIKdJhdhHO144ei45U9gXfbu14Uw==} + dependencies: + json-buffer: 3.0.1 + dev: false + + /levn/0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: false + + /locate-path/6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: false + + /lodash.merge/4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: false + + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /log-symbols/4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: false + + /lowercase-keys/2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: false + + /lru-cache/6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: false + + /marked-terminal/5.1.1_marked@4.0.12: + resolution: {integrity: sha512-+cKTOx9P4l7HwINYhzbrBSyzgxO2HaHKGZGuB1orZsMIgXYaJyfidT81VXRdpelW/PcHEWxywscePVgI/oUF6g==} + engines: {node: '>=14.13.1 || >=16.0.0'} + peerDependencies: + marked: ^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 + dependencies: + ansi-escapes: 5.0.0 + cardinal: 2.1.1 + chalk: 5.0.0 + cli-table3: 0.6.1 + marked: 4.0.12 + node-emoji: 1.11.0 + supports-hyperlinks: 2.2.0 + dev: false + + /marked/4.0.12: + resolution: {integrity: sha512-hgibXWrEDNBWgGiK18j/4lkS6ihTe9sxtV4Q1OQppb/0zzyPSzoFANBa5MfsG/zgsWklmNnhm0XACZOH/0HBiQ==} + engines: {node: '>= 12'} + hasBin: true + dev: false + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: false + + /metric-lcs/0.1.2: + resolution: {integrity: sha512-+TZ5dUDPKPJaU/rscTzxyN8ZkX7eAVLAiQU/e+YINleXPv03SCmJShaMT1If1liTH8OcmWXZs0CmzCBRBLcMpA==} + dev: false + + /micromatch/4.0.4: + resolution: {integrity: sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: false + + /mime-db/1.51.0: + resolution: {integrity: sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types/2.1.34: + resolution: {integrity: sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.51.0 + dev: false + + /mimic-response/1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: false + + /mimic-response/3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + + /minimatch/3.0.4: + resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} + dependencies: + brace-expansion: 1.1.11 + dev: false + + /minimist/1.2.5: + resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==} + dev: false + + /minipass/3.1.6: + resolution: {integrity: sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minizlib/2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.1.6 + yallist: 4.0.0 + dev: false + + /mkdirp/1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: false + + /mocha/9.2.0: + resolution: {integrity: sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==} + engines: {node: '>= 12.0.0'} + hasBin: true + dependencies: + '@ungap/promise-all-settled': 1.1.2 + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.3_supports-color@8.1.1 + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + growl: 1.10.5 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 3.0.4 + ms: 2.1.3 + nanoid: 3.2.0 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + which: 2.0.2 + workerpool: 6.2.0 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + dev: false + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + /ms/2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /multimatch/5.0.0: + resolution: {integrity: sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==} + engines: {node: '>=10'} + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 3.0.0 + array-union: 2.1.0 + arrify: 2.0.1 + minimatch: 3.0.4 + dev: false + + /nanoid/3.2.0: + resolution: {integrity: sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false + + /natural-compare/1.4.0: + resolution: {integrity: sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=} + dev: false + + /node-emoji/1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + dependencies: + lodash: 4.17.21 + dev: false + + /node-fetch/2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: false + + /normalize-url/6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: false + + /object-inspect/1.12.0: + resolution: {integrity: sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==} + dev: false + + /object-is/1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.3 + dev: false + + /object-keys/1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: false + + /object.assign/4.1.2: + resolution: {integrity: sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.3 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: false + + /once/1.4.0: + resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=} + dependencies: + wrappy: 1.0.2 + dev: false + + /optionator/0.9.1: + resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.3 + dev: false + + /p-cancelable/2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + dev: false + + /p-limit/3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: false + + /p-locate/5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: false + + /parent-module/1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: false + + /path-exists/4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: false + + /path-is-absolute/1.0.1: + resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=} + engines: {node: '>=0.10.0'} + dev: false + + /path-key/3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: false + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: false + + /path-type/4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: false + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: false + + /prelude-ls/1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: false + + /process/0.11.10: + resolution: {integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=} + engines: {node: '>= 0.6.0'} + dev: false + + /psl/1.8.0: + resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} + dev: false + + /pump/3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + + /punycode/2.1.1: + resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + engines: {node: '>=6'} + dev: false + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: false + + /quick-lru/5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: false + + /randombytes/2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /readable-stream/3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: false + + /rechoir/0.6.2: + resolution: {integrity: sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.0 + dev: false + + /redeyed/2.1.1: + resolution: {integrity: sha1-iYS1gV2ZyyIEacme7v/jiRPmzAs=} + dependencies: + esprima: 4.0.1 + dev: false + + /regexp.prototype.flags/1.4.1: + resolution: {integrity: sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.3 + dev: false + + /regexpp/3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: false + + /require-directory/2.1.1: + resolution: {integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I=} + engines: {node: '>=0.10.0'} + dev: false + + /resolve-alpn/1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: false + + /resolve-from/4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: false + + /resolve/1.22.0: + resolution: {integrity: sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==} + hasBin: true + dependencies: + is-core-module: 2.8.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: false + + /responselike/2.0.0: + resolution: {integrity: sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==} + dependencies: + lowercase-keys: 2.0.0 + dev: false + + /resumer/0.0.0: + resolution: {integrity: sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=} + dependencies: + through: 2.3.8 + dev: false + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: false + + /rimraf/3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.0 + dev: false + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: false + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /sax/1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + dev: false + + /sed-lite/0.8.4: + resolution: {integrity: sha512-s6qpifBdif5TO5ilHQOzz7dbr14Mc4EjTlnYS8QVyy3sPruNXYj83LqKJlnDLkC8qiO/H2lL0dd/GOrHpeleDg==} + dev: false + + /semver/5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: false + + /semver/7.3.5: + resolution: {integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + + /serialize-javascript/6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + dependencies: + randombytes: 2.1.0 + dev: false + + /shebang-command/2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: false + + /shebang-regex/3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: false + + /shelljs/0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.0 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: false + + /shimmer/1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + dev: false + + /shx/0.3.4: + resolution: {integrity: sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==} + engines: {node: '>=6'} + hasBin: true + dependencies: + minimist: 1.2.5 + shelljs: 0.8.5 + dev: false + + /side-channel/1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.1 + object-inspect: 1.12.0 + dev: false + + /slash/3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: false + + /sorted-btree/1.6.0: + resolution: {integrity: sha512-1GB1zaxtugp75RYiVN2YunIiKBjKh8W0mod5g16MgWRBSC8bv+sVl0KOCDTR/pdEghJq+zJlO/28Kk4IgI1RLA==} + dev: false + + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: false + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false + + /stack-chain/1.3.7: + resolution: {integrity: sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=} + dev: false + + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /string.prototype.trim/1.2.5: + resolution: {integrity: sha512-Lnh17webJVsD6ECeovpVN17RlAKjmz4rF9S+8Y45CkMc/ufVpTkU3vZIyIC7sllQ1FCvObZnnCdNs/HXTUOTlg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.3 + es-abstract: 1.19.1 + dev: false + + /string.prototype.trimend/1.0.4: + resolution: {integrity: sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.3 + dev: false + + /string.prototype.trimstart/1.0.4: + resolution: {integrity: sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.3 + dev: false + + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: false + + /strnum/1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: false + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: false + + /supports-color/8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: false + + /supports-hyperlinks/2.2.0: + resolution: {integrity: sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + supports-color: 7.2.0 + dev: false + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: false + + /tape/4.15.0: + resolution: {integrity: sha512-SfRmG2I8QGGgJE/MCiLH8c11L5XxyUXxwK9xLRD0uiK5fehRkkSZGmR6Y1pxOt8vJ19m3sY+POTQpiaVv45/LQ==} + hasBin: true + dependencies: + call-bind: 1.0.2 + deep-equal: 1.1.1 + defined: 1.0.0 + dotignore: 0.1.2 + for-each: 0.3.3 + glob: 7.2.0 + has: 1.0.3 + inherits: 2.0.4 + is-regex: 1.1.4 + minimist: 1.2.5 + object-inspect: 1.12.0 + resolve: 1.22.0 + resumer: 0.0.0 + string.prototype.trim: 1.2.5 + through: 2.3.8 + dev: false + + /tar-stream/2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + + /tar/6.1.11: + resolution: {integrity: sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==} + engines: {node: '>= 10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 3.1.6 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + + /text-table/0.2.0: + resolution: {integrity: sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=} + dev: false + + /through/2.3.8: + resolution: {integrity: sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=} + dev: false + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: false + + /tough-cookie/3.0.1: + resolution: {integrity: sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==} + engines: {node: '>=6'} + dependencies: + ip-regex: 2.1.0 + psl: 1.8.0 + punycode: 2.1.1 + dev: false + + /tough-cookie/4.0.0: + resolution: {integrity: sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==} + engines: {node: '>=6'} + dependencies: + psl: 1.8.0 + punycode: 2.1.1 + universalify: 0.1.2 + dev: false + + /tr46/0.0.3: + resolution: {integrity: sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=} + dev: false + + /translate-strings/1.0.11: + resolution: {integrity: sha512-nMEc4UNwdNCjW+Rn3U0wTW91sUy1oYTF9PdKA3Y982PwqpiLdOD0RdKhS2UWxZzOOZTjGwauGM8RRdkqI8MYqw==} + engines: {node: '>=10.12.0'} + hasBin: true + dependencies: + '@azure/cognitiveservices-translatortext': 1.0.1 + '@azure/ms-rest-azure-js': 2.1.0 + chalk: 4.1.0 + ts-morph: 9.1.0 + transitivePeerDependencies: + - encoding + dev: false + + /ts-morph/9.1.0: + resolution: {integrity: sha512-sei4u651MBenr27sD6qLDXN3gZ4thiX71E3qV7SuVtDas0uvK2LtgZkIYUf9DKm/fLJ6AB/+yhRJ1vpEBJgy7Q==} + dependencies: + '@dsherret/to-absolute-glob': 2.0.2 + '@ts-morph/common': 0.7.5 + code-block-writer: 10.1.1 + dev: false + + /tslib/1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: false + + /tslib/2.3.1: + resolution: {integrity: sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==} + dev: false + + /tsutils/3.21.0_typescript@4.5.5: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + 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' + dependencies: + tslib: 1.14.1 + typescript: 4.5.5 + dev: false + + /tunnel/0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + dev: false + + /txtgen/2.2.8: + resolution: {integrity: sha512-b3wteOTrO1RPV1f6dhYaNz2G48a6dCH+pvTBRMdtjTWSeVBBE4rFypoc+2uT0F7OFSgBlOQefDuDvp9VYqA73g==} + engines: {node: '>= 10.14.2'} + dev: false + + /type-check/0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: false + + /type-fest/0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: false + + /type-fest/1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + dev: false + + /typescript/4.1.6: + resolution: {integrity: sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + + /typescript/4.5.5: + resolution: {integrity: sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + + /unbox-primitive/1.0.1: + resolution: {integrity: sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==} + dependencies: + function-bind: 1.1.1 + has-bigints: 1.0.1 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: false + + /unbzip2-stream/1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + dependencies: + buffer: 5.7.1 + through: 2.3.8 + dev: false + + /unc-path-regex/0.1.2: + resolution: {integrity: sha1-5z3T17DXxe2G+6xrCufYxqadUPo=} + engines: {node: '>=0.10.0'} + dev: false + + /universalify/0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: false + + /uri-js/4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.1.1 + dev: false + + /util-deprecate/1.0.2: + resolution: {integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=} + dev: false + + /uuid/8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + + /v8-compile-cache/2.3.0: + resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} + dev: false + + /vscode-uri/3.0.3: + resolution: {integrity: sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA==} + dev: false + + /webidl-conversions/3.0.1: + resolution: {integrity: sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=} + dev: false + + /whatwg-url/5.0.0: + resolution: {integrity: sha1-lmRU6HZUYuN2RNNib2dCzotwll0=} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /which-boxed-primitive/1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.6 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: false + + /which/2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: false + + /word-wrap/1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: false + + /workerpool/6.2.0: + resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==} + dev: false + + /wrap-ansi/7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: false + + /wrappy/1.0.2: + resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} + dev: false + + /xml2js/0.4.23: + resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.2.4 + xmlbuilder: 11.0.1 + dev: false + + /xmlbuilder/11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: false + + /y18n/5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + + /yallist/4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false + + /yaml/2.0.0-10: + resolution: {integrity: sha512-FHV8s5ODFFQXX/enJEU2EkanNl1UDBUz8oa4k5Qo/sR+Iq7VmhCDkRMb0/mjJCNeAWQ31W8WV6PYStDE4d9EIw==} + engines: {node: '>= 12'} + dev: false + + /yargs-parser/20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: false + + /yargs-unparser/2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: false + + /yargs/16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.4 + dev: false + + /yocto-queue/0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: false + + file:projects/tar-stream.tgz: + resolution: {integrity: sha512-nvhITZd7tBuaPdqIbR6vk9a7iQzf2OtURzlkW+eSEPkRj3N1GJ5XcFWYqDWzzW2YxG1ydg2JygmRspWglyxebQ==, tarball: file:projects/tar-stream.tgz} + name: '@rush-temp/tar-stream' + version: 0.0.0 + dependencies: + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + tape: 4.15.0 + dev: false + + file:projects/vcpkg-ce.test.tgz: + resolution: {integrity: sha512-/bzmskWSXUXHuRaSwIgHnWdqLwXNGmtZlVM5fPazQCC/Q+clcXxZGpGPDapHNVFsvLKLQdFWCeSW9DE0Fb8Xag==, tarball: file:projects/vcpkg-ce.test.tgz} + name: '@rush-temp/vcpkg-ce.test' + version: 0.0.0 + dependencies: + '@types/mocha': 9.1.0 + '@types/node': 17.0.15 + '@types/semver': 7.3.9 + '@typescript-eslint/eslint-plugin': 5.10.2_2595c2126aec4d4b6e944b931dabb4c2 + '@typescript-eslint/parser': 5.10.2_eslint@8.8.0+typescript@4.5.5 + eslint: 8.8.0 + eslint-plugin-notice: 0.9.10_eslint@8.8.0 + mocha: 9.2.0 + semver: 7.3.5 + shx: 0.3.4 + source-map-support: 0.5.21 + txtgen: 2.2.8 + typescript: 4.5.5 + yaml: 2.0.0-10 + transitivePeerDependencies: + - supports-color + dev: false + + file:projects/vcpkg-ce.tgz: + resolution: {integrity: sha512-w5KSYh3XmlMetgClCJl9yaqPTwWOiuUZ91yXM/L3SUkpH8RL3Ti49xC7ma7Jq2/UaJVWSpu2o74mnvjE5P/xQg==, tarball: file:projects/vcpkg-ce.tgz} + name: '@rush-temp/vcpkg-ce' + version: 0.0.0 + dependencies: + '@snyk/nuget-semver': 1.3.0 + '@types/cli-progress': 3.9.2 + '@types/marked': 4.0.2 + '@types/marked-terminal': 3.1.3 + '@types/micromatch': 4.0.2 + '@types/mocha': 9.1.0 + '@types/node': 17.0.15 + '@types/semver': 7.3.9 + '@types/tar-stream': 2.2.2 + '@typescript-eslint/eslint-plugin': 5.10.2_2595c2126aec4d4b6e944b931dabb4c2 + '@typescript-eslint/parser': 5.10.2_eslint@8.8.0+typescript@4.5.5 + applicationinsights: 2.2.1 + chalk: 4.1.2 + cli-progress: 3.10.0 + ee-ts: 2.0.0-rc.6_typescript@4.5.5 + eslint: 8.8.0 + eslint-plugin-notice: 0.9.10_eslint@8.8.0 + fast-glob: 3.2.11 + fast-xml-parser: 4.0.3 + got: 11.8.3 + marked: 4.0.12 + marked-terminal: 5.1.1_marked@4.0.12 + micromatch: 4.0.4 + sed-lite: 0.8.4 + semver: 7.3.5 + shx: 0.3.4 + sorted-btree: 1.6.0 + source-map-support: 0.5.21 + tar: 6.1.11 + tar-stream: 2.2.0 + translate-strings: 1.0.11 + typescript: 4.5.5 + unbzip2-stream: 1.4.3 + vscode-uri: 3.0.3 + yaml: 2.0.0-10 + transitivePeerDependencies: + - applicationinsights-native-metrics + - encoding + - supports-color + dev: false diff --git a/ce/common/config/rush/version-policies.json b/ce/common/config/rush/version-policies.json new file mode 100644 index 0000000000..ef0b4ba10c --- /dev/null +++ b/ce/common/config/rush/version-policies.json @@ -0,0 +1,90 @@ +/** + * This is configuration file is used for advanced publishing configurations with Rush. + * For full documentation, please see https://rushjs.io + */ + +/** + * A list of version policy definitions. A "version policy" is a custom package versioning + * strategy that affects "rush change", "rush version", and "rush publish". The strategy applies + * to a set of projects that are specified using the "versionPolicyName" field in rush.json. + */ +[ + // { + // /** + // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). + // * + // * The "lockStepVersion" mode specifies that the projects will use "lock-step versioning". This + // * strategy is appropriate for a set of packages that act as selectable components of a + // * unified product. The entire set of packages are always published together, and always share + // * the same NPM version number. When the packages depend on other packages in the set, the + // * SemVer range is usually restricted to a single version. + // */ + // "definitionName": "lockStepVersion", + // + // /** + // * (Required) The name that will be used for the "versionPolicyName" field in rush.json. + // * This name is also used command-line parameters such as "--version-policy" + // * and "--to-version-policy". + // */ + // "policyName": "MyBigFramework", + // + // /** + // * (Required) The current version. All packages belonging to the set should have this version + // * in the current branch. When bumping versions, Rush uses this to determine the next version. + // * (The "version" field in package.json is NOT considered.) + // */ + // "version": "1.0.0", + // + // /** + // * (Required) The type of bump that will be performed when publishing the next release. + // * When creating a release branch in Git, this field should be updated according to the + // * type of release. + // * + // * Valid values are: "prerelease", "release", "minor", "patch", "major" + // */ + // "nextBump": "prerelease", + // + // /** + // * (Optional) If specified, all packages in the set share a common CHANGELOG.md file. + // * This file is stored with the specified "main" project, which must be a member of the set. + // * + // * If this field is omitted, then a separate CHANGELOG.md file will be maintained for each + // * package in the set. + // */ + // "mainProject": "my-app" + // }, + // + // { + // /** + // * (Required) Indicates the kind of version policy being defined ("lockStepVersion" or "individualVersion"). + // * + // * The "individualVersion" mode specifies that the projects will use "individual versioning". + // * This is the typical NPM model where each package has an independent version number + // * and CHANGELOG.md file. Although a single CI definition is responsible for publishing the + // * packages, they otherwise don't have any special relationship. The version bumping will + // * depend on how developers answer the "rush change" questions for each package that + // * is changed. + // */ + // "definitionName": "individualVersion", + // + // "policyName": "MyRandomLibraries", + // + // /** + // * (Optional) This can be used to enforce that all packages in the set must share a common + // * major version number, e.g. because they are from the same major release branch. + // * It can also be used to discourage people from accidentally making "MAJOR" SemVer changes + // * inappropriately. The minor/patch version parts will be bumped independently according + // * to the types of changes made to each project, according to the "rush change" command. + // */ + // "lockedMajor": 3, + // + // /** + // * (Optional) When publishing is managed by Rush, by default the "rush change" command will + // * request changes for any projects that are modified by a pull request. These change entries + // * will produce a CHANGELOG.md file. If you author your CHANGELOG.md manually or announce updates + // * in some other way, set "exemptFromRushChange" to true to tell "rush change" to ignore the projects + // * belonging to this version policy. + // */ + // "exemptFromRushChange": false + // } +] diff --git a/ce/common/git-hooks/commit-msg.sample b/ce/common/git-hooks/commit-msg.sample new file mode 100644 index 0000000000..59cacb80ca --- /dev/null +++ b/ce/common/git-hooks/commit-msg.sample @@ -0,0 +1,25 @@ +#!/bin/sh +# +# This is an example Git hook for use with Rush. To enable this hook, rename this file +# to "commit-msg" and then run "rush install", which will copy it from common/git-hooks +# to the .git/hooks folder. +# +# TO LEARN MORE ABOUT GIT HOOKS +# +# The Git documentation is here: https://git-scm.com/docs/githooks +# Some helpful resources: https://githooks.com +# +# ABOUT THIS EXAMPLE +# +# The commit-msg hook is called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero status after issuing +# an appropriate message if it wants to stop the commit. The hook is allowed to edit +# the commit message file. + +# This example enforces that commit message should contain a minimum amount of +# description text. +if [ `cat $1 | wc -w` -lt 3 ]; then + echo "" + echo "Invalid commit message: The message must contain at least 3 words." + exit 1 +fi diff --git a/ce/common/header.txt b/ce/common/header.txt new file mode 100644 index 0000000000..5f34b52a07 --- /dev/null +++ b/ce/common/header.txt @@ -0,0 +1,3 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + diff --git a/ce/common/scripts/install-run-rush.js b/ce/common/scripts/install-run-rush.js new file mode 100644 index 0000000000..5afdb9ad84 --- /dev/null +++ b/ce/common/scripts/install-run-rush.js @@ -0,0 +1,86 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for license information. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where the Rush command may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush +// specified in the rush.json configuration file (if not already installed), and then pass a command-line to it. +// An example usage would be: +// +// node common/scripts/install-run-rush.js install +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +const path = __importStar(require("path")); +const fs = __importStar(require("fs")); +const install_run_1 = require("./install-run"); +const PACKAGE_NAME = '@microsoft/rush'; +const RUSH_PREVIEW_VERSION = 'RUSH_PREVIEW_VERSION'; +function _getRushVersion() { + const rushPreviewVersion = process.env[RUSH_PREVIEW_VERSION]; + if (rushPreviewVersion !== undefined) { + console.log(`Using Rush version from environment variable ${RUSH_PREVIEW_VERSION}=${rushPreviewVersion}`); + return rushPreviewVersion; + } + const rushJsonFolder = (0, install_run_1.findRushJsonFolder)(); + const rushJsonPath = path.join(rushJsonFolder, install_run_1.RUSH_JSON_FILENAME); + try { + const rushJsonContents = fs.readFileSync(rushJsonPath, 'utf-8'); + // Use a regular expression to parse out the rushVersion value because rush.json supports comments, + // but JSON.parse does not and we don't want to pull in more dependencies than we need to in this script. + const rushJsonMatches = rushJsonContents.match(/\"rushVersion\"\s*\:\s*\"([0-9a-zA-Z.+\-]+)\"/); + return rushJsonMatches[1]; + } + catch (e) { + throw new Error(`Unable to determine the required version of Rush from rush.json (${rushJsonFolder}). ` + + "The 'rushVersion' field is either not assigned in rush.json or was specified " + + 'using an unexpected syntax.'); + } +} +function _run() { + const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, ...packageBinArgs /* [build, --to, myproject] */] = process.argv; + // Detect if this script was directly invoked, or if the install-run-rushx script was invokved to select the + // appropriate binary inside the rush package to run + const scriptName = path.basename(scriptPath); + const bin = scriptName.toLowerCase() === 'install-run-rushx.js' ? 'rushx' : 'rush'; + if (!nodePath || !scriptPath) { + throw new Error('Unexpected exception: could not detect node path or script path'); + } + if (process.argv.length < 3) { + console.log(`Usage: ${scriptName} [args...]`); + if (scriptName === 'install-run-rush.js') { + console.log(`Example: ${scriptName} build --to myproject`); + } + else { + console.log(`Example: ${scriptName} custom-command`); + } + process.exit(1); + } + (0, install_run_1.runWithErrorAndStatusCode)(() => { + const version = _getRushVersion(); + console.log(`The rush.json configuration requests Rush version ${version}`); + return (0, install_run_1.installAndRun)(PACKAGE_NAME, version, bin, packageBinArgs); + }); +} +_run(); +//# sourceMappingURL=install-run-rush.js.map \ No newline at end of file diff --git a/ce/common/scripts/install-run-rushx.js b/ce/common/scripts/install-run-rushx.js new file mode 100644 index 0000000000..bf26eb5e50 --- /dev/null +++ b/ce/common/scripts/install-run-rushx.js @@ -0,0 +1,18 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for license information. +Object.defineProperty(exports, "__esModule", { value: true }); +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where the Rush command may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the version of Rush +// specified in the rush.json configuration file (if not already installed), and then pass a command-line to the +// rushx command. +// +// An example usage would be: +// +// node common/scripts/install-run-rushx.js custom-command +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +require("./install-run-rush"); +//# sourceMappingURL=install-run-rushx.js.map \ No newline at end of file diff --git a/ce/common/scripts/install-run.js b/ce/common/scripts/install-run.js new file mode 100644 index 0000000000..fa5522e847 --- /dev/null +++ b/ce/common/scripts/install-run.js @@ -0,0 +1,478 @@ +"use strict"; +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See the @microsoft/rush package's LICENSE file for license information. +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runWithErrorAndStatusCode = exports.installAndRun = exports.findRushJsonFolder = exports.getNpmPath = exports.RUSH_JSON_FILENAME = void 0; +// THIS FILE WAS GENERATED BY A TOOL. ANY MANUAL MODIFICATIONS WILL GET OVERWRITTEN WHENEVER RUSH IS UPGRADED. +// +// This script is intended for usage in an automated build environment where a Node tool may not have +// been preinstalled, or may have an unpredictable version. This script will automatically install the specified +// version of the specified tool (if not already installed), and then pass a command-line to it. +// An example usage would be: +// +// node common/scripts/install-run.js qrcode@1.2.2 qrcode https://rushjs.io +// +// For more information, see: https://rushjs.io/pages/maintainer/setup_new_repo/ +const childProcess = __importStar(require("child_process")); +const fs = __importStar(require("fs")); +const os = __importStar(require("os")); +const path = __importStar(require("path")); +exports.RUSH_JSON_FILENAME = 'rush.json'; +const RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME = 'RUSH_TEMP_FOLDER'; +const INSTALLED_FLAG_FILENAME = 'installed.flag'; +const NODE_MODULES_FOLDER_NAME = 'node_modules'; +const PACKAGE_JSON_FILENAME = 'package.json'; +/** + * Parse a package specifier (in the form of name\@version) into name and version parts. + */ +function _parsePackageSpecifier(rawPackageSpecifier) { + rawPackageSpecifier = (rawPackageSpecifier || '').trim(); + const separatorIndex = rawPackageSpecifier.lastIndexOf('@'); + let name; + let version = undefined; + if (separatorIndex === 0) { + // The specifier starts with a scope and doesn't have a version specified + name = rawPackageSpecifier; + } + else if (separatorIndex === -1) { + // The specifier doesn't have a version + name = rawPackageSpecifier; + } + else { + name = rawPackageSpecifier.substring(0, separatorIndex); + version = rawPackageSpecifier.substring(separatorIndex + 1); + } + if (!name) { + throw new Error(`Invalid package specifier: ${rawPackageSpecifier}`); + } + return { name, version }; +} +/** + * As a workaround, copyAndTrimNpmrcFile() copies the .npmrc file to the target folder, and also trims + * unusable lines from the .npmrc file. + * + * Why are we trimming the .npmrc lines? NPM allows environment variables to be specified in + * the .npmrc file to provide different authentication tokens for different registry. + * However, if the environment variable is undefined, it expands to an empty string, which + * produces a valid-looking mapping with an invalid URL that causes an error. Instead, + * we'd prefer to skip that line and continue looking in other places such as the user's + * home directory. + * + * IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities.copyAndTrimNpmrcFile() + */ +function _copyAndTrimNpmrcFile(sourceNpmrcPath, targetNpmrcPath) { + console.log(`Transforming ${sourceNpmrcPath}`); // Verbose + console.log(` --> "${targetNpmrcPath}"`); + let npmrcFileLines = fs.readFileSync(sourceNpmrcPath).toString().split('\n'); + npmrcFileLines = npmrcFileLines.map((line) => (line || '').trim()); + const resultLines = []; + // This finds environment variable tokens that look like "${VAR_NAME}" + const expansionRegExp = /\$\{([^\}]+)\}/g; + // Comment lines start with "#" or ";" + const commentRegExp = /^\s*[#;]/; + // Trim out lines that reference environment variables that aren't defined + for (const line of npmrcFileLines) { + let lineShouldBeTrimmed = false; + // Ignore comment lines + if (!commentRegExp.test(line)) { + const environmentVariables = line.match(expansionRegExp); + if (environmentVariables) { + for (const token of environmentVariables) { + // Remove the leading "${" and the trailing "}" from the token + const environmentVariableName = token.substring(2, token.length - 1); + // Is the environment variable defined? + if (!process.env[environmentVariableName]) { + // No, so trim this line + lineShouldBeTrimmed = true; + break; + } + } + } + } + if (lineShouldBeTrimmed) { + // Example output: + // "; MISSING ENVIRONMENT VARIABLE: //my-registry.com/npm/:_authToken=${MY_AUTH_TOKEN}" + resultLines.push('; MISSING ENVIRONMENT VARIABLE: ' + line); + } + else { + resultLines.push(line); + } + } + fs.writeFileSync(targetNpmrcPath, resultLines.join(os.EOL)); +} +/** + * syncNpmrc() copies the .npmrc file to the target folder, and also trims unusable lines from the .npmrc file. + * If the source .npmrc file not exist, then syncNpmrc() will delete an .npmrc that is found in the target folder. + * + * IMPORTANT: THIS CODE SHOULD BE KEPT UP TO DATE WITH Utilities._syncNpmrc() + */ +function _syncNpmrc(sourceNpmrcFolder, targetNpmrcFolder, useNpmrcPublish) { + const sourceNpmrcPath = path.join(sourceNpmrcFolder, !useNpmrcPublish ? '.npmrc' : '.npmrc-publish'); + const targetNpmrcPath = path.join(targetNpmrcFolder, '.npmrc'); + try { + if (fs.existsSync(sourceNpmrcPath)) { + _copyAndTrimNpmrcFile(sourceNpmrcPath, targetNpmrcPath); + } + else if (fs.existsSync(targetNpmrcPath)) { + // If the source .npmrc doesn't exist and there is one in the target, delete the one in the target + console.log(`Deleting ${targetNpmrcPath}`); // Verbose + fs.unlinkSync(targetNpmrcPath); + } + } + catch (e) { + throw new Error(`Error syncing .npmrc file: ${e}`); + } +} +let _npmPath = undefined; +/** + * Get the absolute path to the npm executable + */ +function getNpmPath() { + if (!_npmPath) { + try { + if (os.platform() === 'win32') { + // We're on Windows + const whereOutput = childProcess.execSync('where npm', { stdio: [] }).toString(); + const lines = whereOutput.split(os.EOL).filter((line) => !!line); + // take the last result, we are looking for a .cmd command + // see https://github.com/microsoft/rushstack/issues/759 + _npmPath = lines[lines.length - 1]; + } + else { + // We aren't on Windows - assume we're on *NIX or Darwin + _npmPath = childProcess.execSync('command -v npm', { stdio: [] }).toString(); + } + } + catch (e) { + throw new Error(`Unable to determine the path to the NPM tool: ${e}`); + } + _npmPath = _npmPath.trim(); + if (!fs.existsSync(_npmPath)) { + throw new Error('The NPM executable does not exist'); + } + } + return _npmPath; +} +exports.getNpmPath = getNpmPath; +function _ensureFolder(folderPath) { + if (!fs.existsSync(folderPath)) { + const parentDir = path.dirname(folderPath); + _ensureFolder(parentDir); + fs.mkdirSync(folderPath); + } +} +/** + * Create missing directories under the specified base directory, and return the resolved directory. + * + * Does not support "." or ".." path segments. + * Assumes the baseFolder exists. + */ +function _ensureAndJoinPath(baseFolder, ...pathSegments) { + let joinedPath = baseFolder; + try { + for (let pathSegment of pathSegments) { + pathSegment = pathSegment.replace(/[\\\/]/g, '+'); + joinedPath = path.join(joinedPath, pathSegment); + if (!fs.existsSync(joinedPath)) { + fs.mkdirSync(joinedPath); + } + } + } + catch (e) { + throw new Error(`Error building local installation folder (${path.join(baseFolder, ...pathSegments)}): ${e}`); + } + return joinedPath; +} +function _getRushTempFolder(rushCommonFolder) { + const rushTempFolder = process.env[RUSH_TEMP_FOLDER_ENV_VARIABLE_NAME]; + if (rushTempFolder !== undefined) { + _ensureFolder(rushTempFolder); + return rushTempFolder; + } + else { + return _ensureAndJoinPath(rushCommonFolder, 'temp'); + } +} +/** + * Resolve a package specifier to a static version + */ +function _resolvePackageVersion(rushCommonFolder, { name, version }) { + if (!version) { + version = '*'; // If no version is specified, use the latest version + } + if (version.match(/^[a-zA-Z0-9\-\+\.]+$/)) { + // If the version contains only characters that we recognize to be used in static version specifiers, + // pass the version through + return version; + } + else { + // version resolves to + try { + const rushTempFolder = _getRushTempFolder(rushCommonFolder); + const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush'); + _syncNpmrc(sourceNpmrcFolder, rushTempFolder); + const npmPath = getNpmPath(); + // This returns something that looks like: + // @microsoft/rush@3.0.0 '3.0.0' + // @microsoft/rush@3.0.1 '3.0.1' + // ... + // @microsoft/rush@3.0.20 '3.0.20' + // + const npmVersionSpawnResult = childProcess.spawnSync(npmPath, ['view', `${name}@${version}`, 'version', '--no-update-notifier'], { + cwd: rushTempFolder, + stdio: [] + }); + if (npmVersionSpawnResult.status !== 0) { + throw new Error(`"npm view" returned error code ${npmVersionSpawnResult.status}`); + } + const npmViewVersionOutput = npmVersionSpawnResult.stdout.toString(); + const versionLines = npmViewVersionOutput.split('\n').filter((line) => !!line); + const latestVersion = versionLines[versionLines.length - 1]; + if (!latestVersion) { + throw new Error('No versions found for the specified version range.'); + } + const versionMatches = latestVersion.match(/^.+\s\'(.+)\'$/); + if (!versionMatches) { + throw new Error(`Invalid npm output ${latestVersion}`); + } + return versionMatches[1]; + } + catch (e) { + throw new Error(`Unable to resolve version ${version} of package ${name}: ${e}`); + } + } +} +let _rushJsonFolder; +/** + * Find the absolute path to the folder containing rush.json + */ +function findRushJsonFolder() { + if (!_rushJsonFolder) { + let basePath = __dirname; + let tempPath = __dirname; + do { + const testRushJsonPath = path.join(basePath, exports.RUSH_JSON_FILENAME); + if (fs.existsSync(testRushJsonPath)) { + _rushJsonFolder = basePath; + break; + } + else { + basePath = tempPath; + } + } while (basePath !== (tempPath = path.dirname(basePath))); // Exit the loop when we hit the disk root + if (!_rushJsonFolder) { + throw new Error('Unable to find rush.json.'); + } + } + return _rushJsonFolder; +} +exports.findRushJsonFolder = findRushJsonFolder; +/** + * Detects if the package in the specified directory is installed + */ +function _isPackageAlreadyInstalled(packageInstallFolder) { + try { + const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); + if (!fs.existsSync(flagFilePath)) { + return false; + } + const fileContents = fs.readFileSync(flagFilePath).toString(); + return fileContents.trim() === process.version; + } + catch (e) { + return false; + } +} +/** + * Removes the following files and directories under the specified folder path: + * - installed.flag + * - + * - node_modules + */ +function _cleanInstallFolder(rushTempFolder, packageInstallFolder) { + try { + const flagFile = path.resolve(packageInstallFolder, INSTALLED_FLAG_FILENAME); + if (fs.existsSync(flagFile)) { + fs.unlinkSync(flagFile); + } + const packageLockFile = path.resolve(packageInstallFolder, 'package-lock.json'); + if (fs.existsSync(packageLockFile)) { + fs.unlinkSync(packageLockFile); + } + const nodeModulesFolder = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME); + if (fs.existsSync(nodeModulesFolder)) { + const rushRecyclerFolder = _ensureAndJoinPath(rushTempFolder, 'rush-recycler'); + fs.renameSync(nodeModulesFolder, path.join(rushRecyclerFolder, `install-run-${Date.now().toString()}`)); + } + } + catch (e) { + throw new Error(`Error cleaning the package install folder (${packageInstallFolder}): ${e}`); + } +} +function _createPackageJson(packageInstallFolder, name, version) { + try { + const packageJsonContents = { + name: 'ci-rush', + version: '0.0.0', + dependencies: { + [name]: version + }, + description: "DON'T WARN", + repository: "DON'T WARN", + license: 'MIT' + }; + const packageJsonPath = path.join(packageInstallFolder, PACKAGE_JSON_FILENAME); + fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContents, undefined, 2)); + } + catch (e) { + throw new Error(`Unable to create package.json: ${e}`); + } +} +/** + * Run "npm install" in the package install folder. + */ +function _installPackage(packageInstallFolder, name, version) { + try { + console.log(`Installing ${name}...`); + const npmPath = getNpmPath(); + const result = childProcess.spawnSync(npmPath, ['install'], { + stdio: 'inherit', + cwd: packageInstallFolder, + env: process.env + }); + if (result.status !== 0) { + throw new Error('"npm install" encountered an error'); + } + console.log(`Successfully installed ${name}@${version}`); + } + catch (e) { + throw new Error(`Unable to install package: ${e}`); + } +} +/** + * Get the ".bin" path for the package. + */ +function _getBinPath(packageInstallFolder, binName) { + const binFolderPath = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); + const resolvedBinName = os.platform() === 'win32' ? `${binName}.cmd` : binName; + return path.resolve(binFolderPath, resolvedBinName); +} +/** + * Write a flag file to the package's install directory, signifying that the install was successful. + */ +function _writeFlagFile(packageInstallFolder) { + try { + const flagFilePath = path.join(packageInstallFolder, INSTALLED_FLAG_FILENAME); + fs.writeFileSync(flagFilePath, process.version); + } + catch (e) { + throw new Error(`Unable to create installed.flag file in ${packageInstallFolder}`); + } +} +function installAndRun(packageName, packageVersion, packageBinName, packageBinArgs) { + const rushJsonFolder = findRushJsonFolder(); + const rushCommonFolder = path.join(rushJsonFolder, 'common'); + const rushTempFolder = _getRushTempFolder(rushCommonFolder); + const packageInstallFolder = _ensureAndJoinPath(rushTempFolder, 'install-run', `${packageName}@${packageVersion}`); + if (!_isPackageAlreadyInstalled(packageInstallFolder)) { + // The package isn't already installed + _cleanInstallFolder(rushTempFolder, packageInstallFolder); + const sourceNpmrcFolder = path.join(rushCommonFolder, 'config', 'rush'); + _syncNpmrc(sourceNpmrcFolder, packageInstallFolder); + _createPackageJson(packageInstallFolder, packageName, packageVersion); + _installPackage(packageInstallFolder, packageName, packageVersion); + _writeFlagFile(packageInstallFolder); + } + const statusMessage = `Invoking "${packageBinName} ${packageBinArgs.join(' ')}"`; + const statusMessageLine = new Array(statusMessage.length + 1).join('-'); + console.log(os.EOL + statusMessage + os.EOL + statusMessageLine + os.EOL); + const binPath = _getBinPath(packageInstallFolder, packageBinName); + const binFolderPath = path.resolve(packageInstallFolder, NODE_MODULES_FOLDER_NAME, '.bin'); + // Windows environment variables are case-insensitive. Instead of using SpawnSyncOptions.env, we need to + // assign via the process.env proxy to ensure that we append to the right PATH key. + const originalEnvPath = process.env.PATH || ''; + let result; + try { + // Node.js on Windows can not spawn a file when the path has a space on it + // unless the path gets wrapped in a cmd friendly way and shell mode is used + const shouldUseShell = binPath.includes(' ') && os.platform() === 'win32'; + const platformBinPath = shouldUseShell ? `"${binPath}"` : binPath; + process.env.PATH = [binFolderPath, originalEnvPath].join(path.delimiter); + result = childProcess.spawnSync(platformBinPath, packageBinArgs, { + stdio: 'inherit', + windowsVerbatimArguments: false, + shell: shouldUseShell, + cwd: process.cwd(), + env: process.env + }); + } + finally { + process.env.PATH = originalEnvPath; + } + if (result.status !== null) { + return result.status; + } + else { + throw result.error || new Error('An unknown error occurred.'); + } +} +exports.installAndRun = installAndRun; +function runWithErrorAndStatusCode(fn) { + process.exitCode = 1; + try { + const exitCode = fn(); + process.exitCode = exitCode; + } + catch (e) { + console.error(os.EOL + os.EOL + e.toString() + os.EOL + os.EOL); + } +} +exports.runWithErrorAndStatusCode = runWithErrorAndStatusCode; +function _run() { + const [nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, rawPackageSpecifier /* qrcode@^1.2.0 */, packageBinName /* qrcode */, ...packageBinArgs /* [-f, myproject/lib] */] = process.argv; + if (!nodePath) { + throw new Error('Unexpected exception: could not detect node path'); + } + if (path.basename(scriptPath).toLowerCase() !== 'install-run.js') { + // If install-run.js wasn't directly invoked, don't execute the rest of this function. Return control + // to the script that (presumably) imported this file + return; + } + if (process.argv.length < 4) { + console.log('Usage: install-run.js @ [args...]'); + console.log('Example: install-run.js qrcode@1.2.2 qrcode https://rushjs.io'); + process.exit(1); + } + runWithErrorAndStatusCode(() => { + const rushJsonFolder = findRushJsonFolder(); + const rushCommonFolder = _ensureAndJoinPath(rushJsonFolder, 'common'); + const packageSpecifier = _parsePackageSpecifier(rawPackageSpecifier); + const name = packageSpecifier.name; + const version = _resolvePackageVersion(rushCommonFolder, packageSpecifier); + if (packageSpecifier.version !== version) { + console.log(`Resolved to ${name}@${version}`); + } + return installAndRun(name, version, packageBinName, packageBinArgs); + }); +} +_run(); +//# sourceMappingURL=install-run.js.map \ No newline at end of file diff --git a/ce/common/tsconfig.json b/ce/common/tsconfig.json new file mode 100644 index 0000000000..e261959481 --- /dev/null +++ b/ce/common/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + "forceConsistentCasingInFileNames": true, + "module": "CommonJS", + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitThis": true, + "inlineSourceMap": true, + "declarationMap": true, + "strict": true, + "declaration": true, + "stripInternal": true, + "noEmitHelpers": false, + "target": "ESNext", + "types": [ + "node" + ], + "lib": [ + "ESNext", + "DOM" + ], + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "newLine": "LF" + }, + "exclude": [ + "../**/dist/**", + "../node_modules/**", + "../**/*.d.ts" + ] +} \ No newline at end of file diff --git a/ce/custom/tar-stream/.gitignore b/ce/custom/tar-stream/.gitignore new file mode 100644 index 0000000000..7938f33ae1 --- /dev/null +++ b/ce/custom/tar-stream/.gitignore @@ -0,0 +1,2 @@ +node_modules +sandbox.js diff --git a/ce/custom/tar-stream/BufferList.js b/ce/custom/tar-stream/BufferList.js new file mode 100644 index 0000000000..471ee77889 --- /dev/null +++ b/ce/custom/tar-stream/BufferList.js @@ -0,0 +1,396 @@ +'use strict' + +const { Buffer } = require('buffer') +const symbol = Symbol.for('BufferList') + +function BufferList (buf) { + if (!(this instanceof BufferList)) { + return new BufferList(buf) + } + + BufferList._init.call(this, buf) +} + +BufferList._init = function _init (buf) { + Object.defineProperty(this, symbol, { value: true }) + + this._bufs = [] + this.length = 0 + + if (buf) { + this.append(buf) + } +} + +BufferList.prototype._new = function _new (buf) { + return new BufferList(buf) +} + +BufferList.prototype._offset = function _offset (offset) { + if (offset === 0) { + return [0, 0] + } + + let tot = 0 + + for (let i = 0; i < this._bufs.length; i++) { + const _t = tot + this._bufs[i].length + if (offset < _t || i === this._bufs.length - 1) { + return [i, offset - tot] + } + tot = _t + } +} + +BufferList.prototype._reverseOffset = function (blOffset) { + const bufferId = blOffset[0] + let offset = blOffset[1] + + for (let i = 0; i < bufferId; i++) { + offset += this._bufs[i].length + } + + return offset +} + +BufferList.prototype.get = function get (index) { + if (index > this.length || index < 0) { + return undefined + } + + const offset = this._offset(index) + + return this._bufs[offset[0]][offset[1]] +} + +BufferList.prototype.slice = function slice (start, end) { + if (typeof start === 'number' && start < 0) { + start += this.length + } + + if (typeof end === 'number' && end < 0) { + end += this.length + } + + return this.copy(null, 0, start, end) +} + +BufferList.prototype.copy = function copy (dst, dstStart, srcStart, srcEnd) { + if (typeof srcStart !== 'number' || srcStart < 0) { + srcStart = 0 + } + + if (typeof srcEnd !== 'number' || srcEnd > this.length) { + srcEnd = this.length + } + + if (srcStart >= this.length) { + return dst || Buffer.alloc(0) + } + + if (srcEnd <= 0) { + return dst || Buffer.alloc(0) + } + + const copy = !!dst + const off = this._offset(srcStart) + const len = srcEnd - srcStart + let bytes = len + let bufoff = (copy && dstStart) || 0 + let start = off[1] + + // copy/slice everything + if (srcStart === 0 && srcEnd === this.length) { + if (!copy) { + // slice, but full concat if multiple buffers + return this._bufs.length === 1 + ? this._bufs[0] + : Buffer.concat(this._bufs, this.length) + } + + // copy, need to copy individual buffers + for (let i = 0; i < this._bufs.length; i++) { + this._bufs[i].copy(dst, bufoff) + bufoff += this._bufs[i].length + } + + return dst + } + + // easy, cheap case where it's a subset of one of the buffers + if (bytes <= this._bufs[off[0]].length - start) { + return copy + ? this._bufs[off[0]].copy(dst, dstStart, start, start + bytes) + : this._bufs[off[0]].slice(start, start + bytes) + } + + if (!copy) { + // a slice, we need something to copy in to + dst = Buffer.allocUnsafe(len) + } + + for (let i = off[0]; i < this._bufs.length; i++) { + const l = this._bufs[i].length - start + + if (bytes > l) { + this._bufs[i].copy(dst, bufoff, start) + bufoff += l + } else { + this._bufs[i].copy(dst, bufoff, start, start + bytes) + bufoff += l + break + } + + bytes -= l + + if (start) { + start = 0 + } + } + + // safeguard so that we don't return uninitialized memory + if (dst.length > bufoff) return dst.slice(0, bufoff) + + return dst +} + +BufferList.prototype.shallowSlice = function shallowSlice (start, end) { + start = start || 0 + end = typeof end !== 'number' ? this.length : end + + if (start < 0) { + start += this.length + } + + if (end < 0) { + end += this.length + } + + if (start === end) { + return this._new() + } + + const startOffset = this._offset(start) + const endOffset = this._offset(end) + const buffers = this._bufs.slice(startOffset[0], endOffset[0] + 1) + + if (endOffset[1] === 0) { + buffers.pop() + } else { + buffers[buffers.length - 1] = buffers[buffers.length - 1].slice(0, endOffset[1]) + } + + if (startOffset[1] !== 0) { + buffers[0] = buffers[0].slice(startOffset[1]) + } + + return this._new(buffers) +} + +BufferList.prototype.toString = function toString (encoding, start, end) { + return this.slice(start, end).toString(encoding) +} + +BufferList.prototype.consume = function consume (bytes) { + // first, normalize the argument, in accordance with how Buffer does it + bytes = Math.trunc(bytes) + // do nothing if not a positive number + if (Number.isNaN(bytes) || bytes <= 0) return this + + while (this._bufs.length) { + if (bytes >= this._bufs[0].length) { + bytes -= this._bufs[0].length + this.length -= this._bufs[0].length + this._bufs.shift() + } else { + this._bufs[0] = this._bufs[0].slice(bytes) + this.length -= bytes + break + } + } + + return this +} + +BufferList.prototype.duplicate = function duplicate () { + const copy = this._new() + + for (let i = 0; i < this._bufs.length; i++) { + copy.append(this._bufs[i]) + } + + return copy +} + +BufferList.prototype.append = function append (buf) { + if (buf == null) { + return this + } + + if (buf.buffer) { + // append a view of the underlying ArrayBuffer + this._appendBuffer(Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength)) + } else if (Array.isArray(buf)) { + for (let i = 0; i < buf.length; i++) { + this.append(buf[i]) + } + } else if (this._isBufferList(buf)) { + // unwrap argument into individual BufferLists + for (let i = 0; i < buf._bufs.length; i++) { + this.append(buf._bufs[i]) + } + } else { + // coerce number arguments to strings, since Buffer(number) does + // uninitialized memory allocation + if (typeof buf === 'number') { + buf = buf.toString() + } + + this._appendBuffer(Buffer.from(buf)) + } + + return this +} + +BufferList.prototype._appendBuffer = function appendBuffer (buf) { + this._bufs.push(buf) + this.length += buf.length +} + +BufferList.prototype.indexOf = function (search, offset, encoding) { + if (encoding === undefined && typeof offset === 'string') { + encoding = offset + offset = undefined + } + + if (typeof search === 'function' || Array.isArray(search)) { + throw new TypeError('The "value" argument must be one of type string, Buffer, BufferList, or Uint8Array.') + } else if (typeof search === 'number') { + search = Buffer.from([search]) + } else if (typeof search === 'string') { + search = Buffer.from(search, encoding) + } else if (this._isBufferList(search)) { + search = search.slice() + } else if (Array.isArray(search.buffer)) { + search = Buffer.from(search.buffer, search.byteOffset, search.byteLength) + } else if (!Buffer.isBuffer(search)) { + search = Buffer.from(search) + } + + offset = Number(offset || 0) + + if (isNaN(offset)) { + offset = 0 + } + + if (offset < 0) { + offset = this.length + offset + } + + if (offset < 0) { + offset = 0 + } + + if (search.length === 0) { + return offset > this.length ? this.length : offset + } + + const blOffset = this._offset(offset) + let blIndex = blOffset[0] // index of which internal buffer we're working on + let buffOffset = blOffset[1] // offset of the internal buffer we're working on + + // scan over each buffer + for (; blIndex < this._bufs.length; blIndex++) { + const buff = this._bufs[blIndex] + + while (buffOffset < buff.length) { + const availableWindow = buff.length - buffOffset + + if (availableWindow >= search.length) { + const nativeSearchResult = buff.indexOf(search, buffOffset) + + if (nativeSearchResult !== -1) { + return this._reverseOffset([blIndex, nativeSearchResult]) + } + + buffOffset = buff.length - search.length + 1 // end of native search window + } else { + const revOffset = this._reverseOffset([blIndex, buffOffset]) + + if (this._match(revOffset, search)) { + return revOffset + } + + buffOffset++ + } + } + + buffOffset = 0 + } + + return -1 +} + +BufferList.prototype._match = function (offset, search) { + if (this.length - offset < search.length) { + return false + } + + for (let searchOffset = 0; searchOffset < search.length; searchOffset++) { + if (this.get(offset + searchOffset) !== search[searchOffset]) { + return false + } + } + return true +} + +;(function () { + const methods = { + readDoubleBE: 8, + readDoubleLE: 8, + readFloatBE: 4, + readFloatLE: 4, + readInt32BE: 4, + readInt32LE: 4, + readUInt32BE: 4, + readUInt32LE: 4, + readInt16BE: 2, + readInt16LE: 2, + readUInt16BE: 2, + readUInt16LE: 2, + readInt8: 1, + readUInt8: 1, + readIntBE: null, + readIntLE: null, + readUIntBE: null, + readUIntLE: null + } + + for (const m in methods) { + (function (m) { + if (methods[m] === null) { + BufferList.prototype[m] = function (offset, byteLength) { + return this.slice(offset, offset + byteLength)[m](0, byteLength) + } + } else { + BufferList.prototype[m] = function (offset = 0) { + return this.slice(offset, offset + methods[m])[m](0) + } + } + }(m)) + } +}()) + +// Used internally by the class and also as an indicator of this object being +// a `BufferList`. It's not possible to use `instanceof BufferList` in a browser +// environment because there could be multiple different copies of the +// BufferList class and some `BufferList`s might be `BufferList`s. +BufferList.prototype._isBufferList = function _isBufferList (b) { + return b instanceof BufferList || BufferList.isBufferList(b) +} + +BufferList.isBufferList = function isBufferList (b) { + return b != null && b[symbol] +} + +module.exports = BufferList diff --git a/ce/custom/tar-stream/LICENSE b/ce/custom/tar-stream/LICENSE new file mode 100644 index 0000000000..757562ec59 --- /dev/null +++ b/ce/custom/tar-stream/LICENSE @@ -0,0 +1,21 @@ +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/ce/custom/tar-stream/README.md b/ce/custom/tar-stream/README.md new file mode 100644 index 0000000000..2679d9d018 --- /dev/null +++ b/ce/custom/tar-stream/README.md @@ -0,0 +1,168 @@ +# tar-stream + +tar-stream is a streaming tar parser and generator and nothing else. It is streams2 and operates purely using streams which means you can easily extract/parse tarballs without ever hitting the file system. + +Note that you still need to gunzip your data if you have a `.tar.gz`. We recommend using [gunzip-maybe](https://github.com/mafintosh/gunzip-maybe) in conjunction with this. + +``` +npm install tar-stream +``` + +[![build status](https://secure.travis-ci.org/mafintosh/tar-stream.png)](http://travis-ci.org/mafintosh/tar-stream) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) + +## Usage + +tar-stream exposes two streams, [pack](https://github.com/mafintosh/tar-stream#packing) which creates tarballs and [extract](https://github.com/mafintosh/tar-stream#extracting) which extracts tarballs. To [modify an existing tarball](https://github.com/mafintosh/tar-stream#modifying-existing-tarballs) use both. + + +It implementes USTAR with additional support for pax extended headers. It should be compatible with all popular tar distributions out there (gnutar, bsdtar etc) + +## Related + +If you want to pack/unpack directories on the file system check out [tar-fs](https://github.com/mafintosh/tar-fs) which provides file system bindings to this module. + +## Packing + +To create a pack stream use `tar.pack()` and call `pack.entry(header, [callback])` to add tar entries. + +``` js +var tar = require('tar-stream') +var pack = tar.pack() // pack is a streams2 stream + +// add a file called my-test.txt with the content "Hello World!" +pack.entry({ name: 'my-test.txt' }, 'Hello World!') + +// add a file called my-stream-test.txt from a stream +var entry = pack.entry({ name: 'my-stream-test.txt', size: 11 }, function(err) { + // the stream was added + // no more entries + pack.finalize() +}) + +entry.write('hello') +entry.write(' ') +entry.write('world') +entry.end() + +// pipe the pack stream somewhere +pack.pipe(process.stdout) +``` + +## Extracting + +To extract a stream use `tar.extract()` and listen for `extract.on('entry', (header, stream, next) )` + +``` js +var extract = tar.extract() + +extract.on('entry', function(header, stream, next) { + // header is the tar header + // stream is the content body (might be an empty stream) + // call next when you are done with this entry + + stream.on('end', function() { + next() // ready for next entry + }) + + stream.resume() // just auto drain the stream +}) + +extract.on('finish', function() { + // all entries read +}) + +pack.pipe(extract) +``` + +The tar archive is streamed sequentially, meaning you **must** drain each entry's stream as you get them or else the main extract stream will receive backpressure and stop reading. + +## Headers + +The header object using in `entry` should contain the following properties. +Most of these values can be found by stat'ing a file. + +``` js +{ + name: 'path/to/this/entry.txt', + size: 1314, // entry size. defaults to 0 + mode: 0o644, // entry mode. defaults to to 0o755 for dirs and 0o644 otherwise + mtime: new Date(), // last modified date for entry. defaults to now. + type: 'file', // type of entry. defaults to file. can be: + // file | link | symlink | directory | block-device + // character-device | fifo | contiguous-file + linkname: 'path', // linked file name + uid: 0, // uid of entry owner. defaults to 0 + gid: 0, // gid of entry owner. defaults to 0 + uname: 'maf', // uname of entry owner. defaults to null + gname: 'staff', // gname of entry owner. defaults to null + devmajor: 0, // device major version. defaults to 0 + devminor: 0 // device minor version. defaults to 0 +} +``` + +## Modifying existing tarballs + +Using tar-stream it is easy to rewrite paths / change modes etc in an existing tarball. + +``` js +var extract = tar.extract() +var pack = tar.pack() +var path = require('path') + +extract.on('entry', function(header, stream, callback) { + // let's prefix all names with 'tmp' + header.name = path.join('tmp', header.name) + // write the new entry to the pack stream + stream.pipe(pack.entry(header, callback)) +}) + +extract.on('finish', function() { + // all entries done - lets finalize it + pack.finalize() +}) + +// pipe the old tarball to the extractor +oldTarballStream.pipe(extract) + +// pipe the new tarball the another stream +pack.pipe(newTarballStream) +``` + +## Saving tarball to fs + + +``` js +var fs = require('fs') +var tar = require('tar-stream') + +var pack = tar.pack() // pack is a streams2 stream +var path = 'YourTarBall.tar' +var yourTarball = fs.createWriteStream(path) + +// add a file called YourFile.txt with the content "Hello World!" +pack.entry({name: 'YourFile.txt'}, 'Hello World!', function (err) { + if (err) throw err + pack.finalize() +}) + +// pipe the pack stream to your file +pack.pipe(yourTarball) + +yourTarball.on('close', function () { + console.log(path + ' has been written') + fs.stat(path, function(err, stats) { + if (err) throw err + console.log(stats) + console.log('Got file info successfully!') + }) +}) +``` + +## Performance + +[See tar-fs for a performance comparison with node-tar](https://github.com/mafintosh/tar-fs/blob/master/README.md#performance) + +# License + +MIT diff --git a/ce/custom/tar-stream/bl.js b/ce/custom/tar-stream/bl.js new file mode 100644 index 0000000000..7244e88554 --- /dev/null +++ b/ce/custom/tar-stream/bl.js @@ -0,0 +1,84 @@ +'use strict' + +const DuplexStream = require('stream').Duplex +const inherits = require('inherits') +const BufferList = require('./BufferList') + +function BufferListStream (callback) { + if (!(this instanceof BufferListStream)) { + return new BufferListStream(callback) + } + + if (typeof callback === 'function') { + this._callback = callback + + const piper = function piper (err) { + if (this._callback) { + this._callback(err) + this._callback = null + } + }.bind(this) + + this.on('pipe', function onPipe (src) { + src.on('error', piper) + }) + this.on('unpipe', function onUnpipe (src) { + src.removeListener('error', piper) + }) + + callback = null + } + + BufferList._init.call(this, callback) + DuplexStream.call(this) +} + +inherits(BufferListStream, DuplexStream) +Object.assign(BufferListStream.prototype, BufferList.prototype) + +BufferListStream.prototype._new = function _new (callback) { + return new BufferListStream(callback) +} + +BufferListStream.prototype._write = function _write (buf, encoding, callback) { + this._appendBuffer(buf) + + if (typeof callback === 'function') { + callback() + } +} + +BufferListStream.prototype._read = function _read (size) { + if (!this.length) { + return this.push(null) + } + + size = Math.min(size, this.length) + this.push(this.slice(0, size)) + this.consume(size) +} + +BufferListStream.prototype.end = function end (chunk) { + DuplexStream.prototype.end.call(this, chunk) + + if (this._callback) { + this._callback(null, this.slice()) + this._callback = null + } +} + +BufferListStream.prototype._destroy = function _destroy (err, cb) { + this._bufs.length = 0 + this.length = 0 + cb(err) +} + +BufferListStream.prototype._isBufferList = function _isBufferList (b) { + return b instanceof BufferListStream || b instanceof BufferList || BufferListStream.isBufferList(b) +} + +BufferListStream.isBufferList = BufferList.isBufferList + +module.exports = BufferListStream +module.exports.BufferListStream = BufferListStream +module.exports.BufferList = BufferList diff --git a/ce/custom/tar-stream/extract.js b/ce/custom/tar-stream/extract.js new file mode 100644 index 0000000000..f55d4d4516 --- /dev/null +++ b/ce/custom/tar-stream/extract.js @@ -0,0 +1,257 @@ +var util = require('util') +var bl = require('./bl') +var headers = require('./headers') + +var Writable = require('stream').Writable +var PassThrough = require('stream').PassThrough + +var noop = function () {} + +var overflow = function (size) { + size &= 511 + return size && 512 - size +} + +var emptyStream = function (self, offset) { + var s = new Source(self, offset) + s.end() + return s +} + +var mixinPax = function (header, pax) { + if (pax.path) header.name = pax.path + if (pax.linkpath) header.linkname = pax.linkpath + if (pax.size) header.size = parseInt(pax.size, 10) + header.pax = pax + return header +} + +var Source = function (self, offset) { + this._parent = self + this.offset = offset + PassThrough.call(this, { autoDestroy: false }) +} + +util.inherits(Source, PassThrough) + +Source.prototype.destroy = function (err) { + this._parent.destroy(err) +} + +var Extract = function (opts) { + if (!(this instanceof Extract)) return new Extract(opts) + Writable.call(this, opts) + + opts = opts || {} + + this._offset = 0 + this._buffer = bl() + this._missing = 0 + this._partial = false + this._onparse = noop + this._header = null + this._stream = null + this._overflow = null + this._cb = null + this._locked = false + this._destroyed = false + this._pax = null + this._paxGlobal = null + this._gnuLongPath = null + this._gnuLongLinkPath = null + + var self = this + var b = self._buffer + + var oncontinue = function () { + self._continue() + } + + var onunlock = function (err) { + self._locked = false + if (err) return self.destroy(err) + if (!self._stream) oncontinue() + } + + var onstreamend = function () { + self._stream = null + var drain = overflow(self._header.size) + if (drain) self._parse(drain, ondrain) + else self._parse(512, onheader) + if (!self._locked) oncontinue() + } + + var ondrain = function () { + self._buffer.consume(overflow(self._header.size)) + self._parse(512, onheader) + oncontinue() + } + + var onpaxglobalheader = function () { + var size = self._header.size + self._paxGlobal = headers.decodePax(b.slice(0, size)) + b.consume(size) + onstreamend() + } + + var onpaxheader = function () { + var size = self._header.size + self._pax = headers.decodePax(b.slice(0, size)) + if (self._paxGlobal) self._pax = Object.assign({}, self._paxGlobal, self._pax) + b.consume(size) + onstreamend() + } + + var ongnulongpath = function () { + var size = self._header.size + this._gnuLongPath = headers.decodeLongPath(b.slice(0, size), opts.filenameEncoding) + b.consume(size) + onstreamend() + } + + var ongnulonglinkpath = function () { + var size = self._header.size + this._gnuLongLinkPath = headers.decodeLongPath(b.slice(0, size), opts.filenameEncoding) + b.consume(size) + onstreamend() + } + + var onheader = function () { + var offset = self._offset + var header + try { + header = self._header = headers.decode(b.slice(0, 512), opts.filenameEncoding, opts.allowUnknownFormat) + } catch (err) { + self.emit('error', err) + } + b.consume(512) + + if (!header) { + self._parse(512, onheader) + oncontinue() + return + } + if (header.type === 'gnu-long-path') { + self._parse(header.size, ongnulongpath) + oncontinue() + return + } + if (header.type === 'gnu-long-link-path') { + self._parse(header.size, ongnulonglinkpath) + oncontinue() + return + } + if (header.type === 'pax-global-header') { + self._parse(header.size, onpaxglobalheader) + oncontinue() + return + } + if (header.type === 'pax-header') { + self._parse(header.size, onpaxheader) + oncontinue() + return + } + + if (self._gnuLongPath) { + header.name = self._gnuLongPath + self._gnuLongPath = null + } + + if (self._gnuLongLinkPath) { + header.linkname = self._gnuLongLinkPath + self._gnuLongLinkPath = null + } + + if (self._pax) { + self._header = header = mixinPax(header, self._pax) + self._pax = null + } + + self._locked = true + + if (!header.size || header.type === 'directory') { + self._parse(512, onheader) + self.emit('entry', header, emptyStream(self, offset), onunlock) + return + } + + self._stream = new Source(self, offset) + + self.emit('entry', header, self._stream, onunlock) + self._parse(header.size, onstreamend) + oncontinue() + } + + this._onheader = onheader + this._parse(512, onheader) +} + +util.inherits(Extract, Writable) + +Extract.prototype.destroy = function (err) { + if (this._destroyed) return + this._destroyed = true + + if (err) this.emit('error', err) + this.emit('close') + if (this._stream) this._stream.emit('close') +} + +Extract.prototype._parse = function (size, onparse) { + if (this._destroyed) return + this._offset += size + this._missing = size + if (onparse === this._onheader) this._partial = false + this._onparse = onparse +} + +Extract.prototype._continue = function () { + if (this._destroyed) return + var cb = this._cb + this._cb = noop + if (this._overflow) this._write(this._overflow, undefined, cb) + else cb() +} + +Extract.prototype._write = function (data, enc, cb) { + if (this._destroyed) return + + var s = this._stream + var b = this._buffer + var missing = this._missing + if (data.length) this._partial = true + + // we do not reach end-of-chunk now. just forward it + + if (data.length < missing) { + this._missing -= data.length + this._overflow = null + if (s) return s.write(data, cb) + b.append(data) + return cb() + } + + // end-of-chunk. the parser should call cb. + + this._cb = cb + this._missing = 0 + + var overflow = null + if (data.length > missing) { + overflow = data.slice(missing) + data = data.slice(0, missing) + } + + if (s) s.end(data) + else b.append(data) + + this._overflow = overflow + this._onparse() +} + +Extract.prototype._final = function (cb) { + if (this._partial) return this.destroy(new Error('Unexpected end of data')) + cb() +} + +module.exports = Extract diff --git a/ce/custom/tar-stream/headers.js b/ce/custom/tar-stream/headers.js new file mode 100644 index 0000000000..aba4ca49a6 --- /dev/null +++ b/ce/custom/tar-stream/headers.js @@ -0,0 +1,295 @@ +var alloc = Buffer.alloc + +var ZEROS = '0000000000000000000' +var SEVENS = '7777777777777777777' +var ZERO_OFFSET = '0'.charCodeAt(0) +var USTAR_MAGIC = Buffer.from('ustar\x00', 'binary') +var USTAR_VER = Buffer.from('00', 'binary') +var GNU_MAGIC = Buffer.from('ustar\x20', 'binary') +var GNU_VER = Buffer.from('\x20\x00', 'binary') +var MASK = parseInt('7777', 8) +var MAGIC_OFFSET = 257 +var VERSION_OFFSET = 263 + +var clamp = function (index, len, defaultValue) { + if (typeof index !== 'number') return defaultValue + index = ~~index // Coerce to integer. + if (index >= len) return len + if (index >= 0) return index + index += len + if (index >= 0) return index + return 0 +} + +var toType = function (flag) { + switch (flag) { + case 0: + return 'file' + case 1: + return 'link' + case 2: + return 'symlink' + case 3: + return 'character-device' + case 4: + return 'block-device' + case 5: + return 'directory' + case 6: + return 'fifo' + case 7: + return 'contiguous-file' + case 72: + return 'pax-header' + case 55: + return 'pax-global-header' + case 27: + return 'gnu-long-link-path' + case 28: + case 30: + return 'gnu-long-path' + } + + return null +} + +var toTypeflag = function (flag) { + switch (flag) { + case 'file': + return 0 + case 'link': + return 1 + case 'symlink': + return 2 + case 'character-device': + return 3 + case 'block-device': + return 4 + case 'directory': + return 5 + case 'fifo': + return 6 + case 'contiguous-file': + return 7 + case 'pax-header': + return 72 + } + + return 0 +} + +var indexOf = function (block, num, offset, end) { + for (; offset < end; offset++) { + if (block[offset] === num) return offset + } + return end +} + +var cksum = function (block) { + var sum = 8 * 32 + for (var i = 0; i < 148; i++) sum += block[i] + for (var j = 156; j < 512; j++) sum += block[j] + return sum +} + +var encodeOct = function (val, n) { + val = val.toString(8) + if (val.length > n) return SEVENS.slice(0, n) + ' ' + else return ZEROS.slice(0, n - val.length) + val + ' ' +} + +/* Copied from the node-tar repo and modified to meet + * tar-stream coding standard. + * + * Source: https://github.com/npm/node-tar/blob/51b6627a1f357d2eb433e7378e5f05e83b7aa6cd/lib/header.js#L349 + */ +function parse256 (buf) { + // first byte MUST be either 80 or FF + // 80 for positive, FF for 2's comp + var positive + if (buf[0] === 0x80) positive = true + else if (buf[0] === 0xFF) positive = false + else return null + + // build up a base-256 tuple from the least sig to the highest + var tuple = [] + for (var i = buf.length - 1; i > 0; i--) { + var byte = buf[i] + if (positive) tuple.push(byte) + else tuple.push(0xFF - byte) + } + + var sum = 0 + var l = tuple.length + for (i = 0; i < l; i++) { + sum += tuple[i] * Math.pow(256, i) + } + + return positive ? sum : -1 * sum +} + +var decodeOct = function (val, offset, length) { + val = val.slice(offset, offset + length) + offset = 0 + + // If prefixed with 0x80 then parse as a base-256 integer + if (val[offset] & 0x80) { + return parse256(val) + } else { + // Older versions of tar can prefix with spaces + while (offset < val.length && val[offset] === 32) offset++ + var end = clamp(indexOf(val, 32, offset, val.length), val.length, val.length) + while (offset < end && val[offset] === 0) offset++ + if (end === offset) return 0 + return parseInt(val.slice(offset, end).toString(), 8) + } +} + +var decodeStr = function (val, offset, length, encoding) { + return val.slice(offset, indexOf(val, 0, offset, offset + length)).toString(encoding) +} + +var addLength = function (str) { + var len = Buffer.byteLength(str) + var digits = Math.floor(Math.log(len) / Math.log(10)) + 1 + if (len + digits >= Math.pow(10, digits)) digits++ + + return (len + digits) + str +} + +exports.decodeLongPath = function (buf, encoding) { + return decodeStr(buf, 0, buf.length, encoding) +} + +exports.encodePax = function (opts) { // TODO: encode more stuff in pax + var result = '' + if (opts.name) result += addLength(' path=' + opts.name + '\n') + if (opts.linkname) result += addLength(' linkpath=' + opts.linkname + '\n') + var pax = opts.pax + if (pax) { + for (var key in pax) { + result += addLength(' ' + key + '=' + pax[key] + '\n') + } + } + return Buffer.from(result) +} + +exports.decodePax = function (buf) { + var result = {} + + while (buf.length) { + var i = 0 + while (i < buf.length && buf[i] !== 32) i++ + var len = parseInt(buf.slice(0, i).toString(), 10) + if (!len) return result + + var b = buf.slice(i + 1, len - 1).toString() + var keyIndex = b.indexOf('=') + if (keyIndex === -1) return result + result[b.slice(0, keyIndex)] = b.slice(keyIndex + 1) + + buf = buf.slice(len) + } + + return result +} + +exports.encode = function (opts) { + var buf = alloc(512) + var name = opts.name + var prefix = '' + + if (opts.typeflag === 5 && name[name.length - 1] !== '/') name += '/' + if (Buffer.byteLength(name) !== name.length) return null // utf-8 + + while (Buffer.byteLength(name) > 100) { + var i = name.indexOf('/') + if (i === -1) return null + prefix += prefix ? '/' + name.slice(0, i) : name.slice(0, i) + name = name.slice(i + 1) + } + + if (Buffer.byteLength(name) > 100 || Buffer.byteLength(prefix) > 155) return null + if (opts.linkname && Buffer.byteLength(opts.linkname) > 100) return null + + buf.write(name) + buf.write(encodeOct(opts.mode & MASK, 6), 100) + buf.write(encodeOct(opts.uid, 6), 108) + buf.write(encodeOct(opts.gid, 6), 116) + buf.write(encodeOct(opts.size, 11), 124) + buf.write(encodeOct((opts.mtime.getTime() / 1000) | 0, 11), 136) + + buf[156] = ZERO_OFFSET + toTypeflag(opts.type) + + if (opts.linkname) buf.write(opts.linkname, 157) + + USTAR_MAGIC.copy(buf, MAGIC_OFFSET) + USTAR_VER.copy(buf, VERSION_OFFSET) + if (opts.uname) buf.write(opts.uname, 265) + if (opts.gname) buf.write(opts.gname, 297) + buf.write(encodeOct(opts.devmajor || 0, 6), 329) + buf.write(encodeOct(opts.devminor || 0, 6), 337) + + if (prefix) buf.write(prefix, 345) + + buf.write(encodeOct(cksum(buf), 6), 148) + + return buf +} + +exports.decode = function (buf, filenameEncoding, allowUnknownFormat) { + var typeflag = buf[156] === 0 ? 0 : buf[156] - ZERO_OFFSET + + var name = decodeStr(buf, 0, 100, filenameEncoding) + var mode = decodeOct(buf, 100, 8) + var uid = decodeOct(buf, 108, 8) + var gid = decodeOct(buf, 116, 8) + var size = decodeOct(buf, 124, 12) + var mtime = decodeOct(buf, 136, 12) + var type = toType(typeflag) + var linkname = buf[157] === 0 ? null : decodeStr(buf, 157, 100, filenameEncoding) + var uname = decodeStr(buf, 265, 32) + var gname = decodeStr(buf, 297, 32) + var devmajor = decodeOct(buf, 329, 8) + var devminor = decodeOct(buf, 337, 8) + + var c = cksum(buf) + + // checksum is still initial value if header was null. + if (c === 8 * 32) return null + + // valid checksum + if (c !== decodeOct(buf, 148, 8)) throw new Error('Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?') + + if (USTAR_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0) { + // ustar (posix) format. + // prepend prefix, if present. + if (buf[345]) name = decodeStr(buf, 345, 155, filenameEncoding) + '/' + name + } else if (GNU_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0 && + GNU_VER.compare(buf, VERSION_OFFSET, VERSION_OFFSET + 2) === 0) { + // 'gnu'/'oldgnu' format. Similar to ustar, but has support for incremental and + // multi-volume tarballs. + } else { + if (!allowUnknownFormat) { + throw new Error('Invalid tar header: unknown format.') + } + } + + // to support old tar versions that use trailing / to indicate dirs + if (typeflag === 0 && name && name[name.length - 1] === '/') typeflag = 5 + + return { + name, + mode, + uid, + gid, + size, + mtime: new Date(1000 * mtime), + type, + linkname, + uname, + gname, + devmajor, + devminor + } +} diff --git a/ce/custom/tar-stream/index.js b/ce/custom/tar-stream/index.js new file mode 100644 index 0000000000..6481704827 --- /dev/null +++ b/ce/custom/tar-stream/index.js @@ -0,0 +1,2 @@ +exports.extract = require('./extract') +exports.pack = require('./pack') diff --git a/ce/custom/tar-stream/pack.js b/ce/custom/tar-stream/pack.js new file mode 100644 index 0000000000..bdce2a8a50 --- /dev/null +++ b/ce/custom/tar-stream/pack.js @@ -0,0 +1,255 @@ +var constants = require('fs-constants') +var eos = require('end-of-stream') +var inherits = require('inherits') +var alloc = Buffer.alloc + +var Readable = require('stream').Readable +var Writable = require('stream').Writable +var StringDecoder = require('string_decoder').StringDecoder + +var headers = require('./headers') + +var DMODE = parseInt('755', 8) +var FMODE = parseInt('644', 8) + +var END_OF_TAR = alloc(1024) + +var noop = function () {} + +var overflow = function (self, size) { + size &= 511 + if (size) self.push(END_OF_TAR.slice(0, 512 - size)) +} + +function modeToType (mode) { + switch (mode & constants.S_IFMT) { + case constants.S_IFBLK: return 'block-device' + case constants.S_IFCHR: return 'character-device' + case constants.S_IFDIR: return 'directory' + case constants.S_IFIFO: return 'fifo' + case constants.S_IFLNK: return 'symlink' + } + + return 'file' +} + +var Sink = function (to) { + Writable.call(this) + this.written = 0 + this._to = to + this._destroyed = false +} + +inherits(Sink, Writable) + +Sink.prototype._write = function (data, enc, cb) { + this.written += data.length + if (this._to.push(data)) return cb() + this._to._drain = cb +} + +Sink.prototype.destroy = function () { + if (this._destroyed) return + this._destroyed = true + this.emit('close') +} + +var LinkSink = function () { + Writable.call(this) + this.linkname = '' + this._decoder = new StringDecoder('utf-8') + this._destroyed = false +} + +inherits(LinkSink, Writable) + +LinkSink.prototype._write = function (data, enc, cb) { + this.linkname += this._decoder.write(data) + cb() +} + +LinkSink.prototype.destroy = function () { + if (this._destroyed) return + this._destroyed = true + this.emit('close') +} + +var Void = function () { + Writable.call(this) + this._destroyed = false +} + +inherits(Void, Writable) + +Void.prototype._write = function (data, enc, cb) { + cb(new Error('No body allowed for this entry')) +} + +Void.prototype.destroy = function () { + if (this._destroyed) return + this._destroyed = true + this.emit('close') +} + +var Pack = function (opts) { + if (!(this instanceof Pack)) return new Pack(opts) + Readable.call(this, opts) + + this._drain = noop + this._finalized = false + this._finalizing = false + this._destroyed = false + this._stream = null +} + +inherits(Pack, Readable) + +Pack.prototype.entry = function (header, buffer, callback) { + if (this._stream) throw new Error('already piping an entry') + if (this._finalized || this._destroyed) return + + if (typeof buffer === 'function') { + callback = buffer + buffer = null + } + + if (!callback) callback = noop + + var self = this + + if (!header.size || header.type === 'symlink') header.size = 0 + if (!header.type) header.type = modeToType(header.mode) + if (!header.mode) header.mode = header.type === 'directory' ? DMODE : FMODE + if (!header.uid) header.uid = 0 + if (!header.gid) header.gid = 0 + if (!header.mtime) header.mtime = new Date() + + if (typeof buffer === 'string') buffer = Buffer.from(buffer) + if (Buffer.isBuffer(buffer)) { + header.size = buffer.length + this._encode(header) + var ok = this.push(buffer) + overflow(self, header.size) + if (ok) process.nextTick(callback) + else this._drain = callback + return new Void() + } + + if (header.type === 'symlink' && !header.linkname) { + var linkSink = new LinkSink() + eos(linkSink, function (err) { + if (err) { // stream was closed + self.destroy() + return callback(err) + } + + header.linkname = linkSink.linkname + self._encode(header) + callback() + }) + + return linkSink + } + + this._encode(header) + + if (header.type !== 'file' && header.type !== 'contiguous-file') { + process.nextTick(callback) + return new Void() + } + + var sink = new Sink(this) + + this._stream = sink + + eos(sink, function (err) { + self._stream = null + + if (err) { // stream was closed + self.destroy() + return callback(err) + } + + if (sink.written !== header.size) { // corrupting tar + self.destroy() + return callback(new Error('size mismatch')) + } + + overflow(self, header.size) + if (self._finalizing) self.finalize() + callback() + }) + + return sink +} + +Pack.prototype.finalize = function () { + if (this._stream) { + this._finalizing = true + return + } + + if (this._finalized) return + this._finalized = true + this.push(END_OF_TAR) + this.push(null) +} + +Pack.prototype.destroy = function (err) { + if (this._destroyed) return + this._destroyed = true + + if (err) this.emit('error', err) + this.emit('close') + if (this._stream && this._stream.destroy) this._stream.destroy() +} + +Pack.prototype._encode = function (header) { + if (!header.pax) { + var buf = headers.encode(header) + if (buf) { + this.push(buf) + return + } + } + this._encodePax(header) +} + +Pack.prototype._encodePax = function (header) { + var paxHeader = headers.encodePax({ + name: header.name, + linkname: header.linkname, + pax: header.pax + }) + + var newHeader = { + name: 'PaxHeader', + mode: header.mode, + uid: header.uid, + gid: header.gid, + size: paxHeader.length, + mtime: header.mtime, + type: 'pax-header', + linkname: header.linkname && 'PaxHeader', + uname: header.uname, + gname: header.gname, + devmajor: header.devmajor, + devminor: header.devminor + } + + this.push(headers.encode(newHeader)) + this.push(paxHeader) + overflow(this, paxHeader.length) + + newHeader.size = header.size + newHeader.type = header.type + this.push(headers.encode(newHeader)) +} + +Pack.prototype._read = function (n) { + var drain = this._drain + this._drain = noop + drain() +} + +module.exports = Pack diff --git a/ce/custom/tar-stream/package.json b/ce/custom/tar-stream/package.json new file mode 100644 index 0000000000..7f51abac80 --- /dev/null +++ b/ce/custom/tar-stream/package.json @@ -0,0 +1,58 @@ +{ + "name": "tar-stream", + "version": "2.3.0", + "description": "tar-stream is a streaming tar parser and generator and nothing else. It is streams2 and operates purely using streams which means you can easily extract/parse tarballs without ever hitting the file system.", + "author": "Mathias Buus ", + "dependencies": { + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3" + }, + "devDependencies": { + "tape": "^4.9.2" + }, + "scripts": { + "build": "", + "clean": "", + "eslint-fix": "", + "eslint": "", + "test": "tape test/extract.js test/pack.js", + "test-all": "tape test/*.js" + }, + "keywords": [ + "tar", + "tarball", + "parse", + "parser", + "generate", + "generator", + "stream", + "stream2", + "streams", + "streams2", + "streaming", + "pack", + "extract", + "modify" + ], + "bugs": { + "url": "https://github.com/mafintosh/tar-stream/issues" + }, + "homepage": "https://github.com/mafintosh/tar-stream", + "main": "index.js", + "files": [ + "*.js", + "LICENSE" + ], + "directories": { + "test": "test" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/mafintosh/tar-stream.git" + }, + "engines": { + "node": ">=6" + } +} \ No newline at end of file diff --git a/ce/getting-started.md b/ce/getting-started.md new file mode 100644 index 0000000000..57e2a28b67 --- /dev/null +++ b/ce/getting-started.md @@ -0,0 +1,84 @@ +# Build Instructions + +## Prerequisites: +1. [Node.js](https://nodejs.org/en/download/) (>=15.10.0) -- We suggest using [NVS](https://github.com/jasongin/nvs) + +2. [Rush](https://rushjs.io/pages/intro/welcome/) + +## VSCode extensions: + - `ESLint` + - `Mocha Test Explorer` + - `Test Explorer UI` + + +``` bash +npm install -g @microsoft/rush +``` + +## Preparation +The first time (or after pulling from upstream) use Rush to install modules packages. +``` bash +rush update +``` + +## Building code +- `rush rebuild` to build the modules +or +- `rush watch` to setup the build watcher +or +- in VSCode, just build (ctrl-shift-b) + +## Important Rush commands + +### `rush update` +Ensures that all the dependencies are installed for all the projects. +Safe to run mulitple times. +If you modify a `package.json` file you should re-run this. (or merge from upstream when `package.json` is updated) + +### `rush purge` +Cleans out all the installed dependencies. +If you run this, you must run `rush update` before building. + +### `rush rebuild` +Rebuilds all the projects from scratch + +### `rush watch` +Runs the typescript compiler in `--watch` mode to watch for modified files on disk. + +### `rush test` +Runs `npm test` on each project (does not build first -- see `rush test-ci`) + +### `rush test-ci` +Runs `npm test-ci` on each project (rebuilds the code then runs the tests) + +### `rush clean` +Runs `npm clean` on each project -- cleans out the `./dist` folder for each project. +Needs to have packages installed, so it can only be run after `rush update` + +### `rush lint` +Runs `npm lint` on each project (runs the eslint on the source) + +### `rush fix` +Runs `npm eslint-fix` on each project (fixes all fixable linter errors) + +### `rush set-versions` +This will set the `x.x.build` verison for each project based on the commit number. + +### `rush sync-versions` +This will ensure that all the projects have consistent versions for all dependencies, and ensures that cross-project version references are set correctly + + + + +# Building a release + +To create the final npm tgz file we're going to statically include our dependencies so that we're not having to pull them down dynamically. + +using `rush deploy` will generate this in `./common/deploy` which will copy all the necessary runtime components, and the contents of the `./assets/` folder (which contains the `package.json` and scripts to build/install the cli correctly. ) + +``` bash +npx rush update # install the packages for the project +npx rush rebuild # build the typescript files +npx rush deploy --overwrite # create the common/deploy folder with all the contents includeing ./assets +npm pack ./common/deploy # pack it up +``` \ No newline at end of file diff --git a/ce/projects.tsconfig.json b/ce/projects.tsconfig.json new file mode 100644 index 0000000000..e2542ea760 --- /dev/null +++ b/ce/projects.tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "ce/tsconfig.json" + }, + { + "path": "test/tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/ce/rush.json b/ce/rush.json new file mode 100644 index 0000000000..af3c61074e --- /dev/null +++ b/ce/rush.json @@ -0,0 +1,66 @@ +/** + * This is the main configuration file for Rush. + * For full documentation, please see https://rushjs.io + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json", + "rushVersion": "5.61.4", + "pnpmVersion": "6.29.1", + /** + * Options that are only used when the PNPM package manager is selected + */ + "pnpmOptions": {}, + "nodeSupportedVersionRange": ">=14.17.0 <17.0.0", + "suppressNodeLtsWarning": true, + "projectFolderMinDepth": 1, + "projectFolderMaxDepth": 5, + "gitPolicy": {}, + "repository": { + "url": "https://github.com/microsoft/vcpkg-tool" + }, + /** + * Event hooks are customized script actions that Rush executes when specific events occur + */ + "eventHooks": { + /** + * The list of shell commands to run before the Rush installation starts + */ + "preRushInstall": [ + // "common/scripts/pre-rush-install.js" + ], + /** + * The list of shell commands to run after the Rush installation finishes + */ + "postRushInstall": [], + /** + * The list of shell commands to run before the Rush build command starts + */ + "preRushBuild": [], + /** + * The list of shell commands to run after the Rush build command finishes + */ + "postRushBuild": [] + }, + "variants": [], + "telemetryEnabled": false, + "projects": [ + { + "packageName": "@microsoft/vcpkg-ce", + "projectFolder": "./ce", + "reviewCategory": "production", + "shouldPublish": false + }, + { + "packageName": "vcpkg-ce.test", + "projectFolder": "./test", + "reviewCategory": "production", + "shouldPublish": false + }, + { + "packageName": "tar-stream", + "projectFolder": "./custom/tar-stream", + "reviewCategory": "production", + "shouldPublish": false + } + ] +} diff --git a/ce/test/.eslintignore b/ce/test/.eslintignore new file mode 100644 index 0000000000..cc6a7fd139 --- /dev/null +++ b/ce/test/.eslintignore @@ -0,0 +1,3 @@ +**/*.d.ts +test/scenarios/** +dist/** diff --git a/ce/test/.eslintrc.yaml b/ce/test/.eslintrc.yaml new file mode 100644 index 0000000000..5198e2a1ce --- /dev/null +++ b/ce/test/.eslintrc.yaml @@ -0,0 +1,10 @@ +--- +# configure plugins first +parser: "@typescript-eslint/parser" +plugins: +- "@typescript-eslint" +- "notice" + +# then inherit the common settings +extends: +- "../common/.default-eslintrc.yaml" \ No newline at end of file diff --git a/ce/test/.npmignore b/ce/test/.npmignore new file mode 100644 index 0000000000..33986ae1f9 --- /dev/null +++ b/ce/test/.npmignore @@ -0,0 +1,20 @@ +!dist/**/* +src/ +dist/test/ +package/ +.npmignore +tsconfig.json +*.ts +changelog.md +.eslint* +!*.d.ts +*.tgz +.vscode +.scripts +attic/ +generated/ +notes.md +Examples/ +samples/ +*.log +package-deps.json diff --git a/ce/test/LICENSE b/ce/test/LICENSE new file mode 100644 index 0000000000..5cf7c8db62 --- /dev/null +++ b/ce/test/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +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 diff --git a/ce/test/core/SuiteLocal.ts b/ce/test/core/SuiteLocal.ts new file mode 100644 index 0000000000..3152530d8d --- /dev/null +++ b/ce/test/core/SuiteLocal.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { LocalFileSystem } from '@microsoft/vcpkg-ce/dist/fs/local-filesystem'; +import { Session } from '@microsoft/vcpkg-ce/dist/session'; +import { Uri } from '@microsoft/vcpkg-ce/dist/util/uri'; +import { strict } from 'assert'; +import { statSync } from 'fs'; +import { rm } from 'fs/promises'; +import { join, resolve } from 'path'; +import { uniqueTempFolder } from './uniqueTempFolder'; + + +require('@microsoft/vcpkg-ce/dist/exports'); + +export function rootFolder(from = __dirname): string { + try { + const resources = join(from, 'resources'); + const s = statSync(resources); + s.isDirectory(); + return from; + } + catch { + // shh! + } + const up = resolve(from, '..'); + strict.notEqual(up, from, 'O_o unable to find root folder'); + return rootFolder(up); +} + +export class SuiteLocal { + readonly tempFolder = uniqueTempFolder(); + readonly session: Session; + readonly fs: LocalFileSystem; + readonly rootFolder: string = rootFolder(); + readonly resourcesFolder = this.rootFolder + '/resources'; + readonly rootFolderUri: Uri; + readonly tempFolderUri: Uri; + readonly resourcesFolderUri: Uri; + + constructor() { + this.tempFolder = uniqueTempFolder(); + this.session = new Session(this.tempFolder, {}, { + homeFolder: join(this.tempFolder, 'vcpkg_root'), + }, {}); + + this.fs = new LocalFileSystem(this.session); + this.rootFolderUri = this.fs.file(this.rootFolder); + this.tempFolderUri = this.fs.file(this.tempFolder); + this.resourcesFolderUri = this.fs.file(this.resourcesFolder); + // set the debug=1 in the environment to have the debug messages dumped during testing + if (process.env['DEBUG'] || process.env['debug']) { + this.session.channels.on('debug', (text, context, msec) => { + SuiteLocal.log(`[${msec}msec] ${text}`); + }); + } + } + + async after() { + await rm(this.tempFolder, { recursive: true }); + } + static log(args: any) { + console['log'](args); + } +} diff --git a/ce/test/core/acquire-tests.ts b/ce/test/core/acquire-tests.ts new file mode 100644 index 0000000000..444ca59316 --- /dev/null +++ b/ce/test/core/acquire-tests.ts @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { acquireArtifactFile, resolveNugetUrl } from '@microsoft/vcpkg-ce/dist/fs/acquire'; +import { strict } from 'assert'; +import { SuiteLocal } from './SuiteLocal'; + +describe('Acquire', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + + it('try some downloads', async () => { + + const remoteFile = local.session.parseUri('https://raw.githubusercontent.com/microsoft/vscode/main/README.md'); + + let acq = acquireArtifactFile(local.session, [remoteFile], 'readme.md', {}); + + const outputFile = await acq; + + strict.ok(await outputFile.exists(), 'File should exist!'); + const size = await outputFile.size(); + // let's try some resume scenarios + + // chopped file, very small. + // let's chop the file in half + const fullFile = await outputFile.readFile(); + const halfFile = fullFile.slice(0, fullFile.length / 2); + + await outputFile.delete(); + await outputFile.writeFile(halfFile); + + local.session.channels.debug('==== chopped the file in half, redownload'); + + acq = acquireArtifactFile(local.session, [remoteFile], 'readme.md', {}); + await acq; + const newsize = await outputFile.size(); + strict.equal(newsize, size, 'the file should be the right size at the end'); + + }); + + + it('larger file', async () => { + const remoteFile = local.session.parseUri('https://user-images.githubusercontent.com/1487073/58344409-70473b80-7e0a-11e9-8570-b2efc6f8fa44.png'); + + let acq = acquireArtifactFile(local.session, [remoteFile], 'xyz.png', {}); + + const outputFile = await acq; + + const fullSize = await outputFile.size(); + + strict.ok(await outputFile.exists(), 'File should exist!'); + strict.ok(fullSize > 1 << 16, 'Should be at least 64k'); + + const size = await outputFile.size(); + + + // try getting the same file again (so, should hit the cache.) + local.session.channels.debug('==== get the same large file again. should hit cache'); + await acquireArtifactFile(local.session, [remoteFile], 'xyz.png', {}); + + local.session.channels.debug('==== was that ok?'); + + // chopped file, big. + // let's chop the file in half + const fullFile = await outputFile.readFile(); + const halfFile = fullFile.slice(0, fullFile.length / 2); + + await outputFile.delete(); + await outputFile.writeFile(halfFile); + + local.session.channels.debug('==== chopped the large file in half, should resume'); + acq = acquireArtifactFile(local.session, [remoteFile], 'xyz.png', {}); + + await acq; + const newsize = await outputFile.size(); + strict.equal(newsize, size, 'the file should be the right size at the end'); + + const newfull = (await outputFile.readFile()); + strict.equal(newfull.compare(fullFile), 0, 'files should be identical'); + }); + + /** + * The NuGet gallery servers don't do redirects on HEAD requests, and to work around it we have to issue a second GET + * for each HEAD, after the HEAD fails, which increases the overhead of getting the target file (or verifying that we have it.) + * + * I've made the test call resolve redirects up front, which did reduce the cost, so... it's about as fast as I can make it. + * (~400msec for the whole test, which ain't terrible.) + * + * The same thing can be accomplished by the all-encompassing nuget() call, but the test suffers if I use that directly, since we're + * calling for the same package multple times. 🤷 + */ + it('Download a nuget file', async () => { + const url = await resolveNugetUrl(local.session, 'zlib-msvc14-x64/1.2.11.7795'); + + local.session.channels.debug('==== Downloading nuget package'); + + const acq = acquireArtifactFile(local.session, [url], 'zlib-msvc.zip', {}); + // or const acq = nuget(local.session, 'zlib-msvc14-x64/1.2.11.7795', 'zlib-msvc.zip'); + + const outputFile = await acq; + local.session.channels.debug('==== done downloading'); + const fullSize = await outputFile.size(); + + strict.ok(await outputFile.exists(), 'File should exist!'); + strict.ok(fullSize > 1 << 16, 'Should be at least 64k'); + + const size = await outputFile.size(); + local.session.channels.debug(`==== Size: ${size}`); + + // what happens if we try again? We should hit our local cache + await acquireArtifactFile(local.session, [url], 'zlib-msvc.zip', {}); + + }); + +}); diff --git a/ce/test/core/amf-tests.ts b/ce/test/core/amf-tests.ts new file mode 100644 index 0000000000..9c0ccc82f3 --- /dev/null +++ b/ce/test/core/amf-tests.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { MetadataFile } from '@microsoft/vcpkg-ce/dist/amf/metadata-file'; +import { strict } from 'assert'; +import { readFile } from 'fs/promises'; +import { join } from 'path'; +import * as s from '../sequence-equal'; +import { rootFolder, SuiteLocal } from './SuiteLocal'; + +// forces the global function for sequence equal to be added to strict before this exectues: +s; + +// sample test using decorators. +describe('Amf', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + + it('readProfile', async () => { + const content = await (await readFile(join(rootFolder(), 'resources', 'sample1.yaml'))).toString('utf-8'); + const doc = await MetadataFile.parseConfiguration('./sample1.yaml', content, local.session); + + strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); + strict.ok(doc.isValid, 'Is it valid?'); + + strict.equal(doc.info.id, 'sample1', 'identity incorrect'); + strict.equal(doc.info.version, '1.2.3', 'version incorrect'); + }); + + it('reads file with nupkg', async () => { + const content = await (await readFile(join(rootFolder(), 'resources', 'repo', 'sdks', 'microsoft', 'windows.yaml'))).toString('utf-8'); + const doc = await MetadataFile.parseConfiguration('./windows.yaml', content, local.session); + + strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); + strict.ok(doc.isValid, 'Is it valid?'); + + SuiteLocal.log(doc.content); + }); + + it('load/persist environment.yaml', async () => { + const content = await (await readFile(join(rootFolder(), 'resources', 'environment.yaml'))).toString('utf-8'); + const doc = await MetadataFile.parseConfiguration('./cenvironment.yaml', content, local.session); + + SuiteLocal.log(doc.content); + for (const each of doc.validationErrors) { + SuiteLocal.log(each); + } + + strict.ok(doc.isFormatValid, 'Ensure it\'s valid yaml'); + strict.ok(doc.isValid, 'better be valid!'); + + SuiteLocal.log(doc.content); + }); + + it('profile checks', async () => { + const content = await (await readFile(join(rootFolder(), 'resources', 'sample1.yaml'))).toString('utf-8'); + const doc = await MetadataFile.parseConfiguration('./sample1.yaml', content, local.session); + + strict.ok(doc.isFormatValid, 'Ensure it\'s valid yaml'); + SuiteLocal.log(doc.validationErrors); + strict.ok(doc.isValid, 'better be valid!'); + + // fixme: validate inputs again. + // strict.throws(() => doc.info.version = '4.1', 'Setting invalid version should throw'); + // strict.equal(doc.info.version = '4.1.0', '4.1.0', 'Version should set correctly'); + + SuiteLocal.log(doc.contacts.get('Bob Smith')); + + strict.sequenceEqual(doc.contacts.get('Bob Smith')!.roles, ['fallguy', 'otherguy'], 'Should return the two roles'); + doc.contacts.get('Bob Smith')!.roles.delete('fallguy'); + + strict.sequenceEqual(doc.contacts.get('Bob Smith')!.roles, ['otherguy'], 'Should return the remaining role'); + + doc.contacts.get('Bob Smith')!.roles.add('the dude'); + + doc.contacts.get('Bob Smith')!.roles.add('the dude'); // shouldn't add this one + + strict.sequenceEqual(doc.contacts.get('Bob Smith')!.roles, ['otherguy', 'the dude'], 'Should return only two roles'); + + const k = doc.contacts.add('James Brown'); + SuiteLocal.log(doc.contacts.keys); + + k.email = 'jim@contoso.net'; + SuiteLocal.log(doc.contacts.keys); + + strict.equal(doc.contacts.keys.length, 3, 'Should have 3 contacts'); + + doc.contacts.delete('James Brown'); + + + strict.equal(doc.contacts.keys.length, 2, 'Should have 2 contacts'); + + doc.contacts.delete('James Brown'); // this is ok. + + // version can be coerced to be a string (via tostring) + SuiteLocal.log(doc.requires.get('foo/bar/bin')?.raw); + strict.equal(doc.requires.get('foo/bar/bin')?.raw == '~2.0.0', true, 'Version must match'); + + // can we get the normalized range? + strict.equal(doc.requires.get('foo/bar/bin')!.range.range, '>=2.0.0 <2.1.0-0', 'The canonical ranges should match'); + + // no resolved version means undefined. + strict.equal(doc.requires.get('foo/bar/bin')!.resolved, undefined, 'Version must match'); + + // the setter is actually smart enough, but typescript does not allow heterogeneous accessors (yet! https://github.com/microsoft/TypeScript/issues/2521) + doc.requires.set('just/a/version', '1.2.3'); + strict.equal(doc.requires.get('just/a/version')!.raw, '1.2.3', 'Should be a static version range'); + + // set it with a struct + doc.requires.set('range/with/resolved', { range: '1.*', resolved: '1.0.0' }); + strict.equal(doc.requires.get('range/with/resolved')!.raw, '1.* 1.0.0'); + + strict.equal(doc.settings.tools.get('CC'), 'foo/bar/cl.exe', 'should have a value'); + strict.equal(doc.settings.tools.get('CXX'), 'bin/baz/cl.exe', 'should have a value'); + strict.equal(doc.settings.tools.get('Whatever'), 'some/tool/path/foo', 'should have a value'); + + doc.settings.tools.delete('CXX'); + strict.equal(doc.settings.tools.keys.length, 2, 'should only have two tools now'); + + strict.sequenceEqual(doc.settings.variables.get('test'), ['abc'], 'variables should be an array'); + strict.sequenceEqual(doc.settings.variables.get('cxxflags'), ['foo=bar', 'bar=baz'], 'variables should be an array'); + + doc.settings.variables.add('test').add('another value'); + strict.sequenceEqual(doc.settings.variables.get('test'), ['abc', 'another value'], 'variables should be an array of two items now'); + + doc.settings.paths.add('bin').add('hello/there'); + strict.deepEqual(doc.settings.paths.get('bin')?.length, 3, 'there should be three paths in bin now'); + + strict.sequenceEqual(doc.conditionalDemands.keys, ['windows and arm'], 'should have one conditional demand'); + /* + const install = doc.get('windows and arm').install[0]; + + strict.ok(isNupkg(install), 'the install type should be nupkg'); + strict.equal((install).location, 'floobaloo/1.2.3', 'should have correct location'); +*/ + SuiteLocal.log(doc.toString()); + }); + + it('read invalid yaml file', async () => { + const content = await (await readFile(join(rootFolder(), 'resources', 'errors.yaml'))).toString('utf-8'); + const doc = await MetadataFile.parseConfiguration('./errors.yaml', content, local.session); + + strict.equal(doc.isFormatValid, false, 'this document should have errors'); + strict.equal(doc.formatErrors.length, 2, 'This document should have two error'); + + strict.equal(doc.info.id, 'bob', 'identity incorrect'); + strict.equal(doc.info.version, '1.0.2', 'version incorrect'); + }); + + it('read empty yaml file', async () => { + const content = await (await readFile(join(rootFolder(), 'resources', 'empty.yaml'))).toString('utf-8'); + const doc = await MetadataFile.parseConfiguration('./empty.yaml', content, local.session); + + strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); + + strict.equal(doc.isValid, false, 'Should have some validation errors'); + strict.equal(doc.validationErrors[0], './empty.yaml:1:1 SectionMessing, Missing section \'info\'', 'Should have an error about info'); + }); + + it('validation errors', async () => { + const content = await (await readFile(join(rootFolder(), 'resources', 'validation-errors.yaml'))).toString('utf-8'); + const doc = await MetadataFile.parseConfiguration('./validation-errors.yaml', content, local.session); + + strict.ok(doc.isFormatValid, 'Ensure it is valid yaml'); + + SuiteLocal.log(doc.validationErrors); + strict.equal(doc.validationErrors.length, 2, `Expecting two errors, found: ${JSON.stringify(doc.validationErrors, null, 2)}`); + }); +}); diff --git a/ce/test/core/archive-tests.ts b/ce/test/core/archive-tests.ts new file mode 100644 index 0000000000..fb3f04708b --- /dev/null +++ b/ce/test/core/archive-tests.ts @@ -0,0 +1,430 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { TarBzUnpacker, TarGzUnpacker, TarUnpacker } from '@microsoft/vcpkg-ce/dist/archivers/tar'; +import { FileEntry, Unpacker } from '@microsoft/vcpkg-ce/dist/archivers/unpacker'; +import { ZipUnpacker } from '@microsoft/vcpkg-ce/dist/archivers/ZipUnpacker'; +import { Uri } from '@microsoft/vcpkg-ce/dist/util/uri'; +import { rejects, strict } from 'assert'; +import { SuiteLocal } from './SuiteLocal'; + +const isWindows = process.platform === 'win32'; + +describe('Unpacker', () => { + it('StripsPaths', () => { + ['', '/'].forEach((prefix) => { + ['', '/'].forEach((suffix) => { + const d = prefix + 'delta' + suffix; + const cd = prefix + 'charlie/delta' + suffix; + const bcd = prefix + 'beta/charlie/delta' + suffix; + const abcd = prefix + 'alpha/beta/charlie/delta' + suffix; + strict.equal(Unpacker.stripPath(abcd, 0), abcd); + strict.equal(Unpacker.stripPath(abcd, 1), bcd); + strict.equal(Unpacker.stripPath(abcd, 2), cd); + strict.equal(Unpacker.stripPath(abcd, 3), d); + strict.equal(Unpacker.stripPath(abcd, 4), undefined); + + strict.equal(Unpacker.stripPath(prefix + 'some///slashes\\\\\\\\here' + suffix, 0), prefix + 'some/slashes/here' + suffix); + }); + }); + }); +}); + +/** Checks that progress delivers 0, 100, and constantly increasing percentages. */ +class PercentageChecker { + seenZero = false; + lastSeen: number | undefined = undefined; + recordPercent(percentage: number) { + if (percentage === 0) { + this.seenZero = true; + } + + if (this.lastSeen !== undefined) { + strict.ok(percentage >= this.lastSeen, `${percentage} vs ${this.lastSeen}`); + } + + this.lastSeen = percentage; + } + + test() { + strict.equal(this.lastSeen, 100); + } + + testRequireZero() { + strict.ok(this.seenZero); + } + + reset() { + this.seenZero = false; + this.lastSeen = undefined; + } +} + +class ProgressCheckerEntry { + seenZero = false; + seenUnpacked = false; + filePercentage = new PercentageChecker(); + + constructor(public entryPath: string, public entryIdentity: any) { } + + onProgress(entry: any, filePercentage: number) { + strict.equal(this.entryIdentity, entry); + if (filePercentage === 0) { + this.seenZero = true; + } + + this.filePercentage.recordPercent(filePercentage); + } + + onUnpacked(entry: any) { + strict.equal(this.entryIdentity, entry); + this.seenUnpacked = true; + } + + test() { + strict.ok(this.seenUnpacked, 'Should have got an unpacked message'); + strict.ok(this.seenZero, 'Should have seen a zero progress'); + this.filePercentage.testRequireZero(); + } +} + +class ProgressChecker { + seenEntries = new Map(); + archivePercentage = new PercentageChecker(); + + onFileProgress(entry: any, filePercentage: number) { + let checkerEntry = this.seenEntries.get(entry.path); + if (!checkerEntry) { + checkerEntry = new ProgressCheckerEntry(entry.path, entry); + this.seenEntries.set(entry.path, checkerEntry); + } + + checkerEntry.onProgress(entry, filePercentage); + } + + onProgress(archivePercentage: number) { + this.archivePercentage.recordPercent(archivePercentage); + } + + onUnpacked(entry: FileEntry) { + const checkerEntry = this.seenEntries.get(entry.path); + strict.ok(checkerEntry, `Did not find unpack progress entries for ${entry.path}`); + checkerEntry.onUnpacked(entry); + } + + reset() { + this.seenEntries.clear(); + this.archivePercentage.reset(); + } + + test(entryCount: number) { + strict.equal(entryCount, this.seenEntries.size, `Should have unpacked ${entryCount}, actually unpacked ${this.seenEntries.size}`); + this.seenEntries.forEach((value) => value.test()); + this.archivePercentage.test(); + } +} + +describe('ZipUnpacker', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + const unpacker = new ZipUnpacker(local.session); + const progressChecker = new ProgressChecker(); + unpacker.on('progress', progressChecker.onProgress.bind(progressChecker)); + unpacker.on('fileProgress', progressChecker.onFileProgress.bind(progressChecker)); + unpacker.on('unpacked', progressChecker.onUnpacked.bind(progressChecker)); + it('UnpacksLegitimateSmallZips', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example'); + await unpacker.unpack(zipUri, targetUri, {}, {}); + strict.equal((await targetUri.readFile('a.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.stat('a.txt')).mtime, Date.parse('2021-03-23T09:31:14.000Z')); + strict.equal((await targetUri.readFile('b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('only-not-directory.txt')).toString(), + 'This content is only not in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/a.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/only-directory.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/inner/only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); + progressChecker.test(9); + }); + + it('Truncates', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example-truncates'); + await unpacker.unpack(zipUri, targetUri, {}, {}); + progressChecker.reset(); + await unpacker.unpack(zipUri, targetUri, {}, {}); // intentionally doubled + strict.equal((await targetUri.readFile('a.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('only-not-directory.txt')).toString(), + 'This content is only not in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/a.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/only-directory.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/inner/only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); + progressChecker.test(9); + }); + + it('UnpacksZipsWithCompression', async () => { + // big-compression.zip is an example input from yauzl: + // https://github.com/thejoshwolfe/yauzl/blob/96f0eb552c560632a754ae0e1701a7edacbda389/test/big-compression.zip + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('big-compression.zip'); + const targetUri = local.tempFolderUri.join('big-compression'); + await unpacker.unpack(zipUri, targetUri, {}, {}); + const contents = await targetUri.readFile('0x100000'); + strict.equal(contents.length, 0x100000); + strict.ok(contents.every((value: number) => value === 0x0)); + progressChecker.test(1); + }); + + it('FailsToUnpackMalformed', async () => { + // wrong-entry-sizes.zip is an example input from yauzl: + // https://github.com/thejoshwolfe/yauzl/blob/96f0eb552c560632a754ae0e1701a7edacbda389/test/wrong-entry-sizes/wrong-entry-sizes.zip + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('wrong-entry-sizes.zip'); + const targetUri = local.tempFolderUri.join('wrong-entry-sizes'); + await rejects(unpacker.unpack(zipUri, targetUri, {}, {})); + }); + + it('Strips1', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example-strip-1'); + await unpacker.unpack(zipUri, targetUri, {}, { strip: 1 }); + strict.equal((await targetUri.readFile('a.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('only-directory.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('inner/only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); + progressChecker.test(5); + }); + + it('Strips2', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example-strip-2'); + await unpacker.unpack(zipUri, targetUri, {}, { strip: 2 }); + strict.equal((await targetUri.readFile('only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); + progressChecker.test(1); + }); + + it('StripsAll', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example-strip-all'); + await unpacker.unpack(zipUri, targetUri, {}, { strip: 3 }); + strict.ok(!await targetUri.exists()); + progressChecker.test(0); + }); + + it('TransformsOne', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example-transform-one'); + await unpacker.unpack(zipUri, targetUri, {}, { transform: ['s/a\\.txt/ehh.txt/'] }); + strict.equal((await targetUri.readFile('ehh.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('only-not-directory.txt')).toString(), + 'This content is only not in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/ehh.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/only-directory.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/inner/only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); + progressChecker.test(9); + }); + + it('TransformsArray', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example-transform-array'); + await unpacker.unpack(zipUri, targetUri, {}, { + transform: [ + 's/a\\.txt/ehh.txt/', + 's/c\\.txt/see.txt/', + 's/see\\.txt/seeee.txt/', + 's/directory//g', + ] + }); + strict.equal((await targetUri.readFile('ehh.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('seeee.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('only-not-.txt')).toString(), + 'This content is only not in the directory.\n'); + strict.equal((await targetUri.readFile('a-/ehh.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('a-/b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('a-/seeee.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('a-/only-.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('a-/inner/only--.txt')).toString(), + 'This content is only doubly nested.\n'); + progressChecker.test(9); + }); + + it('StripsThenTransforms', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example-strip-then-transform'); + await unpacker.unpack(zipUri, targetUri, {}, { strip: 1, transform: ['s/b/beeee/'] }); + strict.equal((await targetUri.readFile('a.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('beeee.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('only-directory.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('inner/only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); + progressChecker.test(5); + }); + + it('AllowsTransformToNotExtract', async () => { + progressChecker.reset(); + const zipUri = local.resourcesFolderUri.join('example-zip.zip'); + const targetUri = local.tempFolderUri.join('example-transform-no-extract'); + await unpacker.unpack(zipUri, targetUri, {}, { transform: ['s/.+a.txt$//'] }); + strict.equal((await targetUri.readFile('b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('only-not-directory.txt')).toString(), + 'This content is only not in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/c.txt')).toString(), 'The contents of c.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/only-directory.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/inner/only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); + progressChecker.test(8); + }); +}); + +async function checkExtractedTar(targetUri: Uri): Promise { + strict.equal((await targetUri.readFile('a.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.stat('a.txt')).mtime, Date.parse('2021-03-23T09:31:14.000Z')); + strict.equal((await targetUri.readFile('b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('executable.sh')).toString(), '#/bin/sh\necho "Hello world!"\n\n'); + if (!isWindows) { + // executable must be executable + const execStat = await targetUri.stat('executable.sh'); + strict.ok((execStat.mode & 0o111) !== 0); + } + strict.equal((await targetUri.readFile('only-not-directory.txt')).toString(), + 'This content is only not in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/a.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('a-directory/only-directory.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('a-directory/inner/only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); +} + +const transformedTarUnpackOptions = { + strip: 1, + transform: ['s/a\\.txt/ehh\\.txt/'] +}; + +async function checkExtractedTransformedTar(targetUri: Uri): Promise { + strict.equal((await targetUri.readFile('ehh.txt')).toString(), 'The contents of a.txt.\n'); + strict.equal((await targetUri.readFile('b.txt')).toString(), 'The contents of b.txt.\n'); + strict.equal((await targetUri.readFile('only-directory.txt')).toString(), + 'This content is only in the directory.\n'); + strict.equal((await targetUri.readFile('inner/only-directory-directory.txt')).toString(), + 'This content is only doubly nested.\n'); +} + +describe('TarUnpacker', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + const unpacker = new TarUnpacker(local.session); + const progressChecker = new ProgressChecker(); + unpacker.on('progress', progressChecker.onProgress.bind(progressChecker)); + unpacker.on('fileProgress', progressChecker.onFileProgress.bind(progressChecker)); + unpacker.on('unpacked', progressChecker.onUnpacked.bind(progressChecker)); + const archiveUri = local.resourcesFolderUri.join('example-tar.tar'); + it('UnpacksLegitimateSmallTar', async () => { + progressChecker.reset(); + const targetUri = local.tempFolderUri.join('example-tar'); + await unpacker.unpack(archiveUri, targetUri, {}, {}); + await checkExtractedTar(targetUri); + progressChecker.test(8); + }); + it('ImplementsUnpackOptions', async () => { + progressChecker.reset(); + const targetUri = local.tempFolderUri.join('example-tar-transformed'); + await unpacker.unpack(archiveUri, targetUri, {}, transformedTarUnpackOptions); + await checkExtractedTransformedTar(targetUri); + progressChecker.test(4); + }); +}); + +describe('TarBzUnpacker', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + const unpacker = new TarBzUnpacker(local.session); + const progressChecker = new ProgressChecker(); + unpacker.on('progress', progressChecker.onProgress.bind(progressChecker)); + unpacker.on('fileProgress', progressChecker.onFileProgress.bind(progressChecker)); + unpacker.on('unpacked', progressChecker.onUnpacked.bind(progressChecker)); + const archiveUri = local.resourcesFolderUri.join('example-tar.tar.bz2'); + it('UnpacksLegitimateSmallTarBz', async () => { + progressChecker.reset(); + const targetUri = local.tempFolderUri.join('example-tar-bz'); + await unpacker.unpack(archiveUri, targetUri, {}, {}); + await checkExtractedTar(targetUri); + progressChecker.test(8); + }); + it('ImplementsUnpackOptions', async () => { + progressChecker.reset(); + const targetUri = local.tempFolderUri.join('example-tar-bz2-transformed'); + await unpacker.unpack(archiveUri, targetUri, {}, transformedTarUnpackOptions); + await checkExtractedTransformedTar(targetUri); + progressChecker.test(4); + }); +}); + +describe('TarGzUnpacker', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + const unpacker = new TarGzUnpacker(local.session); + const progressChecker = new ProgressChecker(); + unpacker.on('progress', progressChecker.onProgress.bind(progressChecker)); + unpacker.on('fileProgress', progressChecker.onFileProgress.bind(progressChecker)); + unpacker.on('unpacked', progressChecker.onUnpacked.bind(progressChecker)); + const archiveUri = local.resourcesFolderUri.join('example-tar.tar.gz'); + it('UnpacksLegitimateSmallTarGz', async () => { + progressChecker.reset(); + const targetUri = local.tempFolderUri.join('example-tar-gz'); + await unpacker.unpack(archiveUri, targetUri, {}, {}); + await checkExtractedTar(targetUri); + progressChecker.test(8); + }); + it('ImplementsUnpackOptions', async () => { + progressChecker.reset(); + const targetUri = local.tempFolderUri.join('example-tar-gz-transformed'); + await unpacker.unpack(archiveUri, targetUri, {}, transformedTarUnpackOptions); + await checkExtractedTransformedTar(targetUri); + progressChecker.test(4); + }); +}); diff --git a/ce/test/core/evaluator.ts b/ce/test/core/evaluator.ts new file mode 100644 index 0000000000..74a5e76b63 --- /dev/null +++ b/ce/test/core/evaluator.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Activation } from '@microsoft/vcpkg-ce/dist/artifacts/activation'; +import { Evaluator } from '@microsoft/vcpkg-ce/dist/util/evaluator'; +import { linq } from '@microsoft/vcpkg-ce/dist/util/linq'; +import { equalsIgnoreCase } from '@microsoft/vcpkg-ce/dist/util/text'; +import { strict, strictEqual } from 'assert'; +import { SuiteLocal } from './SuiteLocal'; + +describe('Evaluator', () => { + const local = new SuiteLocal(); + const fs = local.fs; + const session = local.session; + + after(local.after.bind(local)); + + it('evaluates', () => { + const activation = new Activation(session); + activation.environment.set('foo', ['bar']); + const e = new Evaluator({ $0: 'c:/foo/bar/python.exe' }, process.env, activation.output); + + // handle expressions that use the artifact data + strictEqual(e.evaluate('$0'), 'c:/foo/bar/python.exe', 'Should return $0 from artifact data'); + + // unmatched variables should be passed thru + strictEqual(e.evaluate('$1'), '$1', 'items with no value are not replaced'); + + // handle expressions that use the environment + const pathVar = linq.keys(process.env).first(each => equalsIgnoreCase(each, 'path')); + strict(pathVar); + strictEqual(e.evaluate(`$host.${pathVar}`), process.env[pathVar], 'Should be able to get environment variables from host'); + + // handle expressions that use the activation's output + strictEqual(e.evaluate('$environment.foo'), 'bar', 'Should be able to get environment variables from activation'); + }); +}); \ No newline at end of file diff --git a/ce/test/core/http-filesystem-tests.ts b/ce/test/core/http-filesystem-tests.ts new file mode 100644 index 0000000000..da92a48ad2 --- /dev/null +++ b/ce/test/core/http-filesystem-tests.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { FileType } from '@microsoft/vcpkg-ce/dist/fs/filesystem'; +import { HttpsFileSystem } from '@microsoft/vcpkg-ce/dist/fs/http-filesystem'; +import { fail, strict } from 'assert'; +import { SuiteLocal } from './SuiteLocal'; + + +describe('HttpFileSystemTests', () => { + const local = new SuiteLocal(); + + after(local.after.bind(local)); + const fs = new HttpsFileSystem(local.session); + + it('stat a file', async () => { + + const uri = fs.parse('https://aka.ms/vcpkg-ce.version'); + const s = await fs.stat(uri); + strict.equal(s.type, FileType.File, 'Should be a file'); + strict.ok(s.size < 40, 'should be less than 40 bytes'); + strict.ok(s.size > 20, 'should be more than 20 bytes'); + + }); + + it('stat a non existant file', async () => { + try { + const uri = fs.parse('https://file.not.found/blabla'); + const s = await fs.stat(uri); + } catch { + return; + } + fail('Should have thrown'); + }); + + it('read a stream', async () => { + const uri = fs.parse('https://aka.ms/vcpkg-ce.version'); + + let text = ''; + + for await (const chunk of await fs.readStream(uri)) { + text += chunk.toString('utf8'); + } + strict.ok(text.length > 5, 'should have some text'); + strict.ok(text.length < 20, 'shouldnt have too much text'); + }); +}); diff --git a/ce/test/core/i18n-tests.ts b/ce/test/core/i18n-tests.ts new file mode 100644 index 0000000000..2b4f710a27 --- /dev/null +++ b/ce/test/core/i18n-tests.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { i } from '@microsoft/vcpkg-ce/dist/i18n'; +import { strict } from 'assert'; + +// sample test using decorators. +describe('i18n', () => { + it('make sure tagged templates work like templates', () => { + strict.equal(`this is ${100} a test `, i`this is ${100} a test `, 'strings should be the same'); + strict.equal(`${true}${false}this is ${100} a test ${undefined}`, i`${true}${false}this is ${100} a test ${undefined}`, 'strings should be the same'); + }); + + /* + it('try translations', () => { + setLocale('de'); + const uri = 'hello://world'; + + strict.equal(i`uri ${uri} has no scheme`, `uri ${uri} hat kein Schema`, 'Translation did not work correctly'); + }); + */ +}); diff --git a/ce/test/core/index-tests.ts b/ce/test/core/index-tests.ts new file mode 100644 index 0000000000..8df44becc0 --- /dev/null +++ b/ce/test/core/index-tests.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + + +import { Index, IndexSchema, SemverKey, StringKey } from '@microsoft/vcpkg-ce/dist/registries/indexer'; +import { Dictionary, keys } from '@microsoft/vcpkg-ce/dist/util/linq'; +import { describe, it } from 'mocha'; +import { SemVer } from 'semver'; +import { SuiteLocal } from './SuiteLocal'; + +interface TestData { + info: { + id: string, + version: SemVer; + summary?: string + description?: string; + }, + contacts?: Dictionary<{ + email?: string; + role?: Array; + }> +} + + +/** An Index implementation for TestData */ +class MyIndex extends IndexSchema { + + id = new StringKey(this, (i) => i.info.id); + version = new SemverKey(this, (i) => new SemVer(i.info.version)); + description = new StringKey(this, (i) => i.info.description); + + contacts = new StringKey(this, (i) => keys(i.contacts)).with({ + email: new StringKey(this, (i, index: string) => i.contacts?.[index]?.email) + }); +} + + +// sample test using decorators. +describe('Index Tests', () => { + it('Create index from some data', () => { + const index = new Index(MyIndex); + + index.insert({ + info: { + id: 'bob', + version: new SemVer('1.2.3') + } + }, 'foo/bob'); + + index.insert({ + info: { + id: 'wham/blam/sam', + version: new SemVer('0.0.4'), + description: 'this is a test' + } + }, 'other/sam'); + + index.insert({ + info: { + id: 'tom', + version: new SemVer('2.3.4') + }, + contacts: { + 'bob Smith': { + email: 'garrett@contoso.org' + }, + 'rob Smith': { + email: 'tarrett@contoso.org' + }, + } + }, 'foo/tom'); + + index.insert({ + info: { + id: 'sam/blam/bam', + version: new SemVer('0.3.1'), + description: 'this is a test' + } + }, 'sam/blam/bam'); + + const results = index.where. + + version.greaterThan(new SemVer('0.3.0')). + items; + + // results); + // serialize(index.serialize())); + + const data = index.serialize(); + const index2 = new Index(MyIndex); + index2.deserialize(data); + const results2 = index.where. + + version.greaterThan(new SemVer('0.3.0')). + items; + + SuiteLocal.log(results2); + }); + +}); diff --git a/ce/test/core/linq-tests.ts b/ce/test/core/linq-tests.ts new file mode 100644 index 0000000000..f4c498bb44 --- /dev/null +++ b/ce/test/core/linq-tests.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { length, linq } from '@microsoft/vcpkg-ce/dist/util/linq'; +import * as assert from 'assert'; + +const anArray = ['A', 'B', 'C', 'D', 'E']; + +describe('Linq', () => { + it('distinct', async () => { + + const items = ['one', 'two', 'two', 'three']; + const distinct = linq.values(items).distinct().toArray(); + assert.strictEqual(length(distinct), 3); + + const dic = { + happy: 'hello', + sad: 'hello', + more: 'name', + maybe: 'foo', + }; + + const result = linq.values(dic).distinct().toArray(); + assert.strictEqual(length(distinct), 3); + }); + + it('iterating thru collections', async () => { + // items are items. + assert.strictEqual([...linq.values(anArray)].join(','), anArray.join(',')); + assert.strictEqual(linq.values(anArray).count(), 5); + }); +}); diff --git a/ce/test/core/local-file-system-tests.ts b/ce/test/core/local-file-system-tests.ts new file mode 100644 index 0000000000..b51e586cbb --- /dev/null +++ b/ce/test/core/local-file-system-tests.ts @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { FileType } from '@microsoft/vcpkg-ce/dist/fs/filesystem'; +import { hash } from '@microsoft/vcpkg-ce/dist/util/hash'; +import { strict } from 'assert'; +import { pipeline as origPipeline, Writable } from 'stream'; +import { promisify } from 'util'; +import { SuiteLocal } from './SuiteLocal'; + +const pipeline = promisify(origPipeline); + +function writeAsync(writable: Writable, chunk: Buffer): Promise { + return new Promise((resolve, reject) => { + if (writable.write(chunk, (error: Error | null | undefined) => { + // callback gave us an error. + if (error) { + reject(error); + } + })) { + // returned true, we're good to go. + resolve(); + } else { + // returned false + // we were told to wait for it to drain. + writable.once('drain', resolve); + writable.once('error', reject); + } + }); +} + +describe('LocalFileSystemTests', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + it('create/delete folder', async () => { + + const tmp = local.tempFolderUri; + + // create a path to a folder + const someFolder = tmp.join('someFolder'); + + // create the directory + await fs.createDirectory(someFolder); + + // is there a directory there? + strict.ok(await fs.isDirectory(someFolder), `the directory ${someFolder.fsPath} should exist`); + + // delete it + await fs.delete(someFolder, { recursive: true }); + + // make sure it's gone! + strict.ok(!(await fs.isDirectory(someFolder)), `the directory ${someFolder.fsPath} should not exist`); + + }); + + it('create/read file', async () => { + const tmp = local.tempFolderUri; + + const file = tmp.join('hello.txt'); + const expectedText = 'hello world'; + const expectedBuffer = Buffer.from(expectedText, 'utf8'); + + await fs.writeFile(file, expectedBuffer); + + // is there a file there? + strict.ok(await fs.isFile(file), `the file ${file.fsPath} is not present`); + + // read it back + const actualBuffer = await fs.readFile(file); + strict.deepEqual(expectedBuffer, actualBuffer, 'contents should be the same'); + const actualText = actualBuffer.toString(); + strict.equal(expectedText, actualText, 'text should be equal too'); + + }); + + it('readDirectory', async () => { + const tmp = local.tempFolderUri; + const thisFolder = fs.file(__dirname); + + // look in the current folder + const files = await fs.readDirectory(thisFolder); + + // find this file + const found = files.find(each => each[0].fsPath.indexOf('local-file-system') > -1); + + // should be a file, right? + strict.ok(found?.[1] && FileType.File, `${__filename} should be a path`); + + }); + + it('read/write stream', async () => { + const tmp = local.tempFolderUri; + + const thisFile = fs.file(__filename); + const outputFile = tmp.join('output.txt'); + + const outStream = await fs.writeStream(outputFile); + const outStreamDone = new Promise((resolve, reject) => { + outStream.once('close', resolve); + outStream.once('error', reject); + }); + + let text = ''; + // you can iterate thru a stream with 'for await' without casting because I forced the return type to be AsnycIterable + for await (const chunk of await fs.readStream(thisFile)) { + text += chunk.toString('utf8'); + await writeAsync(outStream, chunk); + } + // close the stream once we're done. + outStream.end(); + + await outStreamDone; + + strict.equal((await fs.stat(outputFile)).size, (await fs.stat(thisFile)).size, 'outputFile should be the same length as the input file'); + strict.equal((await fs.stat(thisFile)).size, text.length, 'buffer should be the same size as the input file'); + }); + + it('calculate hashes', async () => { + const tmp = local.tempFolderUri; + const path = local.rootFolderUri.join('resources', 'small-file.txt'); + + strict.equal(await hash(await fs.readStream(path), path, 0, 'sha256', {}), '9cfed8b9e45f47e735098c399fb523755e4e993ac64d81171c93efbb523a57e6', 'hash should match'); + strict.equal(await hash(await fs.readStream(path), path, 0, 'sha384', {}), '8168d029154548a4e1dd5212b722b03d6220f212f8974f6bd45e71715b13945e343c9d1097f8e393db22c8a07d8cf6f6', 'hash should match'); + strict.equal(await hash(await fs.readStream(path), path, 0, 'sha512', {}), '1bacd5dd190731b5c3d2a2ad61142b4054137d6adff5fb085543dcdede77e4a1446225ca31b2f4699b0cda4534e91ea372cf8d73816df3577e38700c299eab5e', 'hash should match'); + }); + + it('reads blocks via open', async () => { + const file = local.rootFolderUri.join('resources', 'small-file.txt'); + const handle = await file.openFile(); + let bytesRead = 0; + for await (const chunk of handle.readStream(0, 3)) { + bytesRead += chunk.length; + strict.equal(chunk.length, 4, 'chunk should be 4 bytes long'); + strict.equal(chunk.toString('utf-8'), 'this', 'chunk should be a word'); + } + strict.equal(bytesRead, 4, 'Stream should read some bytes'); + + bytesRead = 0; + // should be able to read that same chunk again. + for await (const chunk of handle.readStream(0, 3)) { + bytesRead += chunk.length; + strict.equal(chunk.length, 4, 'chunk should be 4 bytes long'); + strict.equal(chunk.toString('utf-8'), 'this', 'chunk should be a word'); + } + strict.equal(bytesRead, 4, 'Stream should read some bytes'); + + bytesRead = 0; + for await (const chunk of handle.readStream()) { + bytesRead += chunk.length; + strict.equal(chunk.byteLength, 23, 'chunk should be 23 bytes long'); + strict.equal(chunk.toString('utf-8'), 'this is a small file.\n\n', 'File contents should equal known result'); + } + strict.equal(bytesRead, 23, 'Stream should read some bytes'); + + await handle.close(); + + + }); + it('reads blocks via open in a large file', async () => { + const file = local.rootFolderUri.join('resources', 'large-file.txt'); + const handle = await file.openFile(); + let bytesRead = 0; + for await (const chunk of handle.readStream()) { + if (bytesRead === 0) { + strict.equal(chunk.length, 32768, 'first chunk should be 32768 bytes long'); + } + else { + strict.equal(chunk.length, 4134, 'second chunk should be 4134 bytes long'); + } + bytesRead += chunk.length; + } + strict.equal(bytesRead, 36902, 'Stream should read some bytes'); + + await handle.close(); + }); + + it('read/write stream with pipe ', async () => { + const tmp = local.tempFolderUri; + + const thisFile = fs.file(__filename); + const thisFileText = (await fs.readFile(thisFile)).toString(); + const outputFile = tmp.join('output2.txt'); + + const inputStream = await fs.readStream(thisFile); + const outStream = await fs.writeStream(outputFile); + await pipeline(inputStream, outStream); + + strict.ok(fs.isFile(outputFile), `there should be a file at ${outputFile.fsPath}`); + + const outFileText = (await fs.readFile(outputFile)).toString(); + strict.equal(outFileText, thisFileText); + + // this will throw if it fails. + await fs.delete(outputFile); + + // make sure it's gone! + strict.ok(!(await fs.isFile(outputFile)), `the file ${outputFile.fsPath} should not exist`); + }); + + it('can copy files', async () => { + // now copy the files from the test folder + const files = await local.fs.copy(local.rootFolderUri, local.session.homeFolder.join('junk')); + strict.ok(files > 3000, `There should be at least 3000 files copied. Only copied ${files}`); + }); +}); diff --git a/ce/test/core/media-query-tests.ts b/ce/test/core/media-query-tests.ts new file mode 100644 index 0000000000..39eb9fc31c --- /dev/null +++ b/ce/test/core/media-query-tests.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { parseQuery } from '@microsoft/vcpkg-ce/dist/mediaquery/media-query'; +import { strict } from 'assert'; +import * as s from '../sequence-equal'; + +// forces the global function for sequence equal to be added to strict before this exectues: +s; + +describe('MediaQuery', () => { + it('windows', async () => { + const queryList = parseQuery('windows'); + strict.equal(queryList.length, 1, 'should be just one query'); + strict.equal(queryList.queries[0].expressions.length, 1, 'should be just one expression'); + strict.equal(queryList.queries[0].expressions[0].feature, 'windows'); + }); + + it('windows and arm', async () => { + const queryList = parseQuery('windows and arm'); + strict.equal(queryList.length, 1, 'should be just one query'); + strict.equal(queryList.queries[0].expressions.length, 2, 'should be two expressions'); + strict.sequenceEqual(queryList.queries[0].expressions.map(each => each.feature), ['windows', 'arm']); + }); + + it('target:x64', async () => { + const queryList = parseQuery('target:x64'); + strict.equal(queryList.length, 1, 'should be just one query'); + strict.equal(queryList.queries[0].expressions.length, 1, 'should be one expression'); + strict.equal(queryList.queries[0].expressions[0].feature, 'target', `feature should say target (got ${queryList.queries[0].expressions[0].feature})`); + strict.equal(queryList.queries[0].expressions[0].constant, 'x64', 'constant should say x64'); + }); + + it('just test the parser for good queries', async () => { + parseQuery('foo and bar'); + parseQuery('foo and (bar)'); + parseQuery('foo and (bar:100)'); + parseQuery('foo and (bar:"hello")'); + parseQuery('foo and (bar:"hello") and buzz'); + parseQuery('not foo and not bar'); + }); + + it('test for known bad query strings', async () => { + + strict.equal(parseQuery('!').error?.message, 'Expected expression, found "!"'); + strict.equal(parseQuery('foo and !').error?.message, 'Expected expression, found "!"'); + strict.equal(parseQuery('foo or (bar:100)').error?.message, 'Expected comma, found "or"'); + strict.equal(parseQuery('not not bar').error?.message, 'Expression specified NOT twice'); + strict.equal(parseQuery('"hello" and bar').error?.message, 'Expected expression, found "\\"hello\\""'); + strict.equal(parseQuery('foo and (bar: : 200 )').error?.message, 'Expected one of {Number, Boolean, Identifier, String}, found token ":"'); + strict.equal(parseQuery('"').error?.message, 'Unexpected end of file while searching for \'"\''); + strict.equal(parseQuery('foo:0x01fz').error?.message, 'Expected comma, found "z"'); + strict.equal(parseQuery('foo:?100').error?.message, 'Expected one of {Number, Boolean, Identifier, String}, found token "?"'); + }); + + it('positive matches', async () => { + strict.ok(parseQuery('foo').match({ foo: true }), 'foo was present, it should match!'); + strict.ok(parseQuery('foo').match({ foo: null }), 'foo was present, it should match!'); + + strict.ok(parseQuery('foo:false').match({}), 'foo was not present, it should match!'); + strict.ok(parseQuery('foo:true').match({ foo: true }), 'foo was true, it should match!'); + strict.ok(parseQuery('foo:true').match({ foo: null }), 'foo was true, it should match!'); + strict.ok(parseQuery('foo and windows').match({ foo: true, windows: true, books: true }), 'foo,windows was present, it should match!'); + strict.ok(parseQuery('windows and x64 and target:amd64, osx').match({ windows: true, x64: true, target: 'amd64' }), 'should match'); + strict.ok(parseQuery('windows and (x64) and (target:amd64), osx').match({ windows: true, x64: true, target: 'amd64' }), 'should match'); + strict.ok(parseQuery('windows and x64 and target:amd64, osx').match({ osx: true }), 'should match'); + strict.ok(parseQuery('not windows').match({ windows: false, linux: true }), 'it should match!'); + }); + + it('negative matches', async () => { + strict.ok(!parseQuery('not foo').match({ foo: true }), 'foo was present, it should not match!'); + strict.ok(!parseQuery('not foo').match({ foo: null }), 'foo was present, it should not match!'); + strict.ok(!parseQuery('foo').match({ foo: false }), 'foo was false, it should not match!'); + strict.ok(!parseQuery('not foo:true').match({ foo: true }), 'foo was true, it should not match!'); + strict.ok(!parseQuery('not foo:true').match({ foo: null }), 'foo was true, it should not match!'); + + + strict.ok(!parseQuery('foo').match({}), 'foo was not present, it should not match!'); + strict.ok(!parseQuery('not foo:false').match({}), 'foo was not present, it should match false!'); + strict.ok(!parseQuery('bar and windows').match({ foo: true, windows: true, books: true }), 'bar was not , it should not match!'); + strict.ok(!parseQuery('windows and x64 and target:amd64, osx').match({ linux: true }), 'should not match'); + strict.ok(!parseQuery('not windows and not linux').match({ windows: false, linux: true }), 'it should not match!'); + }); +}); diff --git a/ce/test/core/msbuild-tests.ts b/ce/test/core/msbuild-tests.ts new file mode 100644 index 0000000000..2d3d56f42b --- /dev/null +++ b/ce/test/core/msbuild-tests.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Activation } from '@microsoft/vcpkg-ce/dist/artifacts/activation'; +import { strict } from 'assert'; +import { platform } from 'os'; +import { SuiteLocal } from './SuiteLocal'; + +describe('MSBuild Generator', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + + it('Generates locations in order', () => { + + const activation = new Activation(local.session); + + (]>>[ + ['z', 'zse&tting'], + ['a', 'asend', 'third']] + ]).forEach(([key, value]) => activation.properties.set(key, typeof value === 'string' ? [value] : value)); + + activation.locations.set('somepath', local.fs.file('c:/tmp')); + activation.paths.set('include', [local.fs.file('c:/tmp'), local.fs.file('c:/tmp2')]); + + const expected = (platform() === 'win32') ? ` + + + c:\\tmp + + + zse&tting + ase<tting + csetting + bsetting + first;seco>nd;third + + + c:\\tmp;c:\\tmp2 + +` : ` + + + c:/tmp + + + zse&tting + ase<tting + csetting + bsetting + first;seco>nd;third + + + c:/tmp;c:/tmp2 + +` ; + + strict.equal(activation.generateMSBuild([]), expected); + }); +}); diff --git a/ce/test/core/repo-tests.ts b/ce/test/core/repo-tests.ts new file mode 100644 index 0000000000..5fb49b53d6 --- /dev/null +++ b/ce/test/core/repo-tests.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { serialize } from '@microsoft/vcpkg-ce/dist/yaml/yaml'; +import { createHash } from 'crypto'; +import { describe } from 'mocha'; +import { getNouns, paragraph, sentence } from 'txtgen'; +import { SuiteLocal } from './SuiteLocal'; + + +const idwords = ['compilers', 'tools', 'contoso', 'adatum', 'cheese', 'utility', 'crunch', 'make', 'build', 'cc', 'c++', 'debugger', 'flasher', 'whatever', 'else']; +const firstNames = ['sandy', 'tracy', 'skyler', 'nick', 'bob', 'jack', 'alice']; +const lastNames = ['smith', 'jones', 'quack', 'fry', 'lunch']; +const companies = ['contoso', 'adatum', 'etcetcetc']; +const roles = ['pusher', 'do-er', 'maker', 'publisher', 'slacker', 'originator']; +const suffixes = ['com', 'org', 'net', 'gov', 'ca', 'uk', 'etc']; + +function rnd(min: number, max: number) { + return Math.floor((Math.random() * (max - min)) + min); +} + +function rndSemver() { + return [rnd(0, 100), rnd(0, 200), rnd(0, 5000)].join('.'); +} + +function randomWord(from: Array) { + return from[rnd(0, from.length - 1)]; +} + +function randomHost() { + return `${randomWord(companies)}.${randomWord(suffixes)}`; +} + +function randomContacts() { + const result = {}; + + for (let i = 0; i < rnd(0, 3); i++) { + const name = `${randomWord(firstNames)} ${randomWord(lastNames)}`; + result[name] = { + email: `${randomWord(getNouns())}@${randomHost()}`, + roles: randomWords(roles, 1, 3) + }; + } + return result; +} + +function randomWords(from: Array, min = 3, max = 6) { + const s = new Set(); + const n = rnd(min, max); + while (s.size < n) { + s.add(randomWord(from)); + } + return [...s.values()]; +} + +class Template { + info = { + id: randomWords(idwords).join('/'), + version: rndSemver(), + summary: sentence(), + description: paragraph(), + }; + + contacts = randomContacts(); + install = { + unzip: `https://${randomHost()}/${sentence().replace(/ /g, '/')}.zip`, + sha256: createHash('sha256').update(sentence()).digest('hex'), + }; +} + +describe('StandardRegistry Tests', () => { + + const local = new SuiteLocal(); + + after(local.after.bind(local)); + + before(async () => { + const repoFolder = local.session.homeFolder.join('repo', 'default'); + // creates a bunch of artifacts, with multiple versions + const pkgs = 100; + + for (let i = 0; i < pkgs; i++) { + const versions = rnd(1, 5); + const t = new Template(); + for (let j = 0; j < versions; j++) { + const p = { + ...t, + }; + p.info.version = rndSemver(); + const target = repoFolder.join(`${p.info.id}-${p.info.version}.yaml`); + await target.writeFile(Buffer.from(serialize(p), 'utf8')); + } + } + // now copy the files from the test folder + await local.fs.copy(local.rootFolderUri.join('resources', 'repo'), repoFolder); + }); + + /* fixme! + it('can save and load the index', async () => { + const registry = local.session.defaultRegistry; + await registry.regenerate(); + await registry.save(); + + const anotherregistry = new LocalRegistry(local.session, local.session.homeFolder.join('repo', 'default')); + await anotherregistry.load(); + strict.equal(registry.count, anotherregistry.count, 'repo should be the same size as the last one'); + }); + + it('Loads a bunch items', async () => { + const registry = local.session.defaultRegistry; + await registry.regenerate(); + + const all = await registry.openArtifacts(registry.values); + const items = [...all.values()].flat(); + strict.equal(items.length, registry.count, 'Should have loaded everything'); + + }); + + + it('Create index from some data', async () => { + const start = process.uptime() * 1000; + + const registry = local.session.defaultRegistry; + local.session.channels.on('debug', (d, x, m) => SuiteSuiteLocal.log(`${m}msec : ${d}`)); + await registry.regenerate(); + await registry.save(); + + const arm = registry.where.id.equals('compilers/gnu/gcc/arm-none-eabi').items; + strict.equal(arm.length, 3, 'should be 3 results'); + + local.session.channels.on('debug', (t) => SuiteSuiteLocal.log(t)); + + const map = await registry.openArtifacts(arm); + strict.equal(map.size, 1, 'Should have one pkg id'); + + const versions = map.get('compilers/gnu/gcc/arm-none-eabi'); + strict.ok(versions, 'should have some versions'); + strict.equal(versions.length, 3, 'should have three versions of the package'); + + const anotherregistry = new LocalRegistry(local.session, local.session.homeFolder.join('repo', 'default')); + await anotherregistry.load(); + const anotherArm = registry.where.id.equals('compilers/gnu/gcc/arm-none-eabi').items; + strict.equal(anotherArm.length, 3, 'should be 3 results'); + + + const cmakes = registry.where.id.equals('tools/kitware/cmake').items; + strict.equal(cmakes.length, 5, 'should be 5 results'); + }); + */ +}); \ No newline at end of file diff --git a/ce/test/core/sample-tests.ts b/ce/test/core/sample-tests.ts new file mode 100644 index 0000000000..87697f97f3 --- /dev/null +++ b/ce/test/core/sample-tests.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { sanitizePath } from '@microsoft/vcpkg-ce/dist/artifacts/artifact'; +import { notStrictEqual, strict } from 'assert'; +import { describe, it } from 'mocha'; +import { pipeline as origPipeline } from 'stream'; +import { promisify } from 'util'; + +const pipeline = promisify(origPipeline); + +// sample test using decorators. +describe('SomeTests', () => { + it('Try This Sample Test', () => { + notStrictEqual(5, 4, 'numbers should not be equal'); + }); +}); + +// sample test that uses describe/it +describe('sample test', () => { + it('does not make mistakes', () => { + notStrictEqual('A', 'B', 'letters should not be equal'); + }); +}); + + +describe('sanitization of paths', () => { + it('makes nice clean paths', () => { + strict.equal(sanitizePath(''), ''); + strict.equal(sanitizePath('.'), ''); + strict.equal(sanitizePath('..'), ''); + strict.equal(sanitizePath('..../....'), ''); + strict.equal(sanitizePath('..../foo/....'), 'foo'); + strict.equal(sanitizePath('..../..foo/....'), '..foo'); + strict.equal(sanitizePath('.config'), '.config'); + strict.equal(sanitizePath('\\.config'), '.config'); + strict.equal(sanitizePath('..\\.config'), '.config'); + strict.equal(sanitizePath('/bar'), 'bar'); + strict.equal(sanitizePath('\\this\\is\\a//test/of//a\\path//..'), 'this/is/a/test/of/a/path'); + + }); +}); + diff --git a/ce/test/core/stream-tests.ts b/ce/test/core/stream-tests.ts new file mode 100644 index 0000000000..79449460d8 --- /dev/null +++ b/ce/test/core/stream-tests.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { Channels } from '@microsoft/vcpkg-ce/dist/util/channels'; +import { strictEqual } from 'assert'; +import { SuiteLocal } from './SuiteLocal'; + +describe('StreamTests', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + it('event emitter works', async () => { + + const expected = ['a', 'b', 'c', 'd']; + let i = 0; + + const session = local.session; + const m = new Channels(session); + m.on('message', (message, context, msec) => { + // check that each message comes in order + strictEqual(message, expected[i], 'messages should be in order'); + i++; + }); + + for (const each of expected) { + m.message(each); + } + + strictEqual(expected.length, i, 'should have got the right number of messages'); + }); +}); diff --git a/ce/test/core/uniqueTempFolder.ts b/ce/test/core/uniqueTempFolder.ts new file mode 100644 index 0000000000..eec287c5b3 --- /dev/null +++ b/ce/test/core/uniqueTempFolder.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { mkdtempSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; + +export function uniqueTempFolder(): string { + return mkdtempSync(join(tmpdir(), '/ce-temp!')); +} diff --git a/ce/test/core/uri-tests.ts b/ce/test/core/uri-tests.ts new file mode 100644 index 0000000000..6cb7387d3a --- /dev/null +++ b/ce/test/core/uri-tests.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { strictEqual } from 'assert'; +import { SuiteLocal } from './SuiteLocal'; + +describe('Uri', () => { + const local = new SuiteLocal(); + const fs = local.fs; + + after(local.after.bind(local)); + const tempUrl = local.tempFolderUri; + const tempUrlForward = tempUrl.join().toString(); + + it('Converts slashes on join', () => { + const unixPath = fs.parse('/some/unixy/path').join(); + strictEqual(unixPath.toString(), 'file:///some/unixy/path'); + const windowsPath = fs.parse('C:\\Windows\\System32').join(); + strictEqual(windowsPath.toString(), 'C:/Windows/System32'); + }); + + it('Can go to a child path', () => { + const child = tempUrl.join('uriChild').toString(); + strictEqual(child, tempUrlForward + '/uriChild'); + }); + + it('Can go to parent path', () => { + const child = tempUrl.join('uriChild'); + const actual = child.parent.join().toString(); + strictEqual(actual.toString(), tempUrlForward); + }); +}); diff --git a/ce/test/core/util/percentage-scaler-tests.ts b/ce/test/core/util/percentage-scaler-tests.ts new file mode 100644 index 0000000000..16d75b092d --- /dev/null +++ b/ce/test/core/util/percentage-scaler-tests.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { PercentageScaler } from '@microsoft/vcpkg-ce/dist/util/percentage-scaler'; +import { strict, throws } from 'assert'; + +describe('PercentageScaler', () => { + it('ScalesPercentagesTo100', () => { + const uut = new PercentageScaler(0, 1000); + for (let idx = 0; idx < 1000; ++idx) { + strict.equal(uut.scalePosition(idx), idx / 10); + } + }); + + it('ScalesPercentagesToDifferentRanges', () => { + const uut = new PercentageScaler(0, 1000, 10, 20); + for (let idx = 0; idx < 10; ++idx) { + strict.equal(uut.scalePosition(idx * 100), 10 + idx); + } + }); + + it('ScalesZeroRangesAsMax', () => { + const uut = new PercentageScaler(0, 0, 0, 200); + strict.equal(uut.scalePosition(0), 200); + }); + + it('ScalesUniquePercentageRangesAsThatPercent', () => { + const uut = new PercentageScaler(0, 100, 200, 200); + for (let idx = -1; idx < 102; ++idx) { + strict.equal(uut.scalePosition(idx), 200); + } + }); + + it('ClampsDomain', () => { + const uut = new PercentageScaler(0, 10); + strict.equal(uut.scalePosition(Number.MIN_VALUE), 0); + strict.equal(uut.scalePosition(-100), 0); + strict.equal(uut.scalePosition(0), 0); + strict.equal(uut.scalePosition(1), 10); + strict.equal(uut.scalePosition(10), 100); + strict.equal(uut.scalePosition(11), 100); + strict.equal(uut.scalePosition(Number.MAX_VALUE), 100); + }); + + it('ValidatesParameters', () => { + throws(() => new PercentageScaler(0, -1)); // transposed domain range + new PercentageScaler(0, 0); // OK + new PercentageScaler(0, 0, 0, 0); // OK + throws(() => new PercentageScaler(0, 0, 0, -1)); // percentage range is reversed + }); +}); diff --git a/ce/test/core/willow-tests.ts b/ce/test/core/willow-tests.ts new file mode 100644 index 0000000000..1b8abb2633 --- /dev/null +++ b/ce/test/core/willow-tests.ts @@ -0,0 +1,622 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { parseVsManFromChannel, resolveVsManId, VsManDatabase } from '@microsoft/vcpkg-ce/dist/willow/willow'; +import { strict, throws } from 'assert'; +import { describe, it } from 'mocha'; +import { SuiteLocal } from './SuiteLocal'; + +function testMalformedChannelParse(chmanContent: string, damage: (chmanObjects: any) => void) { + const chmanObjects = JSON.parse(chmanContent); + damage(chmanObjects); + throws(() => parseVsManFromChannel(JSON.stringify(chmanObjects))); +} + +describe('Willow', () => { + // "channelItems": [ + // { + // "id": "Microsoft.VisualStudio.Manifests.VisualStudio", + // "version": "16.0.31205.134", + // "type": "Manifest", + // "payloads": [ + // { + // "fileName": "VisualStudio.vsman", + // "sha256": "6c99749816b688015ce25f72664610e3b5e163621a2e3dd19b06982b5c9cb31f", + // "size": 18413665, + // "url": "https://download.visualstudio.microsoft.com/download/pr/.../VisualStudio.vsman" + // } + // ] + // }, + + it('Parses Channel Manifests', async () => { + const local = new SuiteLocal(); + const chmanUri = local.resourcesFolderUri.join('2020-05-06-VisualStudio.16.Release.chman'); + const chmanContent = await chmanUri.readUTF8(); + const result = parseVsManFromChannel(chmanContent); + strict.deepStrictEqual(result, { + fileName: 'VisualStudio.vsman', + sha256: '6c99749816b688015ce25f72664610e3b5e163621a2e3dd19b06982b5c9cb31f', + size: 18413665, + url: 'https://download.visualstudio.microsoft.com/download/pr/3105fcfe-e771-41d6-9a1c-fc971e7d03a7/6c99749816b688015ce25f72664610e3b5e163621a2e3dd19b06982b5c9cb31f/VisualStudio.vsman', + version: '16.0.31205.134' + }); + + await local.after(); + }); + + it('Errors On Bad Channel Manifests', async () => { + throws(() => parseVsManFromChannel('{{{')); + const local = new SuiteLocal(); + const chmanUri = local.resourcesFolderUri.join('2020-05-06-VisualStudio.16.Release.chman'); + const chmanContent = await chmanUri.readUTF8(); + const test = (damage: (chmanObjects: any) => void) => { + testMalformedChannelParse(chmanContent, damage); + }; + + // bad version + test((chmanObjects) => chmanObjects.manifestVersion = '0'); + // bad channelItems + test((chmanObjects) => chmanObjects.channelItems = '0'); + // no Microsoft.VisualStudio.Manifests.VisualStudio + test((chmanObjects) => chmanObjects.channelItems = chmanObjects.channelItems.slice(1)); + // too many Microsoft.VisualStudio.Manifests.VisualStudio + test((chmanObjects) => chmanObjects.channelItems = chmanObjects.channelItems.push(chmanObjects.channelItems[0])); + + const testVsManEntry = (damage: (channelItem: any) => void) => { + testMalformedChannelParse(chmanContent, (chmanObjects) => damage(chmanObjects.channelItems[0])); + }; + + // bad payloads + testVsManEntry((entry) => entry.payloads = 0); + // not manifest + testVsManEntry((entry) => entry.type = 0); + // version not string + testVsManEntry((entry) => entry.version = 0); + // multiple payloads + testVsManEntry((entry) => entry.payloads = entry.payloads.push(entry.payloads[0])); + // bad filename + testVsManEntry((entry) => entry.payloads[0].fileName = 0); + // bad sha + testVsManEntry((entry) => entry.payloads[0].sha256 = 0); + // bad sha content + testVsManEntry((entry) => entry.payloads[0].sha256 = '6c99749816b688015ce25f726'); // length bad + // bad size + testVsManEntry((entry) => entry.payloads[0].size = '0'); + // bad url + testVsManEntry((entry) => entry.payloads[0].url = 0); + + await local.after(); + }); + + it('Parses VS Manifests', async () => { + const local = new SuiteLocal(); + const vsmanUri = local.resourcesFolderUri.join('2021-05-06-VisualStudio.vsman'); + const vsmanContent = await vsmanUri.readUTF8(); + const result = new VsManDatabase(vsmanContent); + strict.deepStrictEqual(result.get('Microsoft.VisualCpp.Tools.HostX86.TargetX86'), + [ + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86', + 'version': '14.28.29914', + 'language': undefined, + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.vsix', + 'sha256': '5b9e9d7f332e79bcc2342c49e7f99ddf43ecf6d130e5e6c6908dc2c7a39b15c6', + 'size': 16126219, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/5b9e9d7f332e79bcc2342c49e7f99ddf43ecf6d130e5e6c6908dc2c7a39b15c6/Microsoft.VisualCpp.Tools.HostX86.TargetX86.vsix', + 'installSize': 35276532, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86,version=14.28.29914/payload.vsix' + } + ]); + + strict.deepStrictEqual(result.get('Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources'), [ + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'cs-CZ', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.csy.vsix', + 'sha256': '47db023518b45bab6e7b9bba9aa2b0254895f3eb21221a70bdeb453e2747bafd', + 'size': 229756, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/47db023518b45bab6e7b9bba9aa2b0254895f3eb21221a70bdeb453e2747bafd/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.csy.vsix', + 'installSize': 906288, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=cs-CZ/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'de-DE', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.deu.vsix', + 'sha256': '5e128ff82600b53cebd63afaa40d3714d52c539104bf8ad4b44d61f261e933a1', + 'size': 234819, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/5e128ff82600b53cebd63afaa40d3714d52c539104bf8ad4b44d61f261e933a1/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.deu.vsix', + 'installSize': 1026640, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=de-DE/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'en-US', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.enu.vsix', + 'sha256': '4752b7e2053d1db888b43309d604ae7ed807d363ecbb39a89937ca051a10bdc8', + 'size': 204190, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/4752b7e2053d1db888b43309d604ae7ed807d363ecbb39a89937ca051a10bdc8/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.enu.vsix', + 'installSize': 862328, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=en-US/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'es-ES', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.esn.vsix', + 'sha256': 'b9c8500e4f49df54292c7aab3bae930a940cd264877b4feee7cc26618f99edf4', + 'size': 224027, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/b9c8500e4f49df54292c7aab3bae930a940cd264877b4feee7cc26618f99edf4/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.esn.vsix', + 'installSize': 1001560, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=es-ES/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'fr-FR', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.fra.vsix', + 'sha256': 'fe41ec5957aaa14e5a613cd2352bc1171b1e4b550e980bf4dcd0b11fce1d7192', + 'size': 225466, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/fe41ec5957aaa14e5a613cd2352bc1171b1e4b550e980bf4dcd0b11fce1d7192/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.fra.vsix', + 'installSize': 1012800, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=fr-FR/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'it-IT', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.ita.vsix', + 'sha256': '8c540bc9953a7f1c26072e16748926a73818ae5fb5b8288bedbc0841e789bfac', + 'size': 222846, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/8c540bc9953a7f1c26072e16748926a73818ae5fb5b8288bedbc0841e789bfac/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.ita.vsix', + 'installSize': 1012272, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=it-IT/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'ja-JP', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.jpn.vsix', + 'sha256': '91e4c239726a104a38c7ae69d1590abc20249354c0fd5a5ae580ceef8e5149eb', + 'size': 200323, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/91e4c239726a104a38c7ae69d1590abc20249354c0fd5a5ae580ceef8e5149eb/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.jpn.vsix', + 'installSize': 636488, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=ja-JP/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'ko-KR', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.kor.vsix', + 'sha256': '3b1fa4d736a7d82bef9054fbe5f52919d34a8c5ec614e3d957e7665b94ae9ece', + 'size': 195053, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/3b1fa4d736a7d82bef9054fbe5f52919d34a8c5ec614e3d957e7665b94ae9ece/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.kor.vsix', + 'installSize': 640568, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=ko-KR/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'pl-PL', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.plk.vsix', + 'sha256': '13b2331b7157e21006b79c873d532e5e256ae049f41b11be972e316eae6a8a1b', + 'size': 233518, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/13b2331b7157e21006b79c873d532e5e256ae049f41b11be972e316eae6a8a1b/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.plk.vsix', + 'installSize': 982080, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=pl-PL/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'pt-BR', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.ptb.vsix', + 'sha256': '2d4133d05cf9adec2bdbc73cbdb266c359d7bb1fe6e012cc346b1dff334df860', + 'size': 219322, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/2d4133d05cf9adec2bdbc73cbdb266c359d7bb1fe6e012cc346b1dff334df860/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.ptb.vsix', + 'installSize': 947784, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=pt-BR/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'ru-RU', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.rus.vsix', + 'sha256': '54d321dc5471c42d655aa5b613b55dab3f1129df179bef8597e470d46f464445', + 'size': 233022, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/54d321dc5471c42d655aa5b613b55dab3f1129df179bef8597e470d46f464445/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.rus.vsix', + 'installSize': 968256, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=ru-RU/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'tr-TR', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.trk.vsix', + 'sha256': '7b1cbe7df387e5132d8e639e407afe8c83b45f624cde06fc9aa6af0bf896f32d', + 'size': 216776, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7b1cbe7df387e5132d8e639e407afe8c83b45f624cde06fc9aa6af0bf896f32d/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.trk.vsix', + 'installSize': 911416, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=tr-TR/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'zh-CN', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.chs.vsix', + 'sha256': '7538c083be1f37b91217b8690a72746fc57bcd4c0e8c21f553ac4f88f8c60c98', + 'size': 183804, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7538c083be1f37b91217b8690a72746fc57bcd4c0e8c21f553ac4f88f8c60c98/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.chs.vsix', + 'installSize': 478256, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=zh-CN/payload.vsix' + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources', + 'version': '14.28.29914', + 'language': 'zh-TW', + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.cht.vsix', + 'sha256': 'f0c019c004dcca5f7f5ce3a9b97730967cfff6f960113a55f7f317a41ade3df7', + 'size': 187675, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/f0c019c004dcca5f7f5ce3a9b97730967cfff6f960113a55f7f317a41ade3df7/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.cht.vsix', + 'installSize': 503864, + 'localPath': 'vsix:///Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources,version=14.28.29914,language=zh-TW/payload.vsix' + } + ]); + + await local.after(); + }); + + it('Hard Rejects Completely Broken VS Manifests', () => { + throws(() => { new VsManDatabase('{'); }); + throws(() => { new VsManDatabase('{}'); }); + throws(() => { new VsManDatabase('{\'packages\': 42}'); }); + }); + + it('Ignores Unusuable Packages', () => { + const testInput = { + 'packages': [ + { + 'id': 'good', + 'version': '14.28.29914', + 'type': 'Vsix', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'sha256': '7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42/Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 33650830 + } + }, + { + // no id + 'version': '14.28.29914', + 'type': 'Vsix', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'sha256': '7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42/Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 33650830 + } + }, + { + 'id': 'badversion', + 'version': 42, + 'type': 'Vsix', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'sha256': '7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42/Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 33650830 + } + }, + { + 'id': 'badtype', + 'version': '14.28.29914', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'sha256': '7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42/Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 33650830 + } + }, + { + 'id': 'badinstallsize', + 'version': '14.28.29914', + 'type': 'Vsix', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'sha256': '7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42/Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'elsewhere': 33650830 + } + }, + { + 'id': 'badpayload', + 'version': '14.28.29914', + 'type': 'Vsix', + 'payloads': [ + { + 'sha256': '7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42/Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 33650830 + } + }, + { + 'id': 'multipayload', + 'version': '14.28.29914', + 'type': 'Vsix', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'sha256': '7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42/Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'signer': { + '$ref': '2' + } + }, + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix_other', + 'sha256': 'xxxx3eaab5d6000185cf8c44e3eeb8229cd1afa3dda2cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://example.com/hello.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 33650830 + } + } + ] + }; + + const actual = new VsManDatabase(JSON.stringify(testInput)); + strict.equal(actual.size, 1); + strict.deepStrictEqual(actual.get('good'), [{ + 'id': 'good', + 'version': '14.28.29914', + 'language': undefined, + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'sha256': '7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42', + 'size': 15447514, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7c3a3eaab5d6000185cf8c44e3eeb8229cd1afa3dd72cf1d9c579f9125561a42/Microsoft.VisualCpp.Tools.HostX86.TargetX64.vsix', + 'installSize': 33650830, + 'localPath': 'vsix:///good,version=14.28.29914/payload.vsix' + }]); + }); + + it('Follows Pointer-like Packages', () => { + const testInput = { + 'packages': [ + { + 'id': 'Microsoft.VisualCpp.ASAN.X86', + 'version': '14.29.30037', + 'type': 'Vsix', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.ASAN.X86.vsix', + 'sha256': '97c926de9fef5e5c8760638ad05ceaa7793f923ab608ece35c9102c7cc992338', + 'size': 1071, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/97c926de9fef5e5c8760638ad05ceaa7793f923ab608ece35c9102c7cc992338/Microsoft.VisualCpp.ASAN.X86.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'dependencies': { + 'Microsoft.VC.14.29.16.10.ASAN.X86.base': '14.29.30037' + } + }, + { + 'id': 'Microsoft.VC.14.29.16.10.ASAN.X86.base', + 'version': '14.29.30037', + 'type': 'Vsix', + 'payloads': [ + { + 'fileName': 'Microsoft.VC.14.29.16.10.ASAN.X86.base.vsix', + 'sha256': '4a39aa98ee4540b8cd9a55ad6f1717602ee45ffd2db3a70894b9a3b41dfdac1c', + 'size': 17401493, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/4a39aa98ee4540b8cd9a55ad6f1717602ee45ffd2db3a70894b9a3b41dfdac1c/Microsoft.VC.14.29.16.10.ASAN.X86.base.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 63068674 + }, + 'dependencies': { + 'Microsoft.VC.14.29.16.10.ASAN.Headers.base': '14.29.30037', + 'Microsoft.VC.14.29.16.10.ASAN.X64.base': '14.29.30037', + 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetX86.base': '14.29.30037', + 'Microsoft.VC.14.29.16.10.Tools.HostX64.TargetX86.base': '14.29.30037' + } + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetARM.Resources', + 'version': '14.29.30037', + 'type': 'Vsix', + 'language': 'de-DE', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetARM.Resources.deu.vsix', + 'sha256': 'a4cf6d5ac182a7f386afabdcb27605fca34869bc2b6bd39c4210021f85813599', + 'size': 1100, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/a4cf6d5ac182a7f386afabdcb27605fca34869bc2b6bd39c4210021f85813599/Microsoft.VisualCpp.Tools.HostX86.TargetARM.Resources.deu.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'dependencies': { + 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base': '14.29.30037' + } + }, + { + 'id': 'Microsoft.VisualCpp.Tools.HostX86.TargetARM.Resources', + 'version': '14.29.30037', + 'type': 'Vsix', + 'language': 'en-US', + 'payloads': [ + { + 'fileName': 'Microsoft.VisualCpp.Tools.HostX86.TargetARM.Resources.enu.vsix', + 'sha256': '0b83f64f4a9d9bc5200092d06117a0e7936ca7f1a4e3b596537cdba2893504d5', + 'size': 1100, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/0b83f64f4a9d9bc5200092d06117a0e7936ca7f1a4e3b596537cdba2893504d5/Microsoft.VisualCpp.Tools.HostX86.TargetARM.Resources.enu.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'dependencies': { + 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base': '14.29.30037' + } + }, + { + 'id': 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base', + 'version': '14.29.30037', + 'type': 'Vsix', + 'language': 'de-DE', + 'payloads': [ + { + 'fileName': 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base.deu.vsix', + 'sha256': '23612dd70c849c6735b17ce2887edf6d7ca243e9a2f9bd427daace8653b17987', + 'size': 236777, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/23612dd70c849c6735b17ce2887edf6d7ca243e9a2f9bd427daace8653b17987/Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base.deu.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 1033240 + } + }, + { + 'id': 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base', + 'version': '14.29.30037', + 'type': 'Vsix', + 'language': 'en-US', + 'payloads': [ + { + 'fileName': 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base.enu.vsix', + 'sha256': 'b6c39771b8c5564f9998e7b1bd3a89e1d23d287f6625ef65f2cd43c1ccabd306', + 'size': 205517, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/b6c39771b8c5564f9998e7b1bd3a89e1d23d287f6625ef65f2cd43c1ccabd306/Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base.enu.vsix', + 'signer': { + '$ref': '2' + } + } + ], + 'installSizes': { + 'targetDrive': 868400 + } + } + ] + }; + + const actual = new VsManDatabase(JSON.stringify(testInput)); + strict.equal(actual.size, 4); + const asanExpected = [{ + 'id': 'Microsoft.VC.14.29.16.10.ASAN.X86.base', + 'version': '14.29.30037', + 'language': undefined, + 'fileName': 'Microsoft.VC.14.29.16.10.ASAN.X86.base.vsix', + 'sha256': '4a39aa98ee4540b8cd9a55ad6f1717602ee45ffd2db3a70894b9a3b41dfdac1c', + 'size': 17401493, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/4a39aa98ee4540b8cd9a55ad6f1717602ee45ffd2db3a70894b9a3b41dfdac1c/Microsoft.VC.14.29.16.10.ASAN.X86.base.vsix', + 'installSize': 63068674, + 'localPath': 'vsix:///Microsoft.VC.14.29.16.10.ASAN.X86.base,version=14.29.30037/payload.vsix' + }]; + strict.deepStrictEqual(actual.get('Microsoft.VisualCpp.ASAN.X86'), asanExpected); + strict.deepStrictEqual(actual.get('Microsoft.VC.$(SxSVersion).ASAN.X86.base'), asanExpected); + strict.deepStrictEqual(actual.get('Microsoft.VisualCpp.Tools.HostX86.TargetARM.Resources'), [ + { + 'id': 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base', + 'version': '14.29.30037', + 'language': 'de-DE', + 'fileName': 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base.deu.vsix', + 'sha256': '23612dd70c849c6735b17ce2887edf6d7ca243e9a2f9bd427daace8653b17987', + 'size': 236777, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/23612dd70c849c6735b17ce2887edf6d7ca243e9a2f9bd427daace8653b17987/Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base.deu.vsix', + 'installSize': 1033240, + 'localPath': 'vsix:///Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base,version=14.29.30037,language=de-DE/payload.vsix' + }, + { + 'id': 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base', + 'version': '14.29.30037', + 'language': 'en-US', + 'fileName': 'Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base.enu.vsix', + 'sha256': 'b6c39771b8c5564f9998e7b1bd3a89e1d23d287f6625ef65f2cd43c1ccabd306', + 'size': 205517, + 'url': 'https://download.visualstudio.microsoft.com/download/pr/c0ac19c1-e1d7-47e2-bde8-fd11c4410cca/b6c39771b8c5564f9998e7b1bd3a89e1d23d287f6625ef65f2cd43c1ccabd306/Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base.enu.vsix', + 'installSize': 868400, + 'localPath': 'vsix:///Microsoft.VC.14.29.16.10.Tools.HostX86.TargetARM.Resources.base,version=14.29.30037,language=en-US/payload.vsix' + } + ]); + }); + + it('Finds Latest Numbers', () => { + strict.equal(resolveVsManId(['hello.100.world', 'hello.9.world', 'hello.10.world', 'unrelated'], 'hello.$(SxSVersion).world'), 'hello.100.world'); + + strict.equal(resolveVsManId(['hello.9.world', 'hello.10.world', 'unrelated', 'hello.100.world'], 'hello.$(SxSVersion).world'), 'hello.100.world'); + + strict.equal(resolveVsManId(['hello.9.world', 'hello.100.world', 'hello.10.world', 'unrelated'], 'hello.$(SxSVersion).world'), 'hello.100.world'); + + strict.equal(resolveVsManId(['hello.100.10.world', 'hello.100.world', 'hello.10.world', 'unrelated'], 'hello.$(SxSVersion).world'), 'hello.100.10.world'); + + strict.equal(resolveVsManId(['hello.100.10.world', 'hello.100.world', 'hello.10.world', 'unrelated'], '$(SxSVersion)'), '$(SxSVersion)'); + + strict.equal(resolveVsManId(['100.10', '100', '10', 'unrelated'], '$(SxSVersion)'), '100.10'); + }); +}); diff --git a/ce/test/mocha-config.yaml b/ce/test/mocha-config.yaml new file mode 100644 index 0000000000..667c60be02 --- /dev/null +++ b/ce/test/mocha-config.yaml @@ -0,0 +1,16 @@ +color: true +require: + - 'source-map-support/register' +monkeyPatch: true +spec: ./**/*.js +ignore: + - "./**/*.d.ts" + - "./node_modules/**" + - "../common/**/node_modules/**" + +# this section enables npm watch-test to work right +watch-files: ../*/**/*.js +watch-ignore: + - "./node_modules/**" + - "../common/**/node_modules/**" + - "./**/*.js" diff --git a/ce/test/package.json b/ce/test/package.json new file mode 100644 index 0000000000..bf29874a9b --- /dev/null +++ b/ce/test/package.json @@ -0,0 +1,55 @@ +{ + "name": "vcpkg-ce.test", + "version": "0.7.0", + "description": "ce test project", + "directories": { + "doc": "docs" + }, + "engines": { + "node": ">=10.12.0" + }, + "scripts": { + "eslint-fix": "eslint . --fix --ext .ts", + "eslint": "eslint . --ext .ts", + "clean": "shx rm -rf dist .rush && shx echo Done", + "build": "tsc -p .", + "watch": "tsc -p . --watch", + "prepare": "npm run build", + "test-ci": "npm run build && npm run test", + "test": "node --harmony ./node_modules/mocha/bin/mocha --config ./mocha-config.yaml --timeout 200000", + "watch-test": "node --harmony ./node_modules/mocha/bin/mocha --config ./mocha-config.yaml --timeout 200000 --watch" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Microsoft/vcpkg-ce.git" + }, + "keywords": [ + "ce" + ], + "author": "Microsoft", + "license": "MIT", + "bugs": { + "url": "https://github.com/Microsoft/vcpkg-ce/issues" + }, + "homepage": "https://github.com/Microsoft/vcpkg-ce#readme", + "readme": "https://github.com/Microsoft/vcpkg-ce/blob/master/readme.md", + "devDependencies": { + "@types/node": "17.0.15", + "mocha": "9.2", + "@types/mocha": "9.1.0", + "@typescript-eslint/eslint-plugin": "5.10.2", + "@typescript-eslint/parser": "5.10.2", + "eslint-plugin-notice": "0.9.10", + "eslint": "8.8.0", + "@types/semver": "7.3.9", + "typescript": "4.5.5", + "source-map-support": "0.5.21", + "shx": "0.3.4" + }, + "dependencies": { + "@microsoft/vcpkg-ce": "~0.7.0", + "yaml": "2.0.0-10", + "semver": "7.3.5", + "txtgen": "2.2.8" + } +} \ No newline at end of file diff --git a/ce/test/readme.md b/ce/test/readme.md new file mode 100644 index 0000000000..a80ef3149a --- /dev/null +++ b/ce/test/readme.md @@ -0,0 +1,16 @@ +# vcpkg-ce Project + + +# Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + diff --git a/ce/test/resources/2020-05-06-VisualStudio.16.Release.chman b/ce/test/resources/2020-05-06-VisualStudio.16.Release.chman new file mode 100644 index 0000000000..1edd751918 --- /dev/null +++ b/ce/test/resources/2020-05-06-VisualStudio.16.Release.chman @@ -0,0 +1,35 @@ +{"manifestVersion":"1.1","info":{"id":"VisualStudio.16.Release/16.9.4+31205.134","buildBranch":"d16.9","buildVersion":"16.9.31205.134","commitId":"a3f84039d0bc8d5d7aec0413df8996074be92b46","communityOrLowerFlightId":"9275227367e942f","localBuild":"build-lab","manifestName":"VisualStudio.16.Release","manifestType":"channel","productDisplayVersion":"16.9.4","productLine":"Dev16","productLineVersion":"2019","productMilestone":"RTW","productMilestoneIsPreRelease":"False","productName":"Visual Studio","productPatchVersion":"4","productPreReleaseMilestoneSuffix":"1.0","productSemanticVersion":"16.9.4+31205.134","professionalOrGreaterFlightId":"65da37c6b6f2407","qBuildSessionId":"0b06647a-e115-b73b-7734-d7edcfb674ff"},"channelItems":[{"id":"Microsoft.VisualStudio.Manifests.VisualStudio","version":"16.0.31205.134","type":"Manifest","payloads":[{"fileName":"VisualStudio.vsman","sha256":"6c99749816b688015ce25f72664610e3b5e163621a2e3dd19b06982b5c9cb31f","size":18413665,"url":"https://download.visualstudio.microsoft.com/download/pr/3105fcfe-e771-41d6-9a1c-fc971e7d03a7/6c99749816b688015ce25f72664610e3b5e163621a2e3dd19b06982b5c9cb31f/VisualStudio.vsman"}]},{"id":"Microsoft.VisualStudio.Product.BuildTools","version":"16.9.31205.134","type":"ChannelProduct","icon":{"mimeType":"image/svg+xml","base64":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+DQogIDxzdHlsZT4uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO2ZpbGw6I2Y2ZjZmNn0uYnJhbmQtdnNpZGV7ZmlsbDojODY1ZmM1fTwvc3R5bGU+DQogIDxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTQwIDQwSDBWMGg0MHY0MHoiIGlkPSJjYW52YXMiLz4NCiAgPHBhdGggY2xhc3M9ImJyYW5kLXZzaWRlIiBkPSJNMzAuMjIxLS4wMDJMMTMuODg3IDE2LjE2IDQuMDUyIDguNzQ2IDAgMTAuMTAyVjI5LjlsNC4wNTIgMS4zNTYgOS44MzUtNy40MTQgMTYuMzM0IDE2LjE2TDQwIDM1Ljg0MlY0LjE1OGwtOS43NzktNC4xNnpNNC4wNTIgMjUuODlWMTQuMTExTDEwLjAwNCAyMGwtNS45NTIgNS44OXpNMzAgMjguNDcyTDE4Ljk4MyAyMCAzMCAxMS41Mjh2MTYuOTQ0eiIvPg0KPC9zdmc+"},"isHidden":true,"releaseNotes":"https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.9#16.9.4","localizedResources":[{"language":"en-us","title":"Visual Studio Build Tools 2019","description":"The Visual Studio Build Tools allows you to build native and managed MSBuild-based applications without requiring the Visual Studio IDE. There are options to install the Visual C++ compilers and libraries, MFC, ATL, and C++/CLI support.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"zh-cn","title":"Visual Studio 生成工具 2019","description":"Visual Studio 生成工具允许生成本机和基于 MSBuild 的托管 .NET 应用程序,而不需要 Visual Studio IDE。还可以选择安装 Visual C++ 编译器和库、MFC、ALT 和 C++/CLI 支持。","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"zh-tw","title":"Visual Studio Build Tools 2019","description":"Visual Studio Build Tools 可讓您建置原生及受控 MSBuild 型工具,而不需要 Visual Studio IDE。選項包括安裝 Visual C++ 編譯器與程式庫、MFC、ATL 及 C++/CLI 支援。","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"cs-cz","title":"Visual Studio Build Tools 2019","description":"Nástroje Visual Studio Build Tools umožňují vytváření nativních a spravovaných aplikací na bázi platformy MSBuild, aniž by se vyžadovalo prostředí IDE sady Visual Studio. K dispozici jsou i možnosti pro instalaci nástrojů, jako jsou kompilátory a knihovny nebo podpora Visual C++, MFC, ATL a C++/CLI.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"de-de","title":"Visual Studio Build Tools 2019","description":"Die Visual Studio Build Tools ermöglichen Ihnen die Erstellung nativer und verwalteter MSBuild-basierter Anwendungen, ohne dass die Visual Studio-IDE erforderlich ist. Es stehen Optionen zur Installation von Visual C++-Compilern und -Bibliotheken, MFC, ATL sowie C++/CLI-Unterstützung zur Verfügung.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"es-es","title":"Visual Studio Build Tools 2019","description":"Visual Studio Build Tools permite compilar aplicaciones nativas y administradas basadas en MSBuild sin el IDE de Visual Studio. Hay opciones para instalar los compiladores y las bibliotecas de Visual C++, MFC, ATL y la compatibilidad con C++/CLI.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"fr-fr","title":"Visual Studio Build Tools 2019","description":"Visual Studio Build Tools vous permet de générer des applications MSBuild natives et managées sans passer par l'IDE Visual Studio. Il existe des options pour installer les compilateurs et bibliothèques Visual C++, ainsi que la prise en charge d'ATL, de MFC et de C++/CLI.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"it-it","title":"Visual Studio Build Tools 2019","description":"Visual Studio Build Tools consente di creare applicazioni native e gestite basate su MSBuild senza l'IDE di Visual Studio. Sono disponibili opzioni per installare le librerie e i compilatori Visual C++, MFC, ATL e il supporto C++/CLI.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"ja-jp","title":"Visual Studio Build Tools 2019","description":"Visual Studio Build Tools では、Visual Studio IDE を必要とせずに、MSBuild ベースのネイティブ マネージド アプリケーションをビルドできます。また、Visual C++ のコンパイラやライブラリ、MFC、ATL、および C++/CLI サポートをインストールするオプションも用意されています。","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"ko-kr","title":"Visual Studio Build Tools 2019","description":"Visual Studio Build Tools에서는 Visual Studio IDE 없이 네이티브 및 관리 MSBuild 기반 애플리케이션을 빌드할 수 있습니다. Visual C++ 컴파일러 및 라이브러리, MFC, ATL, C++/CLI 지원 등과 같은 도구를 설치하는 옵션이 있습니다.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"pl-pl","title":"Visual Studio Build Tools 2019","description":"Narzędzia Visual Studio Build Tools umożliwiają tworzenie natywnych i zarządzanych aplikacji opartych na narzędziu MSBuild bez korzystania ze środowiska IDE programu Visual Studio. Dostępne są opcje instalacji narzędzi, takich jak kompilatory i biblioteki Visual C++, ATL, MFC oraz obsługa C++/CLI.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"pt-br","title":"Ferramentas de Build do Visual Studio 2019","description":"As Ferramentas de Build do Visual Studio permitem que você compile aplicativos nativos e gerenciados com base no MSBuild sem precisar do IDE do Visual Studio. Há opções para instalar as bibliotecas e os compiladores do Visual C++, MFC, ATL e suporte C++/CLI.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"ru-ru","title":"Visual Studio Build Tools 2019","description":"Visual Studio Build Tools позволяет осуществлять сборку собственных и управляемых приложений на базе MSBuild без использования среды Visual Studio IDE. Существуют разные варианты установки компиляторов и библиотек Visual C++, ATL, MFC и поддержки C++/CLI.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"},{"language":"tr-tr","title":"Visual Studio Derleme Araçları 2019","description":"Visual Studio Derleme Araçları, MSBuild tabanlı yerel ve yönetilen uygulamaları Visual Studio IDE kullanmanız gerekmeden derlemenizi sağlar. Visual C++ derleyicileri ile kitaplıkları, MFC, ATL ve C++/CLI desteğini yükleyebilirsiniz.","license":"https://go.microsoft.com/fwlink/?LinkId=2086102"}]},{"id":"Microsoft.VisualStudio.Product.Community","version":"16.9.31205.134","type":"ChannelProduct","icon":{"mimeType":"image/svg+xml","base64":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgOTYgOTYiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSI0OCIgeTE9Ijk2IiB4Mj0iNDgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxjbGlwUGF0aCBpZD0iYiI+DQogICAgICA8cGF0aCBkPSJNNjguODkxLDk1LjZhNS45NzYsNS45NzYsMCwwLDAsMy45MzMtLjQzOUw5Mi42LDg1LjY1MUE2LDYsMCwwLDAsOTYsODAuMjQ0VjE1Ljc1N2E2LDYsMCwwLDAtMy40LTUuNDA4TDcyLjgyNC44MzlBNS45OCw1Ljk4LDAsMCwwLDY2LDJMMzQuMTE4LDM3LjI2NCwxNS41LDIybC0xLjYzMS0xLjRhNCw0LDAsMCwwLTMuNjEtLjgzNCwzLjk0NywzLjk0NywwLDAsMC0uNTMxLjE3OUwyLjQ2MiwyMi45NzRBNCw0LDAsMCwwLC4wMTEsMjYuMzY2Yy0uMDA3LjEtLjAxMS4yLS4wMTEuM1Y2OS4zMzNjMCwuMSwwLC4yLjAxMS4zYTQsNCwwLDAsMCwyLjQ1MSwzLjM5Mmw3LjI2NiwzLjAyN2EzLjk0NywzLjk0NywwLDAsMCwuNTMxLjE3OSw0LDQsMCwwLDAsMy42MS0uODM0TDE1LjUsNzQsMzQuMTE3LDU4LjczNiw2Niw5NEE1Ljk2NCw1Ljk2NCwwLDAsMCw2OC44OTEsOTUuNlpNNzIsMjcuNjc3LDQ3LjIxMiw0OCw3Miw2OC4zMjNabS02MCw2LjZMMjQuNDExLDQ4LDEyLDYxLjcyN1oiIGZpbGw9Im5vbmUiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPg0KICAgIDwvY2xpcFBhdGg+DQogIDwvZGVmcz4NCiAgPHRpdGxlPkJyYW5kVmlzdWFsU3R1ZGlvV2luMjAxOTwvdGl0bGU+DQogIDxnIHN0eWxlPSJpc29sYXRpb246aXNvbGF0ZSI+DQogICAgPHJlY3Qgd2lkdGg9Ijk2IiBoZWlnaHQ9Ijk2IiBvcGFjaXR5PSIwIiBmaWxsPSJ1cmwoI2EpIi8+DQogICAgPGcgY2xpcC1wYXRoPSJ1cmwoI2IpIj4NCiAgICAgIDxpbWFnZSB3aWR0aD0iNDAiIGhlaWdodD0iODIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMiA3KSIgb3BhY2l0eT0iMC4yNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFDZ0FBQUJTQ0FZQUFBQW1SNWJLQUFBQUNYQklXWE1BQUFzU0FBQUxFZ0hTM1g3OEFBQUVlMGxFUVZSb1ErMmFXM2ZiSUJDRUIxK2FTOU1tLy85M05va2RKMVlmWU1Sb3RTQnM2cllQbW5QMllFblkraklzcUJVYmhtSEEvNnpOVW9kL3JkMVNoMHNVUWdpbGE4T1ZROVVGV0FCeUlXM1hWdURRMkc4aUF4WUtyV29vdEl1Z0Z3RUttTFlhOXBvQ2FlaTVLbVRURUJmQU5zaGcrbGxoZ1FoeGR0cnhlZ2dobENDcmdBdGdteFJiK2F5Z1FJYlIrRXJYZWN4K3JvcUFCazZkSWxRcEZGQ2h2Z0I4cG10ZnlEb0RRQWpCSFdvWDBJR3pZTHNVZS9tOFEzYVQrVWU0VXdwZUEzSU9qcm5xRFhWdGlCVk93ZllwdnBrZ0pCZi9BUm51QThEUlhOTlE2SWxtZ01rOUM4ZWJFK1lld0YxcTd6R0ZKSVRDSFRDRnMzazV1bWcxQVhUV053VzhTL0dRNGxFK0U1S0FBMksrSFJIaHR1azNPZVNmeVBtbzk1ekpHMkxQdlcrSWNJOHBuZ0I4VHkxQjc1QW55Um5aT2Q1REhkV1pQNEcwZVZqS1FaMFVCTHhIaFBtUjRtZHFuekFGQktJN0I4UmM1ZkV4SFN2Y29rYkF3c3pkSWs4SUJYd0c4SUlNK1pqNmNIZy9rT0ZPeUxBNnl5SHRxS1ZaN0UyUVBYTHVjVmdKK0lJSStJQU1TTGNDc3BOMkNhSUcwODVVR21JdkIrbmdFN0tMejhnT3FtTUVmVWVHczI0dHdnSHRrNFF1S2lSQmZ5STZ1RU84MlJGeGtuRDJlb3V6UmxVdFEweEF1c2lsNVhzS3p1SXQ4aVBzaUF3SHpLSE96cm5sZFZCVWc5UUZtbkdYK3ZLWjZ6bW5DN01MVlh3V093dDB5VWtGMWNmZEh0a3RYVUk4NXp6SXBrbmlRVzVNTU9FSnVwTUl5UCtVVWltWUIxWE5RN3RZenRZbHpHRjFvV1ZZMXlrdno1cWNvMG81Q0V4djVnMjd3dkVQOWR5N0dFcGxIZlJrNGJ4VThGUURhWVp0QWZUa2dkVkFiK3BnaTBwdzNib0c4R1l3bnE0Qi9LdGFBWHUxQXZacUJlelZDdGlyRmJCWEsyQ3ZWc0JlcllDOVdnRjd0UUwyYWdYczFRcllxMnNBTDNxMzBxdHJBRDNkRFBwYVFPOHRWUW15OXZwdVVTMkF0ZGRudGRkcE5aQm0yQnFnM3R3QzJwZmgrdjVaNWJtM0NLV3lyNEE5Tnl6VUY2YUFoTk8rVkFtd0dWWUJCK1F2ZUU2ZEViZTN1Tjk3UXQ3MzVZdDFoYVZDdXJhQkQxZUYzQUhBTUF5RDdKVjR3Nm1iME56elpmdUJES0R1QW5PZ0RjcXdiaTZYM3ZKN2NGcDdjSkRZSTk2QVcyRjBXZDJzd1kxcUthb29PYWUxQis4cFhoRjNtYmFwdjI0bUtpVGdPK25sNDh4Rno4RVNJSjE3QS9BTGNYK08zLy9FZkFQN0JOOUpMdytMcWcyeEF0SzlOMFF3RmxDRTFPOEQ4dzN0UXpwUE43M0pvNjJybGlHbWUrL0lOVE5hUEhHQ1h4THdscjVqaDl3dVE5ck9OQUxLVFBZQXRZS0RoUk1Ec3J0MDFoWlZ2S1gyQ0gvSVp6blhXdlZ4UnZ5cmRJZzVBNEg0dzRRL29GNlc4b284M0FySkNWUlZhWklBMlVHYjBIU1g0TzlZTHV5aGs1cVQraFFhSGFzdU04NkNyWkQybk03c2x0SW81aU9mUURNNFR6TUhFeVFQZFJocXVkbGFYSGJFM01VcVpHMi9XSWVheDNiNW9ZdUUwMW9GKzhkb2VJQXVwQXRvWmpRdy9TSG1EaUczSmpSZnRhOCt6MTMzdktLS2FwSHREVXRFOVhoQVpITkJtcXFBRjBEdFp3VUVNcVRYamc1MkFWSUZVQXVsTFg5OE1LSG5xbVhLRndGU1RwMk4xNnFHUXZ0bkM3MnRET2g0MmprSG1GbTZCRVoxQVZvVmdBRzBBMW45VWNCYnFPWC94ZjlVdndFRXhuUTEwSmJTZmdBQUFBQkpSVTVFcmtKZ2dnPT0iIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpzb2Z0LWxpZ2h0Ii8+DQogICAgICA8cGF0aCBkPSJNMTUuNSw3NGwtMS42MzEsMS40YTQsNCwwLDAsMS00LjE0MS42NTVMMi40NjIsNzMuMDI2QTQsNCwwLDAsMSwwLDY5LjMzM1YyNi42NjdhNCw0LDAsMCwxLDIuNDYyLTMuNjkzbDcuMjY2LTMuMDI3YTQsNCwwLDAsMSw0LjE0MS42NTVMMTUuNSwyMkEyLjIxMywyLjIxMywwLDAsMCwxMiwyMy44VjcyLjJBMi4yMTQsMi4yMTQsMCwwLDAsMTUuNSw3NFoiIGZpbGw9IiM1MjIxOGEiLz4NCiAgICAgIDxpbWFnZSB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEwMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEyIC0xMikiIG9wYWNpdHk9IjAuMjUiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBSGdBQUFCbENBWUFBQUNHTENlWEFBQUFDWEJJV1hNQUFBc1NBQUFMRWdIUzNYNzhBQUFMVDBsRVFWUjRYdTJiNjFiYk9oU0V4N1JBNmUyYzl2MGZzcWZjQzhYblJ6VFJlSHZMbGhJNUpJcG5MUzNuWXFETng3Nk5STmYzUFZhMXE0dTVHMWFkdGo3TzNiQnFySzdydXJsN3FQNmRVK1FLT0ZNTzFDekkrbVh2QVhzRm5DR0JxMWY3bWxVdjF4NTRIOWdyNEFrWnNGTUxjclZndmJXRnZUVG9GWEJDRHR3THVlcXlvQlhrVzFpOWM5MTg4NFZCcjRBZEJiaTZDUE9EV1JZME1BVDdCdUN2dWVwYUhQUUsyTWlCUzVBZnpickVFTFlGL0Rlc1YxbC9aUjBFOUFwWVpPQnExRjVpODFsZHlicVV4WHVCRFN4Q2ZNRUc3SXRaQkw4NDZCVndrQU9Ya1VtSTEyRjlraXRoTThvN3hPaDlCZkJuWXRuSVhnVDBDaGl6Y0JYc1p3QTNjaVZrUmpFQk0yci9BSGdPNnlrc1B2K0RHTkdMZ1Q1N3dBbTRyTEZYMkVDOEFmQWxySy9oK2ptc2E4UjZiQUVyMkVkWkNudFIwRjNtZlUwcUF5NmpsV0MvQWZnZUhuOUZqR1RXYUdBRGcrbjVHUkhxZzF6NWVCZlEyMWs2Qi9MWlJ2QU1YRTNKQ3ZhZnNBajVDemFBV1ljN1RBTytEK3NCMDZBSjIrdTgyYVdqNjdwWnlHY0plS1piWmxvbVhJTDlOeXhDL29ZWXdWZllmSmJhWkRGRnB3QmIyTHlQYWQyQ3ZrQUVEV1E2WW1jSE9PRlFlV21aY0FuMkI0YVFHY0ZzdEFpWVl4SUJLMlFMOTg2OFJ0Qlg4QnN5L2JjemJRTlNvNjNPQ3JBRDEydW9QTGcvc1FIOEE4TVUvVGw4RFp1c0MwVEF0dEY2RFBjL1lQT0xjUitlRS9RTkl2aHJSTmpXTmV2Qzl3Wk04K1ZGOFZrQkRySnBtUVlHNFg3Qkp2MHlMUlB1VDhUby9ZWllmNi9EOTlBYWJHZmhtN0NlRUNHekM3K1Q5emxqYy9UaTkxWFBtMkt6dGUyeXU2N3JMT1N6QVN4MTE0TjdqVGdLS1Z4R0xlSCtpMDMwY2t4aTlCSUVwVzdXRlRhUnpNYU42OFk4bndLcjZwM0ZlODR6Z2hNZE05UHlIRnl0dlpxYTJWeHBlcVlZV1c4WWxnQmQxL0tZVUQyd0dxbk1ETW5SeVVaeDg0QXp4aUdGcXgyendyV2pFU09PWU5TTEJvWlErUE5lRVNFcVRBOG9RZXBtaFoyUHVmVC94Ni9mcW1uQUdSWmthaHl5SGJNMzk3SnoxdVpIMVlmWDMrUWVyYVVLUkpzeTdiNmZNTnpZNEMrR2ZpLytETmIvZ1pvRlBCRzVUSStmRUIycUhMaXN1UXAzcWdIcTVKcHFqbDR4M0pYU3FMYkxRdVVWR1A5eWJhVnBwUmxsV0pEYU1YL0gyTXpRYmxsckx1dm1GRnpJYTUzejNJdDA3WWJ0ZFZ0Zmc3eWZOWHdoam9QdEFaNXhxVmh6VTJuWnE3bTJvVXJCVFVXcVB0WkdTVk15WGF0bkRNME5hMW1PTmgvbTFCVGdESmRxcXVhbUdpb2J1YmFXUXE3MmcvY2FKdDFHZkVLMEt1L05VcDlhM2F4WCtGMzBWazEyMFRNdWxUWlVUTXVha3FjaVY3dmxWT1FDNDJoVnVCWXNiY3M3QUxjQWZvZjFuenkrRGU4VE5DR3prN1p3M2FodUJuQ1FUY3ZhVk5tYW00cGM2MUpadU5veHArQjZxZmdQb21XcGNQK1Q5UXNSOEIxOHdFVXB1d25BR1M2VldwQjJBMEVCSzF3OXJiRUxYSjFmMVk4bVhFYXNCL2NXTVUzcnBvUHVGelB0OTJZTmRQS0FNMTJxejVoMnFiUmpWcmc2dHRpMHJCK21UY25jVGRJdFE5WlpDL2VYUEU3QnpUa0lBQUNqRFllVEJselJwZEp4U0NPWFRaWFhJZXRqL2JDOWV2dUFjYjBsV0Y1dFd0WTk0UlRZMUNpMTFja0NydVJTc2VZcVhEMWo1WFhNMUZ3enhaUk11TnBJS2R5cHRHelRjV3BHN29GeDlBSW5DbmhCbDJxdVk2WnltaW1ia3IybzFjaWRndXRGcTliK2RrNTBaTHBVSGx4ZEtaZHFGN2hlTTBXNFRNbGFieFV1UjZGN0RHZGVDemNKRmtqREJVNE1jRUhONVVHNWZXZGRGVDlFVzI5Zk1UWXV0Tjdtd3MwWmd3WWc1dzdjQVNjRU9NT0N0S2NnNStCNnMrNlVpZUhWVzhMbHFVaTZVRHJmS2xqVzNKU0o4WUp4dmRXb3pZS3FPZ25BaFJZazRYcWpVRTdrMHI3dE1JeWNxWG83MVV3cFlOdE1FUzdISUoxdHQ1RmJDbFYxOUlBTExFZ0xsNUZyeDZFNWw0by95NE9iWTE3WVpvcVBiekhkS2F2SHZJM2FmZUFDSndBNHlLYmxsRXVsYVZrUHlWa0wwbk9wdE9acXZlMnhtM21ScXJjNjR5WmRxWDNCVWtjTnVOQ0M5RXdNQld6aFRuWExXbTlMelFzRlc5Sk1WWWNMSERIZ0NoYmtEd3pocGc3S2VkRnJtNm5jZXF0WHJiZjNpSTJZZGFhcTFWdFBSd200WUJ5YXN5Qzk0emFFUzM5Wk5kZE1sWmdYWHIwbFhLOVRyZzRYT0VMQWxTeklPU1BEcHVWZUZqLzhHdWFGNXltUG5La2x3RkpIQlhoQkM5SnVIbmhwZVNuell0S1pXaEl1Y0VTQU15M0lmUS9LZVhDMVV6NkVlYkZZdmZWMEZJQXpYS3A5RDhwNURkVSt6WlFDUHJoNVVhSjNCMXpvVXVYQTlRN0syVmxYNFo2Y2VWR2lkd1ZjNEZMVk9pZ0hqRHZsa3pNdlN2U3VnSU5zV3ZhTWpGMFB5bG13UUFSTHVDZG5YcFRvM1FBWHVsUWxCK1Z5SXJlazN1cjFLTXlMRXIwTDRBb3VsZTJZUFgvWnpybk5tQmNsT2pqZ2lpNlZkMURPUm02cVV6NXA4NkpFQndWY3lhWEtPU2lua2F2T1ZCUG1SWWtPQm5oaGwwb2pGL0RIb0diTWl4SWRCSENtUzdYdlFUbU4zRjJhS1FWODFPWkZpUllIWEZCemM4OVNwVndxSUVhdVYyK2JNUzlLdENqZ0RBdHkxNE55SGx6QWI2YWFNeTlLdEJqZ1Fnc3k5NkJjNnFpTjdaU2JOaTlLdEFqZ0FndHk3cURjbkVzRnBKdXBaczJMRWkwQ09NaW01WlJMcFduNUo2WXRTSGJNMXNBZ0NJNHdUWnNYSmFvT3VOQ0M5RXdNMnpFcjNGUmFwdWx3RnVaRmlhb0NybUJCL2tDNjdtcERwYnRCbm5seGgyRjluWUo3Y3VaRmlhb0JMaGlINWl4SVBRVnA5M1FCZnd5eXpaUUg5aGNhTVM5S1ZBVndKUXR5eWwvV2hrcmgydmsyQlZjajkrVE5peEx0RGZnQUZxUzZVem9HZWVaRkNtNnFtVHBKODZKRWV3SE90Q0JMRDhyWmhnb1l3cldkY3FxWllrcitqY2JNaXhMdEREakRwY285S09kMXk1cVdDZGZ1NGRxZElLMjFGdTVEV0UyWUZ5WGFDWENoU3pYWFVFMmxaVWFYWnp1V05sUE5tQmNsS2daYzRGS1ZISlJMd1UxMXlydnNCRFZqWHBTb0NMQ0JPMmRrekIyVXN6dERIbHoxbFBmWkNXckt2Q2hSTnVBRTNJOUl1MVE1QitYVXhPZ3dISU1JdDJRbjZCYXgzdHBtaXAxeXMvWFdVeGJnek1oVkl5TmxRZEpmOWh3cTFrUENmUXhyMTUyZ1p3enJiVFBtUllrbUFUdjFWc2NoYTBHbVpsMEM5allQMkMwVExqdGxiNXZQZHNxMjNqYTNFMVJEU2NBSnVIU292TWlkTXpJMExWOWpHTGs2NDNyTmxBWHJOVlBON1FUVmtBczRNUVpOcGVXY1U1QWF1WVRMdEttZGNtNjliWDRucUlhbVVuVEt4UERPVXFVT3lsbTQybENwZ2FGd1M3YjVtdDRKcXFFUjRFVDBsc3k2VE1rSzE4NjVUTXUyVTdiMVZsUHoyZTBFMVZBcWdsUFI2NDFEdXYzSGxkclBCWWI3dU53dzhKb3BCWnd5TDVyZUNhcWhBZUNacnBtMTl4UEdQck11UmkzaFhtTG9LK3RXbjdlSGE2TTNaVjQwdnhOVVExNEVkL0hoeU5Ed1VqVHJNSSsxV25lS2NCbGxkc1BBc3gzbm1xbXoyQW1xb2JrVWJWMHJiYkkrSVVZei83clBPNEh4SnN2YkRkS28xU2oybXFtejJnbXFvWnd1Mmh1VEZQYWx2TVlPR1lqcDJJNUNVMm5aMXR2VnZOaFRPVlpscWk3cmN5Qis0Qng5T214Z3NHT2VHNFdZcWxmem9xSnlBUGR5NVNKSTNkTGp5UE1oM0VQSU5uclpOQkV1UWEvbXhRSktBZTR4Qmtxb0JLb0doYzY0YktRK21PZFBpTkZMd0wvbCtXcGVMQ0FQY0k5b1JpaGNqZFFuYktEcTN3ajE0UjYrcDkzek15SmcxbDlkM3ViOGFsNVUwQUJ3My9kOW1JVTl1Qit3K2VEWlRPa2VManZrUjhTL0g5S05CTjBoSW1SR3JEZmZydVpGSlUzVllGdHZYeEM3YWUyVStkNFR4b2ZtR05WOG41QUptbnUrT3Q4UzdtcGVWTkJVRFFZMkh6RHI2RlRIL0FUL1R6c1p3YXpYSEhrZTViR1hrbGZ6b3BKR2dDVk5BeEhrNEJaRWNQWm9qVWF2cG0vZXgzdWY1YmxOeVdzelZWRnVCQWZJZkxxdGZ4alhaUVdzWm9jQ1pwclc3cHRMall1MTNpNmd1VGxZUDJCTm16b0h2MkRZZUtrSkFveEhMRjd0Ykx2VzJ3WFV6WDJPaWYxaGExOWVtTFVOZi9pL0ZHL3d3YTRwdWJKbUFRTWp5THhxcEY2WXE5N2JZd2o1VFI3cmUydlVMcUFzd0ZRQ3RGMzZQakNzMzNadDMxL2hMcU1pd0pTQUJzWlE5VDFWNzExWHNNdHFKOENVQWIxOTJYa05HRFpzSzlnRGFTL0FWZ25nVzYxUUQ2K3FnRmNkbnk2d3FtbjlEL1FHdkxnc0FUTDVBQUFBQUVsRlRrU3VRbUNDIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6c29mdC1saWdodCIvPg0KICAgICAgPHBhdGggZD0iTTk2LDE1Ljc3VjE2YTMuNzg2LDMuNzg2LDAsMCwwLTYuMTg3LTIuOTI4TDE1LjUsNzRsLTEuNjMxLDEuNGE0LDQsMCwwLDEtNC4xNDEuNjU1TDIuNDYyLDczLjAyNkE0LDQsMCwwLDEsMCw2OS4zMzNWNjlhMi4zMSwyLjMxLDAsMCwwLDQuMDI0LDEuNTQ5TDY2LDJBNS45NzksNS45NzksMCwwLDEsNzIuODIzLjg0MUw5Mi42LDEwLjM2NEE2LDYsMCwwLDEsOTYsMTUuNzdaIiBmaWxsPSIjNmMzM2FmIi8+DQogICAgICA8aW1hZ2Ugd2lkdGg9IjEyMCIgaGVpZ2h0PSIxMDEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMiA3KSIgb3BhY2l0eT0iMC4yNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFIZ0FBQUJsQ0FZQUFBQ0dMQ2VYQUFBQUNYQklXWE1BQUFzU0FBQUxFZ0hTM1g3OEFBQUw2MGxFUVZSNFh1MmQ2M2FiU0JDRWF5UWx6bVYzMy84OWQ1UFljV3oyQjVRb1N0MHpZRXV5THZRNWZXYTREUW9mMWRNMFBxRjBYWWZWYnRjMnJSMVd1MjdidFhaWVlxV1VVdHZlcmVIaTdGYmVjODBUb0Jua3lZbFcyT2V4TnluWXdKYWtkZXUwNVJBcjZOUGFJc0FDVmx0MzNRNU13YXIzTzY2Z1QycXpRblFDZG9NUjZzWmEzVmVodmc3ZVNic0h2a0krdmxVVkhJUmloN2tCc0pYK0JsUEl3QlR1eStDdjVoMkFycFN5UWo2eXBZQk50UXBYb2U2R1BsdXUxM0JOaUM4QS9rakxmcEY5MXBCOVpBc0JCM0JWbllUNWFlaC9rajYzRVRMVlM2RFA1Z29ha05CZFNpa3I1UGRiTFVSSHFsV2duOFVmWk4wT1U4Q3Y2Q0grSHZ4cGNDNXYwTU11bUlKZVEvWVI3QUN3cVZmaEt0aUh3YjhBK0RxMEQ4TzJUeGhWM0tHSDlvd2U2dVBndjRaVzFhNlFDUm9yNVBkWnBtQ2ZkeGwrUDZPSCtRWEFOL092R0NGVHhRelB6eGpCL2h6Mis0RnBTS2VTYWV1OGZBU2JBRTdtWGlaUFZDL2gvaVgrSFZQSW56QlY4RytNY0gvSVBxcDJUYzZlc2M3TFI3Rkl3UkZrenIwTXoxL1JRLzBMd0Q4QS9oNzYzOURmQUorSDQ0QWVGTVB6RDB5VlRzQ2FnVWNoRzFnaHY4bGFJVnJuWUZleFF2NGJQZWp2R0FGdU1RTDZqUjR3YndBcWVDZnV6OU9FckxZbVh3dXRWYW9rNUlJcFpGVXpRelpoZjhjSXNLQlhIcE1zd3RjUTdTcld4RXZEdGhaSTF1UnJwdTBCSjRXTi9XWk1MM2owNktUQXZ3N0xXNHp6OEJQRzBNendySUFqRlN0Z0Q5ZHI4alhEV2dwVzZ4SlgwMGVxQjR6WjlDdEc1U3Jjblhta1pJZE1XNU92R1JZQmppQnF1VkZMalY2VmVzRUlYUit2Z0JHc1ExVzREbG9CYjlEUDVhN21kVjZ1MkI1dzEzV2R2VndBRHVFK1k2eEdhZEdDWVprVktpMC9VdFZBRzJ5a1lwK1hQV1N2UlpHS1pTSGExVXUxN3RCRC9HVCsyVnkzT2JRTkR1SHRyQjhkazgzTHdGb1VTUzBMMGN4K3FaSU54bnF5WG1nSE5HZE8vWXdwTkFlZEhWY0R2QlpGRXBzQWxqRE5DMFBJZnhCbjFacGRSOHJNQVBsTlVqdDJqcHJYb2toaXRSRE45blhvKy94TXE0R09ZTENNdVV1T3lSUmRBMDNJYW12eWhRQ3dKVnNLK0EvaVJ5TmdDa3BWNlJDSzlMZkJjVFhZbVhNOFBjZGFGQmtzVlBBQWViOG9iUmNzQTdHS004RHFEaWdDWEp2ZmErZFlpeUxJUTdSRDVsdzgyV1ZvaTdRWllMOEJIUEpPMW1kcXprSzFoMndkbTNhM3lWY0tHRWlUcnJsaDJpKzRBdVQrYkFtcHBtUlZzZmNkOEFiOTQxeWs1cnVDWEFVTWpDRnRVRE1mb1lCK1RnYnE2blJWTzJodEZiS3FtNkJyY0JVeTI0SnA1WXUvOTY2U3J5WmdtcW1aaWRlK2lvUVlxc1BONE90K08rblhsTndDcmVlbDNWM3lOUnN3Y0FDNXd3ZzRnNmNBRmFJRG9QTVZveXA1RHVnTWJuUmozVlh5dFFnd01JSDhpdjRDcW9wcEdlZ01zSUp3eU5FeEdkeldzektkZHZQSjEyTEFRSnA4UlhPeUxpdk1HdUFJc283aDhKWUEzdURPM2tpOUNUQVFQa1lCNHhzbnRSYmdDSGpCOUZrM0c2T2w0QWgyTWIvcE4xSnZCZ3lra0dtdXZBaEtCamM2MXRmVlFFZkFhK2NDYnZTTjFMc0FBMDNJUUE2NmRjSFpNbHhIa04wejBITUEzK1FicVhjREJsTElCZU5ibmhaY1ZhUURMaGlWeUg0RXVnYVhnR3V3Yi9LTjFGRUFBeUhrMmlOVXBxWk5jZ3lOa05uNnNSRnNMNUJrcWlaa3RhdFB2bzRHR0dpR2F3Y1JnZWZCM3VyeEJGTzdhVnl4TGZjSVVuQWpSWkdqQWdZbWtCVndCak9DMU5wSDk2T1NnU2trVjdKWHZsVFZXVFFoWk9DS2s2K2pBeGJ6a3VaY2FBb3BnZzZNaVpkQzluRWlCV2VoMmtPMm53KzQwdVRySklBSEZSY2NsalRWaXJRWjZHaVpyb25YQm5ueUZhblpGUjBCM3VBR2lpSW5BUXpNcm5acFB3S2RlWFFEUkVwMndEVzQyYnlzZm5WRmtaTUJCbVpYdTk0S1ZtK1M3ZEJtKzZpQ28zQmR5NjU5VE9DS2lpSW5CUXcwTTJ1MXVZQ2pDKzdyZFR3Zk13TzlCUERWRkVWT0RoaG9RczRVcDBDeUM2NjJrMjBPSlJvdmd6dEh6VmRURkRrTFlLQUpHY2lCMU5Ua0R2UlFmRjAwOXRiNkhxNXJvQWxaN1NLVHI3TUJCbzVXMG94QXE3bUNIYmlQNHdwdU9jZldjMXhzVWVTc2dJR2psRFFqRmZ2eHNHUG1qTzBoZW1lZWhXdUZERnhZOG5WMndFQXpYRHVBQ0V3R2JUL28wTy9zT0NBK0psSnlGcW85WkVmbnZwams2ME1BQTR0TG1xcmVKZEJoMnhpdUVSeWZxVGw3blBLb2NwRkZrUThETExha3BMbXhmaFF1YXdvSFlpVkhnR3R3czNsWi9TS0tJaDhLZUZCeFFidWtHWUd1dVVPRnJkYytnckZyNFhyT1k1U08vYUZGa1E4RkRNd3VhVG9JRFkydEM2em1ZRGZvcjBGMjgyU2dsd0QrMEtMSWh3TUdacGMwZ2ZtQS9VSTcxT3pHaVFDemplRE9VZk9IRmtVdUFqQXdPN091S1N4VGtoci92ZHpPUGkrMDN4QU9Xcy9wYzNUMld3aFo3V3pKMThVQUJwcVFnUngwQmpkU2NBUzVCTXZST1Z6QkxlZVkrcHZPV2hTNUtNREEwYXBkZmxIZE9POUM5aWxvS3psU3NhcFpWVjI3NmM1V0ZMazR3TUJScWwxY3oyUDBXRjIzbFQ3WDF5SUF0N3RTczFEdElWdkhvWjAwK2JwSXdFQXpYRHZjREFhQ1phN1Rkb040SFpjZGNLYm03SEhLSTh2WmlpSVhDeGhZWE8yS1FMVDJjYWphWnVzaXdEVzQyYnlzZnJLaXlFVURGbHRTN1hLNDJUeEljTEJqdU53TjYzUmVacXRqMThMMW5NY28vUzFITDRwY1BPQkJ4YnpndFdvWDJ3eDBheHVOVUl1MEROZE16blFNRDlVT2VnbmdveGRGTGg0d01MdmFwZjBJWnVRUllHQUtXZGZWeHQxS0c4R2RvK2IwNnpPMHBiQ3ZBakNRSmwzSC9BTStQZDRWck50cDJibTIxdmR3M1FLdGtQY2hlekRlNlAzQ0ROaFhBeGhvWnRacWN3RXJhRWdMMlVaalA3bzVvbk81Z3VlNGo4MThvek1IQkhZTjlGVUJCcHFRV3hlK05nOUNXdG9tV0srZ2dmcTg3Q3BXTmM5Snd2amZNbFBKRGhsb3pOTlhCeGhvUWdicUZ6eUNxKzdXZ2h5Tm9lZDJsWHFvemg2cm5qRDlqOVlKdXNNVU5sQ0JmSldBZ1JSeXdiS1NaZ1NhcHNzWjVJS3BtaUxBbVpvakZUdG9mdjVQdi9INE9vd2R6czhPK1dvQkF5SGtwU1hOU0lHdy9nN1Q5V3oxK1ZqSDBlTWp3RkhDNWJEWjUrZi85TE1KK3RWV3dDREQ3S29CQTgxdzdSYzZnaDdkREJIMExRNnphMWpmbGU3bjhqQWNoZTNJbjNENGIzakJDSlFoRzY3aXF3Y01UQ0RQS1dtcWV1ZEExL0VnMi9VUktvSk01ZXU0V2NpdVFkL2FNZnFiT0EvcmI3MmRFQjBZRXhDZ1hkTGNXTjh2WWczMkhNaTE4L3E1dkIrMVBJY21XYS9EOXR0NVRNck1xbDBkVHZzSGZFWGFDREw3YXRFNGtQMDZjMTNIaE9yRlhNZUx6bms3Z0lFRHlFQmMwaXkyN0NySzRFYkhjVGtEcS9zQmRXRDZMYXJzaStucS9odjNwdlB3VFFFR2tDVmR4L29EUG1CNk1SMnlHdmRUK0J5WGM2eC9sdWdCMHkvRTZjZkVkQzZlQUszWnpRRUdVc2cwVmFUQzFBUW5VM0prQ3BuV0JhMkhYMXAwby9tTkZZMmRMVTlLbHpjSkdHaENCbkxRR1Z4M1dtZjdjVjBVamx1aFY2dFc3bHF1MUpibkMrMW1BUU1wNUlKbDFTNVhreHVmajZsa29BNVhQdzM0SzNGK05wQ1ZMQzkwUkxCRHlEY05HQWdoTDYxMktWaFhib2ZwL0Zod0NQY1BZcmlQR0Q5NS94K0FmOFgvRzliL3dBaWMzNFZVcFN2Y0VQTE5Bd2FhNGRyaFppR1o3aGVVU1pBRFptaWwraUxsRXFJRGR0QS9NU282ZXdFQkFBZXZEdThDTURDQlBLZmFGZmwrS0l3S2ZjSDRuZVN0N0tQS1paalZyN1greEZTOWtSTSs5NldLUFZTbjZnWHVDTENZSmlldGFwZkQxZU01ci83RytCVnpLcGpibm5Hb1dvZXJyVVBsTVU4WTRSS3d6c01BRHRVTDNCbmdCZFV1dHBGNkNaZGg5d3ZHTU0xOXVKMWdOQ1FUcm9MOUthMG1XanhlUTdQRDdkRC8wMVlGQXdlUWdUR2NBbE9ndGJCTTVUNmkvekEyUDVsYlpEd0NWdVdxUW10cTlRemFINSthb1psMmQ0QUJaRW1YVjd0cW9abnd2cUZYY0FTWVNWVUdXTUZ5ZnFaYVcyRDN6NytaY21sM0NSaElJV2ZHaTZyd0h0RURabG1SU1ZZTnNNN0JFVmgvM3RVQ3g0RnFXM0NCT3dZTXpJYXNGMVhWK3d0OWVIN0ErRTNrU01HYVBYc2hZeWxZWUFGYzRNNEJBNHNnUjNNcnd6T2ZnNEV4akRQVU1sRlNxQ2NIUzd0N3dNQXN5Tkg4K3d2VHR6MGNvTVBoTTNEa1d1RTZPbGphQ25pd0NtVFBvUGw0OUlocEZjc1Z6SDJwZW5WbTRpY0RTMXNCaXlXUUhiQVhON1FPemYwVnNpdFZNK0tUZ2FXdGdHUGp4WTBnLzBFUDlUZW1jQjB3L2NYYXM0Q2xsU09QZHhOV1JobDd5YkwyS3BIV1lYcER2RXBmMjVPQ3BhMkFLMVlCSFRsTndXVyszKzlVWUdrcjRJWUpaQ0F1WmVwMk5RZXQ2MDRPbHJZQ25ta0I2TGwyZHFocUsrQTNtTUd1MmtkQVZWc0IzN2pwSDRxdGRvUDJQNHFBMHNRajA0YUVBQUFBQUVsRlRrU3VRbUNDIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6c29mdC1saWdodCIvPg0KICAgICAgPHBhdGggZD0iTTk2LDgwdi4yM2E2LDYsMCwwLDEtMy40LDUuNDA2bC0xOS43OCw5LjUyM0E1Ljk3OSw1Ljk3OSwwLDAsMSw2Niw5NEw0LjAyNCwyNS40NTFBMi4zMSwyLjMxLDAsMCwwLDAsMjd2LS4zMzNhNCw0LDAsMCwxLDIuNDYyLTMuNjkzbDcuMjY2LTMuMDI3YTQsNCwwLDAsMSw0LjE0MS42NTVMMTUuNSwyMiw4OS44MTMsODIuOTI4QTMuNzg2LDMuNzg2LDAsMCwwLDk2LDgwWiIgZmlsbD0iIzg1NGNjNyIvPg0KICAgICAgPGltYWdlIHdpZHRoPSI1NCIgaGVpZ2h0PSIxMjAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDU0IC0xMikiIG9wYWNpdHk9IjAuMjUiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBRFlBQUFCNENBWUFBQUM1RndIQkFBQUFDWEJJV1hNQUFBc1NBQUFMRWdIUzNYNzhBQUFHTTBsRVFWUjRYdTJjVzNQaktoQ0VXODUxdmYvL2Y1N2R6VDJ4T1EraVRUTWFoSlNLSEpPaXE2YWt5TEt0enowTTVJRVpRZ2o0aWRyVmJtaFYxN1VicklaaEdHcjNBRUQ0NWxSWUJPYkFWT0gwTGQ4Qk9kUytVNkJLUjAraGNEd2I1S3hqQnFvVVZnRTVrTWJKeWEwQmkyQU8xRTZPTzhBRjVNTWU0N2s5bm1DMkJuVEJDbENNSzNOVU9ENzhzUktiQTg2bG9vVzZrcmlPY1NXdlV3UTdGT0lzZ0JNd1V3RVZUb0Z1SkFqTGUvblFCd0FmTWQ3ajhTREhUUUdYT0thdVhRTzRkZUlhS1MzcEdJRVliMGlBak0wQVMyRFdOVTFGd3QzSHVFTU9wNmxJb05jWWIwaUFDdm5sZ0xVSjJycEdzQnVNUUw5aUtCeGRvMk9FZWpISFRRRnJxV2ovSHBDN1JyZzlSdmR1a1FySkVRbnNKY1p6RFA2OUZqQU13ekFzZ1pzRDg5NnNjRmRJS2JsSGdydkJDTWR4OW9ZYzZpbkdVa0JDbmliNkplN1ZVbEhmbUUyd1NLbEp1TjhZNGU2UVB2ZUlOTVlJOVlnRVZ3SzBoY1k2Q0ZUY0s0RUZwRlJrTVFnb3c5MGdkKzQyWHVkWVl6b3EyQ055U0FWOHdiVFFjS29BRnNCTndFSUlJYzVsd2NUUkJEOXNRQ29vZEk0cHlYbE5YYk5nRnZBSjR3K2pMdTR3d2dFaklDQndjTFFrRlMyVVhVRUFhZHhwV3Q0aHVYWkFjdTAzcG1BUGNuNHZyOThnWCtFTVNJQkEvUDVoR0NianJWWTgxRGtQU3AzakJNNnBZSS9jTlp1U2U0eVFqL0djZ0wvaThSWnBaVU1vWUpwSkFRQnNTcnBnaFhSYzRwcXVUdTZRWEFQU2hQMktORVVRY0k4RVpTZDlnZ0UrRkdFV093YlV4NWtXRlNDSG8zTjBMV0IwalNzV1R1NTdPVmN3ZFFzb1B3TlRQVk1SYkVVUnNYQzZycnhCcXBCQXFtenE2TDBFblNJVTA0OVo4bUdDV1RNQWVZV3NPVWFWVXNDbUEyV1hZTHJVT3NCZlNETVVpaitrTHFSMUd2RCtId1N3SE14cURrNi95UDV6Q296ZmVVQUMxbitCdUpEVzFOVmxHZWM2V3lrbldnTm1BYXhMcWhJY0gvZ0tJeHdkNVJKTng1TXV4NTZRRnR2ZUZEQ0JXd09tVWpndkZWVVdFaGp2MzVuUThjU3A0UmxwRGFyanp3UDdrbFJjSXYwbEIwd2ZJQlN1YWVVa1VBMXFvaTNCckVwd0ZNY2V4OXN0cHVPdjVOUUpqcFh4SEdEdUx5clhkMGpqVHF1b0YxNzY2dWVmMHBFNXY3VXNYQ2xGN1lOclVTbkJVTm0xYzRGNXNyLzBtb0E1bitnN3dlWlVBeXNDVVpjR1poMzV0QzRKektZbWo0c2Nzcm9rc0M5VkIydE5IYXcxZGJEVzFNRmFVd2RyVFIyc05YV3cxdFRCV2xNSGEwMGRyRFYxc05iVXdWcFRCMnROSGF3MWRiRFcxTUZhVXdkclRSMnNOWFd3MXRUQldsTUhhMDBkckRWMXNOYlV3VnBUQjJ0Tmx3U211eWJXN0oxeGRVbGd3UExkVGxWZEdoZ1ZGc1NzdmhQTXB0NmFnRG1mNkZ4ZzlnRzhNUlF3M2Z0WjJwN3NBV1hYemdGVytsVXREQ0hzQmxPTnVVMnYvRXdBNTkwLzVxVlJ3QlRJZG13cHRjVHcwaE5yZDlWK1JpV1FFcFIyYXRGbUN0cTlSWHZ2ektYbHA4SFc3RDJ4WTJnT1N2dDlhTmllSHlYblRsb0Q1dTNmS3NuQ0hNMXJoR0thYVErQ2Z5YllPZUk1M3JmSXRUVmdxcms5WFI2VVBvUkMwU251Y1g3RUNQTW54dC80OXdQU0h1ZzNUTUVtV2dwVzI2Um0zU01VVTQxZnpyOUxVQThZUWY0QytDL0dYNlMyR0FSajQ1TDFxZWkwU1dQc25GQTR1a1FBZmthUWF5d0k3S09qZlQwSTlrZmlIL0t4NXFaaXRlMkZhQzBVblNMQUxwNERLZjNVSlJZSFFoR01vYW40R045akhkUHhlNUlMVm5CTFlleHVWeUNIZXNQNDJjZjRYbDdYSW1HaEZFN0RGZzl2akswcUhoYk9BNkpqUU83VWE3em10WmJSYmtkZWF4bnRxYU45ckVwUUFMQ3F0UXl3enEwRFV0TUR4SE82dGJZWjBGelBLbDAvQm94TWRjZFdGQTBkVjNUcUpiNzJqdVNXVFVFTDVzSE10VzhxclJNemxSd2IwdWtKU2lFcGxuU0ZPaUI5cm5WTHdlYmNxVFhjY2l1aGFra3E2cmtIeGZRakZNKzFrSEJzZlVtTE5GU2dnR1hGUThVUDVSY3FGTjNodURzaUwrOEt0d1RJYzZnS1JNMkIyVGNybEpaMGhib0dpcFB4aXptdUFRSldRQUgxVk5SZmlxbW5EOHlxOTQ0RXFmZnEwc21PbjAyQXFCSll3SFExd2ZTalcwQSs4YkxBQkxuK0xtRUx3aVpBVkMwVjFTMCtMT1FheHhrYi9kQkJkWmVBSDNKdE15QnFBaWFOdG9CcEd0cHJIOGduYXI2dUR0dllGSWlxT1FhTUQ2RFg5TUh0L0dZZExzVm1RRlN4eWJpekF0bkowUUtwdzBCNmVIdmNISWlhN1o0K3M3enlWaUdVZmZpekFsRS90aTE4RlF6STRFNlgzQnR6blIxR3RRaE01VUM2K2c0WTFXcXdWcVR6ejQvUy8xSEw3YllXaGVSYUFBQUFBRWxGVGtTdVFtQ0MiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpzb2Z0LWxpZ2h0Ii8+DQogICAgICA8cGF0aCBkPSJNNjYsOTRjLjEzOC4xMzguMjkyLjI1NC40NDEuMzc3QTMuNzUxLDMuNzUxLDAsMCwxLDY2LDk0Wm0uNDQxLTkyLjM3N0EzLjc1MSwzLjc1MSwwLDAsMCw2NiwyQzY2LjEzOSwxLjg2MSw2Ni4yOTIsMS43NDYsNjYuNDQxLDEuNjIzWk05Mi42LDEwLjM0OSw3Mi44MjMuODM5YTUuOTY4LDUuOTY4LDAsMCwwLTYuMzgyLjc4NEEzLjUxNywzLjUxNywwLDAsMSw3Miw0LjQ4NXY4Ny4wM2EzLjUxNywzLjUxNywwLDAsMS01LjU1OSwyLjg2Miw1Ljk2OCw1Ljk2OCwwLDAsMCw2LjM4Mi43ODRMOTIuNiw4NS42NTFBNiw2LDAsMCwwLDk2LDgwLjI0NFYxNS43NTdBNiw2LDAsMCwwLDkyLjYsMTAuMzQ5WiIgZmlsbD0iI2IxNzlmMSIvPg0KICAgIDwvZz4NCiAgPC9nPg0KPC9zdmc+"},"releaseNotes":"https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.9#16.9.4","supportsDownloadThenUpdate":true,"localizedResources":[{"language":"en-us","title":"Visual Studio Community 2019","description":"Powerful IDE, free for students, open-source contributors, and individuals","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"zh-cn","title":"Visual Studio Community 2019","description":"功能强大的 IDE,供学生、开放源代码参与者和个人免费使用","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"zh-tw","title":"Visual Studio Community 2019","description":"功能強大的 IDE、免費供學生、開放原始碼參與者及個人使用","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"cs-cz","title":"Visual Studio Community 2019","description":"Výkonné integrované vývojové prostředí (IDE), zdarma pro studenty, open source přispěvatele a jednotlivce","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"de-de","title":"Visual Studio Community 2019","description":"Leistungsstarke IDE, kostenlos für Studenten, Open-Source-Mitwirkende und Einzelpersonen","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"es-es","title":"Visual Studio Community 2019","description":"IDE con un gran potencial, gratis para estudiantes, colaboradores de código abierto y personas","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"fr-fr","title":"Visual Studio Community 2019","description":"IDE puissant, gratuit pour les étudiants, les contributeurs open source et les particuliers","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"it-it","title":"Visual Studio Community 2019","description":"Potente IDE gratuito per studenti, collaboratori open-source e singoli utenti","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"ja-jp","title":"Visual Studio Community 2019","description":"学生、オープンソースの共同作成者、個人用の無料で強力な IDE","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"ko-kr","title":"Visual Studio Community 2019","description":"강력한 IDE, 학생, 오픈 소스 제공자 및 개인을 위해 무료로 제공","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"pl-pl","title":"Visual Studio Community 2019","description":"Zaawansowane środowisko IDE — bezpłatne dla uczniów i studentów, współautorów oprogramowania open source oraz indywidualnych osób","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"pt-br","title":"Visual Studio Community 2019","description":"IDE avançado, gratuito para estudantes, colaboradores de software livre e indivíduos","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"ru-ru","title":"Visual Studio Community 2019","description":"Мощная интегрированная среда разработки, бесплатная для студентов, участников проектов с открытым кодом и отдельных пользователей.","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"},{"language":"tr-tr","title":"Visual Studio Community 2019","description":"Güçlü IDE, öğrenciler için ücretsiz, açık kaynak katkıda bulunanları ve bireyler","license":"https://go.microsoft.com/fwlink/?LinkId=2086016"}],"requirements":{"supportedOS":"6.1.1","conditions":{"expression":"not Win10ThresholdBuildNumber","conditions":[{"registryKey":"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion","id":"Win10ThresholdBuildNumber","registryValue":"CurrentBuildNumber","registryData":"[10240.0,14393.0)"}]}}},{"id":"Microsoft.VisualStudio.Product.Enterprise","version":"16.9.31205.134","type":"ChannelProduct","icon":{"mimeType":"image/svg+xml","base64":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgOTYgOTYiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSI0OCIgeTE9Ijk2IiB4Mj0iNDgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxjbGlwUGF0aCBpZD0iYiI+DQogICAgICA8cGF0aCBkPSJNNjguODkxLDk1LjZhNS45NzYsNS45NzYsMCwwLDAsMy45MzMtLjQzOUw5Mi42LDg1LjY1MUE2LDYsMCwwLDAsOTYsODAuMjQ0VjE1Ljc1N2E2LDYsMCwwLDAtMy40LTUuNDA4TDcyLjgyNC44MzlBNS45OCw1Ljk4LDAsMCwwLDY2LDJMMzQuMTE4LDM3LjI2NCwxNS41LDIybC0xLjYzMS0xLjRhNCw0LDAsMCwwLTMuNjEtLjgzNCwzLjk0NywzLjk0NywwLDAsMC0uNTMxLjE3OUwyLjQ2MiwyMi45NzRBNCw0LDAsMCwwLC4wMTEsMjYuMzY2Yy0uMDA3LjEtLjAxMS4yLS4wMTEuM1Y2OS4zMzNjMCwuMSwwLC4yLjAxMS4zYTQsNCwwLDAsMCwyLjQ1MSwzLjM5Mmw3LjI2NiwzLjAyN2EzLjk0NywzLjk0NywwLDAsMCwuNTMxLjE3OSw0LDQsMCwwLDAsMy42MS0uODM0TDE1LjUsNzQsMzQuMTE3LDU4LjczNiw2Niw5NEE1Ljk2NCw1Ljk2NCwwLDAsMCw2OC44OTEsOTUuNlpNNzIsMjcuNjc3LDQ3LjIxMiw0OCw3Miw2OC4zMjNabS02MCw2LjZMMjQuNDExLDQ4LDEyLDYxLjcyN1oiIGZpbGw9Im5vbmUiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPg0KICAgIDwvY2xpcFBhdGg+DQogIDwvZGVmcz4NCiAgPHRpdGxlPkJyYW5kVmlzdWFsU3R1ZGlvV2luMjAxOTwvdGl0bGU+DQogIDxnIHN0eWxlPSJpc29sYXRpb246aXNvbGF0ZSI+DQogICAgPHJlY3Qgd2lkdGg9Ijk2IiBoZWlnaHQ9Ijk2IiBvcGFjaXR5PSIwIiBmaWxsPSJ1cmwoI2EpIi8+DQogICAgPGcgY2xpcC1wYXRoPSJ1cmwoI2IpIj4NCiAgICAgIDxpbWFnZSB3aWR0aD0iNDAiIGhlaWdodD0iODIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMiA3KSIgb3BhY2l0eT0iMC4yNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFDZ0FBQUJTQ0FZQUFBQW1SNWJLQUFBQUNYQklXWE1BQUFzU0FBQUxFZ0hTM1g3OEFBQUVlMGxFUVZSb1ErMmFXM2ZiSUJDRUIxK2FTOU1tLy85M05va2RKMVlmWU1Sb3RTQnM2cllQbW5QMllFblkraklzcUJVYmhtSEEvNnpOVW9kL3JkMVNoMHNVUWdpbGE4T1ZROVVGV0FCeUlXM1hWdURRMkc4aUF4WUtyV29vdEl1Z0Z3RUttTFlhOXBvQ2FlaTVLbVRURUJmQU5zaGcrbGxoZ1FoeGR0cnhlZ2dobENDcmdBdGdteFJiK2F5Z1FJYlIrRXJYZWN4K3JvcUFCazZkSWxRcEZGQ2h2Z0I4cG10ZnlEb0RRQWpCSFdvWDBJR3pZTHNVZS9tOFEzYVQrVWU0VXdwZUEzSU9qcm5xRFhWdGlCVk93ZllwdnBrZ0pCZi9BUm51QThEUlhOTlE2SWxtZ01rOUM4ZWJFK1lld0YxcTd6R0ZKSVRDSFRDRnMzazV1bWcxQVhUV053VzhTL0dRNGxFK0U1S0FBMksrSFJIaHR1azNPZVNmeVBtbzk1ekpHMkxQdlcrSWNJOHBuZ0I4VHkxQjc1QW55Um5aT2Q1REhkV1pQNEcwZVZqS1FaMFVCTHhIaFBtUjRtZHFuekFGQktJN0I4UmM1ZkV4SFN2Y29rYkF3c3pkSWs4SUJYd0c4SUlNK1pqNmNIZy9rT0ZPeUxBNnl5SHRxS1ZaN0UyUVBYTHVjVmdKK0lJSStJQU1TTGNDc3BOMkNhSUcwODVVR21JdkIrbmdFN0tMejhnT3FtTUVmVWVHczI0dHdnSHRrNFF1S2lSQmZ5STZ1RU84MlJGeGtuRDJlb3V6UmxVdFEweEF1c2lsNVhzS3p1SXQ4aVBzaUF3SHpLSE96cm5sZFZCVWc5UUZtbkdYK3ZLWjZ6bW5DN01MVlh3V093dDB5VWtGMWNmZEh0a3RYVUk4NXp6SXBrbmlRVzVNTU9FSnVwTUl5UCtVVWltWUIxWE5RN3RZenRZbHpHRjFvV1ZZMXlrdno1cWNvMG81Q0V4djVnMjd3dkVQOWR5N0dFcGxIZlJrNGJ4VThGUURhWVp0QWZUa2dkVkFiK3BnaTBwdzNib0c4R1l3bnE0Qi9LdGFBWHUxQXZacUJlelZDdGlyRmJCWEsyQ3ZWc0JlcllDOVdnRjd0UUwyYWdYczFRcllxMnNBTDNxMzBxdHJBRDNkRFBwYVFPOHRWUW15OXZwdVVTMkF0ZGRudGRkcE5aQm0yQnFnM3R3QzJwZmgrdjVaNWJtM0NLV3lyNEE5Tnl6VUY2YUFoTk8rVkFtd0dWWUJCK1F2ZUU2ZEViZTN1Tjk3UXQ3MzVZdDFoYVZDdXJhQkQxZUYzQUhBTUF5RDdKVjR3Nm1iME56elpmdUJES0R1QW5PZ0RjcXdiaTZYM3ZKN2NGcDdjSkRZSTk2QVcyRjBXZDJzd1kxcUthb29PYWUxQis4cFhoRjNtYmFwdjI0bUtpVGdPK25sNDh4Rno4RVNJSjE3QS9BTGNYK08zLy9FZkFQN0JOOUpMdytMcWcyeEF0SzlOMFF3RmxDRTFPOEQ4dzN0UXpwUE43M0pvNjJybGlHbWUrL0lOVE5hUEhHQ1h4THdscjVqaDl3dVE5ck9OQUxLVFBZQXRZS0RoUk1Ec3J0MDFoWlZ2S1gyQ0gvSVp6blhXdlZ4UnZ5cmRJZzVBNEg0dzRRL29GNlc4b284M0FySkNWUlZhWklBMlVHYjBIU1g0TzlZTHV5aGs1cVQraFFhSGFzdU04NkNyWkQybk03c2x0SW81aU9mUURNNFR6TUhFeVFQZFJocXVkbGFYSGJFM01VcVpHMi9XSWVheDNiNW9ZdUUwMW9GKzhkb2VJQXVwQXRvWmpRdy9TSG1EaUczSmpSZnRhOCt6MTMzdktLS2FwSHREVXRFOVhoQVpITkJtcXFBRjBEdFp3VUVNcVRYamc1MkFWSUZVQXVsTFg5OE1LSG5xbVhLRndGU1RwMk4xNnFHUXZ0bkM3MnRET2g0MmprSG1GbTZCRVoxQVZvVmdBRzBBMW45VWNCYnFPWC94ZjlVdndFRXhuUTEwSmJTZmdBQUFBQkpSVTVFcmtKZ2dnPT0iIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpzb2Z0LWxpZ2h0Ii8+DQogICAgICA8cGF0aCBkPSJNMTUuNSw3NGwtMS42MzEsMS40YTQsNCwwLDAsMS00LjE0MS42NTVMMi40NjIsNzMuMDI2QTQsNCwwLDAsMSwwLDY5LjMzM1YyNi42NjdhNCw0LDAsMCwxLDIuNDYyLTMuNjkzbDcuMjY2LTMuMDI3YTQsNCwwLDAsMSw0LjE0MS42NTVMMTUuNSwyMkEyLjIxMywyLjIxMywwLDAsMCwxMiwyMy44VjcyLjJBMi4yMTQsMi4yMTQsMCwwLDAsMTUuNSw3NFoiIGZpbGw9IiM1MjIxOGEiLz4NCiAgICAgIDxpbWFnZSB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEwMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEyIC0xMikiIG9wYWNpdHk9IjAuMjUiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBSGdBQUFCbENBWUFBQUNHTENlWEFBQUFDWEJJV1hNQUFBc1NBQUFMRWdIUzNYNzhBQUFMVDBsRVFWUjRYdTJiNjFiYk9oU0V4N1JBNmUyYzl2MGZzcWZjQzhYblJ6VFJlSHZMbGhJNUpJcG5MUzNuWXFETng3Nk5STmYzUFZhMXE0dTVHMWFkdGo3TzNiQnFySzdydXJsN3FQNmRVK1FLT0ZNTzFDekkrbVh2QVhzRm5DR0JxMWY3bWxVdjF4NTRIOWdyNEFrWnNGTUxjclZndmJXRnZUVG9GWEJDRHR3THVlcXlvQlhrVzFpOWM5MTg4NFZCcjRBZEJiaTZDUE9EV1JZME1BVDdCdUN2dWVwYUhQUUsyTWlCUzVBZnpickVFTFlGL0Rlc1YxbC9aUjBFOUFwWVpPQnExRjVpODFsZHlicVV4WHVCRFN4Q2ZNRUc3SXRaQkw4NDZCVndrQU9Ya1VtSTEyRjlraXRoTThvN3hPaDlCZkJuWXRuSVhnVDBDaGl6Y0JYc1p3QTNjaVZrUmpFQk0yci9BSGdPNnlrc1B2K0RHTkdMZ1Q1N3dBbTRyTEZYMkVDOEFmQWxySy9oK2ptc2E4UjZiQUVyMkVkWkNudFIwRjNtZlUwcUF5NmpsV0MvQWZnZUhuOUZqR1RXYUdBRGcrbjVHUkhxZzF6NWVCZlEyMWs2Qi9MWlJ2QU1YRTNKQ3ZhZnNBajVDemFBV1ljN1RBTytEK3NCMDZBSjIrdTgyYVdqNjdwWnlHY0plS1piWmxvbVhJTDlOeXhDL29ZWXdWZllmSmJhWkRGRnB3QmIyTHlQYWQyQ3ZrQUVEV1E2WW1jSE9PRlFlV21aY0FuMkI0YVFHY0ZzdEFpWVl4SUJLMlFMOTg2OFJ0Qlg4QnN5L2JjemJRTlNvNjNPQ3JBRDEydW9QTGcvc1FIOEE4TVUvVGw4RFp1c0MwVEF0dEY2RFBjL1lQT0xjUitlRS9RTkl2aHJSTmpXTmV2Qzl3Wk04K1ZGOFZrQkRySnBtUVlHNFg3Qkp2MHlMUlB1VDhUby9ZWllmNi9EOTlBYWJHZmhtN0NlRUNHekM3K1Q5emxqYy9UaTkxWFBtMkt6dGUyeXU2N3JMT1N6QVN4MTE0TjdqVGdLS1Z4R0xlSCtpMDMwY2t4aTlCSUVwVzdXRlRhUnpNYU42OFk4bndLcjZwM0ZlODR6Z2hNZE05UHlIRnl0dlpxYTJWeHBlcVlZV1c4WWxnQmQxL0tZVUQyd0dxbk1ETW5SeVVaeDg0QXp4aUdGcXgyendyV2pFU09PWU5TTEJvWlErUE5lRVNFcVRBOG9RZXBtaFoyUHVmVC94Ni9mcW1uQUdSWmthaHl5SGJNMzk3SnoxdVpIMVlmWDMrUWVyYVVLUkpzeTdiNmZNTnpZNEMrR2ZpLytETmIvZ1pvRlBCRzVUSStmRUIycUhMaXN1UXAzcWdIcTVKcHFqbDR4M0pYU3FMYkxRdVVWR1A5eWJhVnBwUmxsV0pEYU1YL0gyTXpRYmxsckx1dm1GRnpJYTUzejNJdDA3WWJ0ZFZ0Zmc3eWZOWHdoam9QdEFaNXhxVmh6VTJuWnE3bTJvVXJCVFVXcVB0WkdTVk15WGF0bkRNME5hMW1PTmgvbTFCVGdESmRxcXVhbUdpb2J1YmFXUXE3MmcvY2FKdDFHZkVLMEt1L05VcDlhM2F4WCtGMzBWazEyMFRNdWxUWlVUTXVha3FjaVY3dmxWT1FDNDJoVnVCWXNiY3M3QUxjQWZvZjFuenkrRGU4VE5DR3prN1p3M2FodUJuQ1FUY3ZhVk5tYW00cGM2MUpadU5veHArQjZxZmdQb21XcGNQK1Q5UXNSOEIxOHdFVXB1d25BR1M2VldwQjJBMEVCSzF3OXJiRUxYSjFmMVk4bVhFYXNCL2NXTVUzcnBvUHVGelB0OTJZTmRQS0FNMTJxejVoMnFiUmpWcmc2dHRpMHJCK21UY25jVGRJdFE5WlpDL2VYUEU3QnpUa0lBQUNqRFllVEJselJwZEp4U0NPWFRaWFhJZXRqL2JDOWV2dUFjYjBsV0Y1dFd0WTk0UlRZMUNpMTFja0NydVJTc2VZcVhEMWo1WFhNMUZ3enhaUk11TnBJS2R5cHRHelRjV3BHN29GeDlBSW5DbmhCbDJxdVk2WnltaW1ia3IybzFjaWRndXRGcTliK2RrNTBaTHBVSGx4ZEtaZHFGN2hlTTBXNFRNbGFieFV1UjZGN0RHZGVDemNKRmtqREJVNE1jRUhONVVHNWZXZGRGVDlFVzI5Zk1UWXV0Tjdtd3MwWmd3WWc1dzdjQVNjRU9NT0N0S2NnNStCNnMrNlVpZUhWVzhMbHFVaTZVRHJmS2xqVzNKU0o4WUp4dmRXb3pZS3FPZ25BaFJZazRYcWpVRTdrMHI3dE1JeWNxWG83MVV3cFlOdE1FUzdISUoxdHQ1RmJDbFYxOUlBTExFZ0xsNUZyeDZFNWw0by95NE9iWTE3WVpvcVBiekhkS2F2SHZJM2FmZUFDSndBNHlLYmxsRXVsYVZrUHlWa0wwbk9wdE9acXZlMnhtM21ScXJjNjR5WmRxWDNCVWtjTnVOQ0M5RXdNQld6aFRuWExXbTlMelFzRlc5Sk1WWWNMSERIZ0NoYmtEd3pocGc3S2VkRnJtNm5jZXF0WHJiZjNpSTJZZGFhcTFWdFBSd200WUJ5YXN5Qzk0emFFUzM5Wk5kZE1sWmdYWHIwbFhLOVRyZzRYT0VMQWxTeklPU1BEcHVWZUZqLzhHdWFGNXltUG5La2x3RkpIQlhoQkM5SnVIbmhwZVNuell0S1pXaEl1Y0VTQU15M0lmUS9LZVhDMVV6NkVlYkZZdmZWMEZJQXpYS3A5RDhwNURkVSt6WlFDUHJoNVVhSjNCMXpvVXVYQTlRN0syVmxYNFo2Y2VWR2lkd1ZjNEZMVk9pZ0hqRHZsa3pNdlN2U3VnSU5zV3ZhTWpGMFB5bG13UUFSTHVDZG5YcFRvM1FBWHVsUWxCK1Z5SXJlazN1cjFLTXlMRXIwTDRBb3VsZTJZUFgvWnpybk5tQmNsT2pqZ2lpNlZkMURPUm02cVV6NXA4NkpFQndWY3lhWEtPU2lua2F2T1ZCUG1SWWtPQm5oaGwwb2pGL0RIb0diTWl4SWRCSENtUzdYdlFUbU4zRjJhS1FWODFPWkZpUllIWEZCemM4OVNwVndxSUVhdVYyK2JNUzlLdENqZ0RBdHkxNE55SGx6QWI2YWFNeTlLdEJqZ1Fnc3k5NkJjNnFpTjdaU2JOaTlLdEFqZ0FndHk3cURjbkVzRnBKdXBaczJMRWkwQ09NaW01WlJMcFduNUo2WXRTSGJNMXNBZ0NJNHdUWnNYSmFvT3VOQ0M5RXdNMnpFcjNGUmFwdWx3RnVaRmlhb0NybUJCL2tDNjdtcERwYnRCbm5seGgyRjluWUo3Y3VaRmlhb0JMaGlINWl4SVBRVnA5M1FCZnd5eXpaUUg5aGNhTVM5S1ZBVndKUXR5eWwvV2hrcmgydmsyQlZjajkrVE5peEx0RGZnQUZxUzZVem9HZWVaRkNtNnFtVHBKODZKRWV3SE90Q0JMRDhyWmhnb1l3cldkY3FxWllrcitqY2JNaXhMdEREakRwY285S09kMXk1cVdDZGZ1NGRxZElLMjFGdTVEV0UyWUZ5WGFDWENoU3pYWFVFMmxaVWFYWnp1V05sUE5tQmNsS2daYzRGS1ZISlJMd1UxMXlydnNCRFZqWHBTb0NMQ0JPMmRrekIyVXN6dERIbHoxbFBmWkNXckt2Q2hSTnVBRTNJOUl1MVE1QitYVXhPZ3dISU1JdDJRbjZCYXgzdHBtaXAxeXMvWFdVeGJnek1oVkl5TmxRZEpmOWh3cTFrUENmUXhyMTUyZ1p3enJiVFBtUllrbUFUdjFWc2NoYTBHbVpsMEM5allQMkMwVExqdGxiNXZQZHNxMjNqYTNFMVJEU2NBSnVIU292TWlkTXpJMExWOWpHTGs2NDNyTmxBWHJOVlBON1FUVmtBczRNUVpOcGVXY1U1QWF1WVRMdEttZGNtNjliWDRucUlhbVVuVEt4UERPVXFVT3lsbTQybENwZ2FGd1M3YjVtdDRKcXFFUjRFVDBsc3k2VE1rSzE4NjVUTXUyVTdiMVZsUHoyZTBFMVZBcWdsUFI2NDFEdXYzSGxkclBCWWI3dU53dzhKb3BCWnd5TDVyZUNhcWhBZUNacnBtMTl4UEdQck11UmkzaFhtTG9LK3RXbjdlSGE2TTNaVjQwdnhOVVExNEVkL0hoeU5Ed1VqVHJNSSsxV25lS2NCbGxkc1BBc3gzbm1xbXoyQW1xb2JrVWJWMHJiYkkrSVVZei83clBPNEh4SnN2YkRkS28xU2oybXFtejJnbXFvWnd1Mmh1VEZQYWx2TVlPR1lqcDJJNUNVMm5aMXR2VnZOaFRPVlpscWk3cmN5Qis0Qng5T214Z3NHT2VHNFdZcWxmem9xSnlBUGR5NVNKSTNkTGp5UE1oM0VQSU5uclpOQkV1UWEvbXhRSktBZTR4Qmtxb0JLb0doYzY0YktRK21PZFBpTkZMd0wvbCtXcGVMQ0FQY0k5b1JpaGNqZFFuYktEcTN3ajE0UjYrcDkzek15SmcxbDlkM3ViOGFsNVUwQUJ3My9kOW1JVTl1Qit3K2VEWlRPa2VManZrUjhTL0g5S05CTjBoSW1SR3JEZmZydVpGSlUzVllGdHZYeEM3YWUyVStkNFR4b2ZtR05WOG41QUptbnUrT3Q4UzdtcGVWTkJVRFFZMkh6RHI2RlRIL0FUL1R6c1p3YXpYSEhrZTViR1hrbGZ6b3BKR2dDVk5BeEhrNEJaRWNQWm9qVWF2cG0vZXgzdWY1YmxOeVdzelZWRnVCQWZJZkxxdGZ4alhaUVdzWm9jQ1pwclc3cHRMall1MTNpNmd1VGxZUDJCTm16b0h2MkRZZUtrSkFveEhMRjd0Ykx2VzJ3WFV6WDJPaWYxaGExOWVtTFVOZi9pL0ZHL3d3YTRwdWJKbUFRTWp5THhxcEY2WXE5N2JZd2o1VFI3cmUydlVMcUFzd0ZRQ3RGMzZQakNzMzNadDMxL2hMcU1pd0pTQUJzWlE5VDFWNzExWHNNdHFKOENVQWIxOTJYa05HRFpzSzlnRGFTL0FWZ25nVzYxUUQ2K3FnRmNkbnk2d3FtbjlEL1FHdkxnc0FUTDVBQUFBQUVsRlRrU3VRbUNDIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6c29mdC1saWdodCIvPg0KICAgICAgPHBhdGggZD0iTTk2LDE1Ljc3VjE2YTMuNzg2LDMuNzg2LDAsMCwwLTYuMTg3LTIuOTI4TDE1LjUsNzRsLTEuNjMxLDEuNGE0LDQsMCwwLDEtNC4xNDEuNjU1TDIuNDYyLDczLjAyNkE0LDQsMCwwLDEsMCw2OS4zMzNWNjlhMi4zMSwyLjMxLDAsMCwwLDQuMDI0LDEuNTQ5TDY2LDJBNS45NzksNS45NzksMCwwLDEsNzIuODIzLjg0MUw5Mi42LDEwLjM2NEE2LDYsMCwwLDEsOTYsMTUuNzdaIiBmaWxsPSIjNmMzM2FmIi8+DQogICAgICA8aW1hZ2Ugd2lkdGg9IjEyMCIgaGVpZ2h0PSIxMDEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMiA3KSIgb3BhY2l0eT0iMC4yNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFIZ0FBQUJsQ0FZQUFBQ0dMQ2VYQUFBQUNYQklXWE1BQUFzU0FBQUxFZ0hTM1g3OEFBQUw2MGxFUVZSNFh1MmQ2M2FiU0JDRWF5UWx6bVYzMy84OWQ1UFljV3oyQjVRb1N0MHpZRXV5THZRNWZXYTREUW9mMWRNMFBxRjBYWWZWYnRjMnJSMVd1MjdidFhaWVlxV1VVdHZlcmVIaTdGYmVjODBUb0Jua3lZbFcyT2V4TnluWXdKYWtkZXUwNVJBcjZOUGFJc0FDVmx0MzNRNU13YXIzTzY2Z1QycXpRblFDZG9NUjZzWmEzVmVodmc3ZVNic0h2a0krdmxVVkhJUmloN2tCc0pYK0JsUEl3QlR1eStDdjVoMkFycFN5UWo2eXBZQk50UXBYb2U2R1BsdXUxM0JOaUM4QS9rakxmcEY5MXBCOVpBc0JCM0JWbllUNWFlaC9rajYzRVRMVlM2RFA1Z29ha05CZFNpa3I1UGRiTFVSSHFsV2duOFVmWk4wT1U4Q3Y2Q0grSHZ4cGNDNXYwTU11bUlKZVEvWVI3QUN3cVZmaEt0aUh3YjhBK0RxMEQ4TzJUeGhWM0tHSDlvd2U2dVBndjRaVzFhNlFDUm9yNVBkWnBtQ2ZkeGwrUDZPSCtRWEFOL092R0NGVHhRelB6eGpCL2h6Mis0RnBTS2VTYWV1OGZBU2JBRTdtWGlaUFZDL2gvaVgrSFZQSW56QlY4RytNY0gvSVBxcDJUYzZlc2M3TFI3Rkl3UkZrenIwTXoxL1JRLzBMd0Q4QS9oNzYzOURmQUorSDQ0QWVGTVB6RDB5VlRzQ2FnVWNoRzFnaHY4bGFJVnJuWUZleFF2NGJQZWp2R0FGdU1RTDZqUjR3YndBcWVDZnV6OU9FckxZbVh3dXRWYW9rNUlJcFpGVXpRelpoZjhjSXNLQlhIcE1zd3RjUTdTcld4RXZEdGhaSTF1UnJwdTBCSjRXTi9XWk1MM2owNktUQXZ3N0xXNHp6OEJQRzBNendySUFqRlN0Z0Q5ZHI4alhEV2dwVzZ4SlgwMGVxQjR6WjlDdEc1U3Jjblhta1pJZE1XNU92R1JZQmppQnF1VkZMalY2VmVzRUlYUit2Z0JHc1ExVzREbG9CYjlEUDVhN21kVjZ1MkI1dzEzV2R2VndBRHVFK1k2eEdhZEdDWVprVktpMC9VdFZBRzJ5a1lwK1hQV1N2UlpHS1pTSGExVXUxN3RCRC9HVCsyVnkzT2JRTkR1SHRyQjhkazgzTHdGb1VTUzBMMGN4K3FaSU54bnF5WG1nSE5HZE8vWXdwTkFlZEhWY0R2QlpGRXBzQWxqRE5DMFBJZnhCbjFacGRSOHJNQVBsTlVqdDJqcHJYb2toaXRSRE45blhvKy94TXE0R09ZTENNdVV1T3lSUmRBMDNJYW12eWhRQ3dKVnNLK0EvaVJ5TmdDa3BWNlJDSzlMZkJjVFhZbVhNOFBjZGFGQmtzVlBBQWViOG9iUmNzQTdHS004RHFEaWdDWEp2ZmErZFlpeUxJUTdSRDVsdzgyV1ZvaTdRWllMOEJIUEpPMW1kcXprSzFoMndkbTNhM3lWY0tHRWlUcnJsaDJpKzRBdVQrYkFtcHBtUlZzZmNkOEFiOTQxeWs1cnVDWEFVTWpDRnRVRE1mb1lCK1RnYnE2blJWTzJodEZiS3FtNkJyY0JVeTI0SnA1WXUvOTY2U3J5WmdtcW1aaWRlK2lvUVlxc1BONE90K08rblhsTndDcmVlbDNWM3lOUnN3Y0FDNXd3ZzRnNmNBRmFJRG9QTVZveXA1RHVnTWJuUmozVlh5dFFnd01JSDhpdjRDcW9wcEdlZ01zSUp3eU5FeEdkeldzektkZHZQSjEyTEFRSnA4UlhPeUxpdk1HdUFJc283aDhKWUEzdURPM2tpOUNUQVFQa1lCNHhzbnRSYmdDSGpCOUZrM0c2T2w0QWgyTWIvcE4xSnZCZ3lra0dtdXZBaEtCamM2MXRmVlFFZkFhK2NDYnZTTjFMc0FBMDNJUUE2NmRjSFpNbHhIa04wejBITUEzK1FicVhjREJsTElCZU5ibmhaY1ZhUURMaGlWeUg0RXVnYVhnR3V3Yi9LTjFGRUFBeUhrMmlOVXBxWk5jZ3lOa05uNnNSRnNMNUJrcWlaa3RhdFB2bzRHR0dpR2F3Y1JnZWZCM3VyeEJGTzdhVnl4TGZjSVVuQWpSWkdqQWdZbWtCVndCak9DMU5wSDk2T1NnU2trVjdKWHZsVFZXVFFoWk9DS2s2K2pBeGJ6a3VaY2FBb3BnZzZNaVpkQzluRWlCV2VoMmtPMm53KzQwdVRySklBSEZSY2NsalRWaXJRWjZHaVpyb25YQm5ueUZhblpGUjBCM3VBR2lpSW5BUXpNcm5acFB3S2RlWFFEUkVwMndEVzQyYnlzZm5WRmtaTUJCbVpYdTk0S1ZtK1M3ZEJtKzZpQ28zQmR5NjU5VE9DS2lpSW5CUXcwTTJ1MXVZQ2pDKzdyZFR3Zk13TzlCUERWRkVWT0RoaG9RczRVcDBDeUM2NjJrMjBPSlJvdmd6dEh6VmRURkRrTFlLQUpHY2lCMU5Ua0R2UlFmRjAwOXRiNkhxNXJvQWxaN1NLVHI3TUJCbzVXMG94QXE3bUNIYmlQNHdwdU9jZldjMXhzVWVTc2dJR2psRFFqRmZ2eHNHUG1qTzBoZW1lZWhXdUZERnhZOG5WMndFQXpYRHVBQ0V3R2JUL28wTy9zT0NBK0psSnlGcW85WkVmbnZwams2ME1BQTR0TG1xcmVKZEJoMnhpdUVSeWZxVGw3blBLb2NwRkZrUThETExha3BMbXhmaFF1YXdvSFlpVkhnR3R3czNsWi9TS0tJaDhLZUZCeFFidWtHWUd1dVVPRnJkYytnckZyNFhyT1k1U08vYUZGa1E4RkRNd3VhVG9JRFkydEM2em1ZRGZvcjBGMjgyU2dsd0QrMEtMSWh3TUdacGMwZ2ZtQS9VSTcxT3pHaVFDemplRE9VZk9IRmtVdUFqQXdPN091S1N4VGtoci92ZHpPUGkrMDN4QU9Xcy9wYzNUMld3aFo3V3pKMThVQUJwcVFnUngwQmpkU2NBUzVCTXZST1Z6QkxlZVkrcHZPV2hTNUtNREEwYXBkZmxIZE9POUM5aWxvS3psU3NhcFpWVjI3NmM1V0ZMazR3TUJScWwxY3oyUDBXRjIzbFQ3WDF5SUF0N3RTczFEdElWdkhvWjAwK2JwSXdFQXpYRHZjREFhQ1phN1Rkb040SFpjZGNLYm03SEhLSTh2WmlpSVhDeGhZWE8yS1FMVDJjYWphWnVzaXdEVzQyYnlzZnJLaXlFVURGbHRTN1hLNDJUeEljTEJqdU53TjYzUmVacXRqMThMMW5NY28vUzFITDRwY1BPQkJ4YnpndFdvWDJ3eDBheHVOVUl1MEROZE16blFNRDlVT2VnbmdveGRGTGg0d01MdmFwZjBJWnVRUllHQUtXZGZWeHQxS0c4R2RvK2IwNnpPMHBiQ3ZBakNRSmwzSC9BTStQZDRWck50cDJibTIxdmR3M1FLdGtQY2hlekRlNlAzQ0ROaFhBeGhvWnRacWN3RXJhRWdMMlVaalA3bzVvbk81Z3VlNGo4MThvek1IQkhZTjlGVUJCcHFRV3hlK05nOUNXdG9tV0srZ2dmcTg3Q3BXTmM5Snd2amZNbFBKRGhsb3pOTlhCeGhvUWdicUZ6eUNxKzdXZ2h5Tm9lZDJsWHFvemg2cm5qRDlqOVlKdXNNVU5sQ0JmSldBZ1JSeXdiS1NaZ1NhcHNzWjVJS3BtaUxBbVpvakZUdG9mdjVQdi9INE9vd2R6czhPK1dvQkF5SGtwU1hOU0lHdy9nN1Q5V3oxK1ZqSDBlTWp3RkhDNWJEWjUrZi85TE1KK3RWV3dDREQ3S29CQTgxdzdSYzZnaDdkREJIMExRNnphMWpmbGU3bjhqQWNoZTNJbjNENGIzakJDSlFoRzY3aXF3Y01UQ0RQS1dtcWV1ZEExL0VnMi9VUktvSk01ZXU0V2NpdVFkL2FNZnFiT0EvcmI3MmRFQjBZRXhDZ1hkTGNXTjh2WWczMkhNaTE4L3E1dkIrMVBJY21XYS9EOXR0NVRNck1xbDBkVHZzSGZFWGFDREw3YXRFNGtQMDZjMTNIaE9yRlhNZUx6bms3Z0lFRHlFQmMwaXkyN0NySzRFYkhjVGtEcS9zQmRXRDZMYXJzaStucS9odjNwdlB3VFFFR2tDVmR4L29EUG1CNk1SMnlHdmRUK0J5WGM2eC9sdWdCMHkvRTZjZkVkQzZlQUszWnpRRUdVc2cwVmFUQzFBUW5VM0prQ3BuV0JhMkhYMXAwby9tTkZZMmRMVTlLbHpjSkdHaENCbkxRR1Z4M1dtZjdjVjBVamx1aFY2dFc3bHF1MUpibkMrMW1BUU1wNUlKbDFTNVhreHVmajZsa29BNVhQdzM0SzNGK05wQ1ZMQzkwUkxCRHlEY05HQWdoTDYxMktWaFhib2ZwL0Zod0NQY1BZcmlQR0Q5NS94K0FmOFgvRzliL3dBaWMzNFZVcFN2Y0VQTE5Bd2FhNGRyaFppR1o3aGVVU1pBRFptaWwraUxsRXFJRGR0QS9NU282ZXdFQkFBZXZEdThDTURDQlBLZmFGZmwrS0l3S2ZjSDRuZVN0N0tQS1paalZyN1greEZTOWtSTSs5NldLUFZTbjZnWHVDTENZSmlldGFwZkQxZU01ci83RytCVnpLcGpibm5Hb1dvZXJyVVBsTVU4WTRSS3d6c01BRHRVTDNCbmdCZFV1dHBGNkNaZGg5d3ZHTU0xOXVKMWdOQ1FUcm9MOUthMG1XanhlUTdQRDdkRC8wMVlGQXdlUWdUR2NBbE9ndGJCTTVUNmkvekEyUDVsYlpEd0NWdVdxUW10cTlRemFINSthb1psMmQ0QUJaRW1YVjd0cW9abnd2cUZYY0FTWVNWVUdXTUZ5ZnFaYVcyRDN6NytaY21sM0NSaElJV2ZHaTZyd0h0RURabG1SU1ZZTnNNN0JFVmgvM3RVQ3g0RnFXM0NCT3dZTXpJYXNGMVhWK3d0OWVIN0ErRTNrU01HYVBYc2hZeWxZWUFGYzRNNEJBNHNnUjNNcnd6T2ZnNEV4akRQVU1sRlNxQ2NIUzd0N3dNQXN5Tkg4K3d2VHR6MGNvTVBoTTNEa1d1RTZPbGphQ25pd0NtVFBvUGw0OUlocEZjc1Z6SDJwZW5WbTRpY0RTMXNCaXlXUUhiQVhON1FPemYwVnNpdFZNK0tUZ2FXdGdHUGp4WTBnLzBFUDlUZW1jQjB3L2NYYXM0Q2xsU09QZHhOV1JobDd5YkwyS3BIV1lYcER2RXBmMjVPQ3BhMkFLMVlCSFRsTndXVyszKzlVWUdrcjRJWUpaQ0F1WmVwMk5RZXQ2MDRPbHJZQ25ta0I2TGwyZHFocUsrQTNtTUd1MmtkQVZWc0IzN2pwSDRxdGRvUDJQNHFBMHNRajA0YUVBQUFBQUVsRlRrU3VRbUNDIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6c29mdC1saWdodCIvPg0KICAgICAgPHBhdGggZD0iTTk2LDgwdi4yM2E2LDYsMCwwLDEtMy40LDUuNDA2bC0xOS43OCw5LjUyM0E1Ljk3OSw1Ljk3OSwwLDAsMSw2Niw5NEw0LjAyNCwyNS40NTFBMi4zMSwyLjMxLDAsMCwwLDAsMjd2LS4zMzNhNCw0LDAsMCwxLDIuNDYyLTMuNjkzbDcuMjY2LTMuMDI3YTQsNCwwLDAsMSw0LjE0MS42NTVMMTUuNSwyMiw4OS44MTMsODIuOTI4QTMuNzg2LDMuNzg2LDAsMCwwLDk2LDgwWiIgZmlsbD0iIzg1NGNjNyIvPg0KICAgICAgPGltYWdlIHdpZHRoPSI1NCIgaGVpZ2h0PSIxMjAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDU0IC0xMikiIG9wYWNpdHk9IjAuMjUiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBRFlBQUFCNENBWUFBQUM1RndIQkFBQUFDWEJJV1hNQUFBc1NBQUFMRWdIUzNYNzhBQUFHTTBsRVFWUjRYdTJjVzNQaktoQ0VXODUxdmYvL2Y1N2R6VDJ4T1EraVRUTWFoSlNLSEpPaXE2YWt5TEt0enowTTVJRVpRZ2o0aWRyVmJtaFYxN1VicklaaEdHcjNBRUQ0NWxSWUJPYkFWT0gwTGQ4Qk9kUytVNkJLUjAraGNEd2I1S3hqQnFvVVZnRTVrTWJKeWEwQmkyQU8xRTZPTzhBRjVNTWU0N2s5bm1DMkJuVEJDbENNSzNOVU9ENzhzUktiQTg2bG9vVzZrcmlPY1NXdlV3UTdGT0lzZ0JNd1V3RVZUb0Z1SkFqTGUvblFCd0FmTWQ3ajhTREhUUUdYT0thdVhRTzRkZUlhS1MzcEdJRVliMGlBak0wQVMyRFdOVTFGd3QzSHVFTU9wNmxJb05jWWIwaUFDdm5sZ0xVSjJycEdzQnVNUUw5aUtCeGRvMk9FZWpISFRRRnJxV2ovSHBDN1JyZzlSdmR1a1FySkVRbnNKY1p6RFA2OUZqQU13ekFzZ1pzRDg5NnNjRmRJS2JsSGdydkJDTWR4OW9ZYzZpbkdVa0JDbmliNkplN1ZVbEhmbUUyd1NLbEp1TjhZNGU2UVB2ZUlOTVlJOVlnRVZ3SzBoY1k2Q0ZUY0s0RUZwRlJrTVFnb3c5MGdkKzQyWHVkWVl6b3EyQ055U0FWOHdiVFFjS29BRnNCTndFSUlJYzVsd2NUUkJEOXNRQ29vZEk0cHlYbE5YYk5nRnZBSjR3K2pMdTR3d2dFaklDQndjTFFrRlMyVVhVRUFhZHhwV3Q0aHVYWkFjdTAzcG1BUGNuNHZyOThnWCtFTVNJQkEvUDVoR0NianJWWTgxRGtQU3AzakJNNnBZSS9jTlp1U2U0eVFqL0djZ0wvaThSWnBaVU1vWUpwSkFRQnNTcnBnaFhSYzRwcXVUdTZRWEFQU2hQMktORVVRY0k4RVpTZDlnZ0UrRkdFV093YlV4NWtXRlNDSG8zTjBMV0IwalNzV1R1NTdPVmN3ZFFzb1B3TlRQVk1SYkVVUnNYQzZycnhCcXBCQXFtenE2TDBFblNJVTA0OVo4bUdDV1RNQWVZV3NPVWFWVXNDbUEyV1hZTHJVT3NCZlNETVVpaitrTHFSMUd2RCtId1N3SE14cURrNi95UDV6Q296ZmVVQUMxbitCdUpEVzFOVmxHZWM2V3lrbldnTm1BYXhMcWhJY0gvZ0tJeHdkNVJKTng1TXV4NTZRRnR2ZUZEQ0JXd09tVWpndkZWVVdFaGp2MzVuUThjU3A0UmxwRGFyanp3UDdrbFJjSXYwbEIwd2ZJQlN1YWVVa1VBMXFvaTNCckVwd0ZNY2V4OXN0cHVPdjVOUUpqcFh4SEdEdUx5clhkMGpqVHF1b0YxNzY2dWVmMHBFNXY3VXNYQ2xGN1lOclVTbkJVTm0xYzRGNXNyLzBtb0E1bitnN3dlWlVBeXNDVVpjR1poMzV0QzRKektZbWo0c2Nzcm9rc0M5VkIydE5IYXcxZGJEVzFNRmFVd2RyVFIyc05YV3cxdFRCV2xNSGEwMGRyRFYxc05iVXdWcFRCMnROSGF3MWRiRFcxTUZhVXdkclRSMnNOWFd3MXRUQldsTUhhMDBkckRWMXNOYlV3VnBUQjJ0Tmx3U211eWJXN0oxeGRVbGd3UExkVGxWZEdoZ1ZGc1NzdmhQTXB0NmFnRG1mNkZ4ZzlnRzhNUlF3M2Z0WjJwN3NBV1hYemdGVytsVXREQ0hzQmxPTnVVMnYvRXdBNTkwLzVxVlJ3QlRJZG13cHRjVHcwaE5yZDlWK1JpV1FFcFIyYXRGbUN0cTlSWHZ2ektYbHA4SFc3RDJ4WTJnT1N2dDlhTmllSHlYblRsb0Q1dTNmS3NuQ0hNMXJoR0thYVErQ2Z5YllPZUk1M3JmSXRUVmdxcms5WFI2VVBvUkMwU251Y1g3RUNQTW54dC80OXdQU0h1ZzNUTUVtV2dwVzI2Um0zU01VVTQxZnpyOUxVQThZUWY0QytDL0dYNlMyR0FSajQ1TDFxZWkwU1dQc25GQTR1a1FBZmthUWF5d0k3S09qZlQwSTlrZmlIL0t4NXFaaXRlMkZhQzBVblNMQUxwNERLZjNVSlJZSFFoR01vYW40R045akhkUHhlNUlMVm5CTFlleHVWeUNIZXNQNDJjZjRYbDdYSW1HaEZFN0RGZzl2akswcUhoYk9BNkpqUU83VWE3em10WmJSYmtkZWF4bnRxYU45ckVwUUFMQ3F0UXl3enEwRFV0TUR4SE82dGJZWjBGelBLbDAvQm94TWRjZFdGQTBkVjNUcUpiNzJqdVNXVFVFTDVzSE10VzhxclJNemxSd2IwdWtKU2lFcGxuU0ZPaUI5cm5WTHdlYmNxVFhjY2l1aGFra3E2cmtIeGZRakZNKzFrSEJzZlVtTE5GU2dnR1hGUThVUDVSY3FGTjNodURzaUwrOEt0d1RJYzZnS1JNMkIyVGNybEpaMGhib0dpcFB4aXptdUFRSldRQUgxVk5SZmlxbW5EOHlxOTQ0RXFmZnEwc21PbjAyQXFCSll3SFExd2ZTalcwQSs4YkxBQkxuK0xtRUx3aVpBVkMwVjFTMCtMT1FheHhrYi9kQkJkWmVBSDNKdE15QnFBaWFOdG9CcEd0cHJIOGduYXI2dUR0dllGSWlxT1FhTUQ2RFg5TUh0L0dZZExzVm1RRlN4eWJpekF0bkowUUtwdzBCNmVIdmNISWlhN1o0K3M3enlWaUdVZmZpekFsRS90aTE4RlF6STRFNlgzQnR6blIxR3RRaE01VUM2K2c0WTFXcXdWcVR6ejQvUy8xSEw3YllXaGVSYUFBQUFBRWxGVGtTdVFtQ0MiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpzb2Z0LWxpZ2h0Ii8+DQogICAgICA8cGF0aCBkPSJNNjYsOTRjLjEzOC4xMzguMjkyLjI1NC40NDEuMzc3QTMuNzUxLDMuNzUxLDAsMCwxLDY2LDk0Wm0uNDQxLTkyLjM3N0EzLjc1MSwzLjc1MSwwLDAsMCw2NiwyQzY2LjEzOSwxLjg2MSw2Ni4yOTIsMS43NDYsNjYuNDQxLDEuNjIzWk05Mi42LDEwLjM0OSw3Mi44MjMuODM5YTUuOTY4LDUuOTY4LDAsMCwwLTYuMzgyLjc4NEEzLjUxNywzLjUxNywwLDAsMSw3Miw0LjQ4NXY4Ny4wM2EzLjUxNywzLjUxNywwLDAsMS01LjU1OSwyLjg2Miw1Ljk2OCw1Ljk2OCwwLDAsMCw2LjM4Mi43ODRMOTIuNiw4NS42NTFBNiw2LDAsMCwwLDk2LDgwLjI0NFYxNS43NTdBNiw2LDAsMCwwLDkyLjYsMTAuMzQ5WiIgZmlsbD0iI2IxNzlmMSIvPg0KICAgIDwvZz4NCiAgPC9nPg0KPC9zdmc+"},"releaseNotes":"https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.9#16.9.4","supportsDownloadThenUpdate":true,"localizedResources":[{"language":"en-us","title":"Visual Studio Enterprise 2019","description":"Scalable, end-to-end solution for teams of any size","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-cn","title":"Visual Studio Enterprise 2019","description":"面向任何规模团队提供的可缩放、端到端解决方案","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-tw","title":"Visual Studio Enterprise 2019","description":"可擴展,且適用於任何規模小組的全方位解決方案","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"cs-cz","title":"Visual Studio Enterprise 2019","description":"Škálovatelné a kompletní řešení pro týmy libovolné velikosti","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"de-de","title":"Visual Studio Enterprise 2019","description":"Skalierbare End-to-End-Lösung für Teams jeder Größe","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"es-es","title":"Visual Studio Enterprise 2019","description":"Solución integral escalable para equipos de cualquier tamaño","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"fr-fr","title":"Visual Studio Enterprise 2019","description":"Solution scalable de bout en bout pour les équipes de toutes tailles","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"it-it","title":"Visual Studio Enterprise 2019","description":"Soluzione end-to-end scalabile per team di qualsiasi dimensione","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ja-jp","title":"Visual Studio Enterprise 2019","description":"あらゆる規模のチーム向けのスケーラブルなエンドツーエンド ソリューション","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ko-kr","title":"Visual Studio Enterprise 2019","description":"모든 규모의 팀에 사용할 수 있는 확장 가능한 엔드투엔드 솔루션","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pl-pl","title":"Visual Studio Enterprise 2019","description":"Skalowalne, kompleksowe rozwiązanie dla zespołów dowolnej wielkości","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pt-br","title":"Visual Studio Enterprise 2019","description":"Solução escalonável e de ponta a ponta para equipes de qualquer tamanho","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ru-ru","title":"Visual Studio Enterprise 2019","description":"Комплексное масштабируемое решение для команд любого размера.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"tr-tr","title":"Visual Studio Enterprise 2019","description":"Her boyuttaki takımlar için ölçeklenebilir, uçtan uca çözüm","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"}],"requirements":{"supportedOS":"6.1.1","conditions":{"expression":"not Win10ThresholdBuildNumber","conditions":[{"registryKey":"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion","id":"Win10ThresholdBuildNumber","registryValue":"CurrentBuildNumber","registryData":"[10240.0,14393.0)"}]}}},{"id":"Microsoft.VisualStudio.Product.Professional","version":"16.9.31205.134","type":"ChannelProduct","icon":{"mimeType":"image/svg+xml","base64":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgOTYgOTYiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSI0OCIgeTE9Ijk2IiB4Mj0iNDgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxjbGlwUGF0aCBpZD0iYiI+DQogICAgICA8cGF0aCBkPSJNNjguODkxLDk1LjZhNS45NzYsNS45NzYsMCwwLDAsMy45MzMtLjQzOUw5Mi42LDg1LjY1MUE2LDYsMCwwLDAsOTYsODAuMjQ0VjE1Ljc1N2E2LDYsMCwwLDAtMy40LTUuNDA4TDcyLjgyNC44MzlBNS45OCw1Ljk4LDAsMCwwLDY2LDJMMzQuMTE4LDM3LjI2NCwxNS41LDIybC0xLjYzMS0xLjRhNCw0LDAsMCwwLTMuNjEtLjgzNCwzLjk0NywzLjk0NywwLDAsMC0uNTMxLjE3OUwyLjQ2MiwyMi45NzRBNCw0LDAsMCwwLC4wMTEsMjYuMzY2Yy0uMDA3LjEtLjAxMS4yLS4wMTEuM1Y2OS4zMzNjMCwuMSwwLC4yLjAxMS4zYTQsNCwwLDAsMCwyLjQ1MSwzLjM5Mmw3LjI2NiwzLjAyN2EzLjk0NywzLjk0NywwLDAsMCwuNTMxLjE3OSw0LDQsMCwwLDAsMy42MS0uODM0TDE1LjUsNzQsMzQuMTE3LDU4LjczNiw2Niw5NEE1Ljk2NCw1Ljk2NCwwLDAsMCw2OC44OTEsOTUuNlpNNzIsMjcuNjc3LDQ3LjIxMiw0OCw3Miw2OC4zMjNabS02MCw2LjZMMjQuNDExLDQ4LDEyLDYxLjcyN1oiIGZpbGw9Im5vbmUiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPg0KICAgIDwvY2xpcFBhdGg+DQogIDwvZGVmcz4NCiAgPHRpdGxlPkJyYW5kVmlzdWFsU3R1ZGlvV2luMjAxOTwvdGl0bGU+DQogIDxnIHN0eWxlPSJpc29sYXRpb246aXNvbGF0ZSI+DQogICAgPHJlY3Qgd2lkdGg9Ijk2IiBoZWlnaHQ9Ijk2IiBvcGFjaXR5PSIwIiBmaWxsPSJ1cmwoI2EpIi8+DQogICAgPGcgY2xpcC1wYXRoPSJ1cmwoI2IpIj4NCiAgICAgIDxpbWFnZSB3aWR0aD0iNDAiIGhlaWdodD0iODIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMiA3KSIgb3BhY2l0eT0iMC4yNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFDZ0FBQUJTQ0FZQUFBQW1SNWJLQUFBQUNYQklXWE1BQUFzU0FBQUxFZ0hTM1g3OEFBQUVlMGxFUVZSb1ErMmFXM2ZiSUJDRUIxK2FTOU1tLy85M05va2RKMVlmWU1Sb3RTQnM2cllQbW5QMllFblkraklzcUJVYmhtSEEvNnpOVW9kL3JkMVNoMHNVUWdpbGE4T1ZROVVGV0FCeUlXM1hWdURRMkc4aUF4WUtyV29vdEl1Z0Z3RUttTFlhOXBvQ2FlaTVLbVRURUJmQU5zaGcrbGxoZ1FoeGR0cnhlZ2dobENDcmdBdGdteFJiK2F5Z1FJYlIrRXJYZWN4K3JvcUFCazZkSWxRcEZGQ2h2Z0I4cG10ZnlEb0RRQWpCSFdvWDBJR3pZTHNVZS9tOFEzYVQrVWU0VXdwZUEzSU9qcm5xRFhWdGlCVk93ZllwdnBrZ0pCZi9BUm51QThEUlhOTlE2SWxtZ01rOUM4ZWJFK1lld0YxcTd6R0ZKSVRDSFRDRnMzazV1bWcxQVhUV053VzhTL0dRNGxFK0U1S0FBMksrSFJIaHR1azNPZVNmeVBtbzk1ekpHMkxQdlcrSWNJOHBuZ0I4VHkxQjc1QW55Um5aT2Q1REhkV1pQNEcwZVZqS1FaMFVCTHhIaFBtUjRtZHFuekFGQktJN0I4UmM1ZkV4SFN2Y29rYkF3c3pkSWs4SUJYd0c4SUlNK1pqNmNIZy9rT0ZPeUxBNnl5SHRxS1ZaN0UyUVBYTHVjVmdKK0lJSStJQU1TTGNDc3BOMkNhSUcwODVVR21JdkIrbmdFN0tMejhnT3FtTUVmVWVHczI0dHdnSHRrNFF1S2lSQmZ5STZ1RU84MlJGeGtuRDJlb3V6UmxVdFEweEF1c2lsNVhzS3p1SXQ4aVBzaUF3SHpLSE96cm5sZFZCVWc5UUZtbkdYK3ZLWjZ6bW5DN01MVlh3V093dDB5VWtGMWNmZEh0a3RYVUk4NXp6SXBrbmlRVzVNTU9FSnVwTUl5UCtVVWltWUIxWE5RN3RZenRZbHpHRjFvV1ZZMXlrdno1cWNvMG81Q0V4djVnMjd3dkVQOWR5N0dFcGxIZlJrNGJ4VThGUURhWVp0QWZUa2dkVkFiK3BnaTBwdzNib0c4R1l3bnE0Qi9LdGFBWHUxQXZacUJlelZDdGlyRmJCWEsyQ3ZWc0JlcllDOVdnRjd0UUwyYWdYczFRcllxMnNBTDNxMzBxdHJBRDNkRFBwYVFPOHRWUW15OXZwdVVTMkF0ZGRudGRkcE5aQm0yQnFnM3R3QzJwZmgrdjVaNWJtM0NLV3lyNEE5Tnl6VUY2YUFoTk8rVkFtd0dWWUJCK1F2ZUU2ZEViZTN1Tjk3UXQ3MzVZdDFoYVZDdXJhQkQxZUYzQUhBTUF5RDdKVjR3Nm1iME56elpmdUJES0R1QW5PZ0RjcXdiaTZYM3ZKN2NGcDdjSkRZSTk2QVcyRjBXZDJzd1kxcUthb29PYWUxQis4cFhoRjNtYmFwdjI0bUtpVGdPK25sNDh4Rno4RVNJSjE3QS9BTGNYK08zLy9FZkFQN0JOOUpMdytMcWcyeEF0SzlOMFF3RmxDRTFPOEQ4dzN0UXpwUE43M0pvNjJybGlHbWUrL0lOVE5hUEhHQ1h4THdscjVqaDl3dVE5ck9OQUxLVFBZQXRZS0RoUk1Ec3J0MDFoWlZ2S1gyQ0gvSVp6blhXdlZ4UnZ5cmRJZzVBNEg0dzRRL29GNlc4b284M0FySkNWUlZhWklBMlVHYjBIU1g0TzlZTHV5aGs1cVQraFFhSGFzdU04NkNyWkQybk03c2x0SW81aU9mUURNNFR6TUhFeVFQZFJocXVkbGFYSGJFM01VcVpHMi9XSWVheDNiNW9ZdUUwMW9GKzhkb2VJQXVwQXRvWmpRdy9TSG1EaUczSmpSZnRhOCt6MTMzdktLS2FwSHREVXRFOVhoQVpITkJtcXFBRjBEdFp3VUVNcVRYamc1MkFWSUZVQXVsTFg5OE1LSG5xbVhLRndGU1RwMk4xNnFHUXZ0bkM3MnRET2g0MmprSG1GbTZCRVoxQVZvVmdBRzBBMW45VWNCYnFPWC94ZjlVdndFRXhuUTEwSmJTZmdBQUFBQkpSVTVFcmtKZ2dnPT0iIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpzb2Z0LWxpZ2h0Ii8+DQogICAgICA8cGF0aCBkPSJNMTUuNSw3NGwtMS42MzEsMS40YTQsNCwwLDAsMS00LjE0MS42NTVMMi40NjIsNzMuMDI2QTQsNCwwLDAsMSwwLDY5LjMzM1YyNi42NjdhNCw0LDAsMCwxLDIuNDYyLTMuNjkzbDcuMjY2LTMuMDI3YTQsNCwwLDAsMSw0LjE0MS42NTVMMTUuNSwyMkEyLjIxMywyLjIxMywwLDAsMCwxMiwyMy44VjcyLjJBMi4yMTQsMi4yMTQsMCwwLDAsMTUuNSw3NFoiIGZpbGw9IiM1MjIxOGEiLz4NCiAgICAgIDxpbWFnZSB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEwMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEyIC0xMikiIG9wYWNpdHk9IjAuMjUiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBSGdBQUFCbENBWUFBQUNHTENlWEFBQUFDWEJJV1hNQUFBc1NBQUFMRWdIUzNYNzhBQUFMVDBsRVFWUjRYdTJiNjFiYk9oU0V4N1JBNmUyYzl2MGZzcWZjQzhYblJ6VFJlSHZMbGhJNUpJcG5MUzNuWXFETng3Nk5STmYzUFZhMXE0dTVHMWFkdGo3TzNiQnFySzdydXJsN3FQNmRVK1FLT0ZNTzFDekkrbVh2QVhzRm5DR0JxMWY3bWxVdjF4NTRIOWdyNEFrWnNGTUxjclZndmJXRnZUVG9GWEJDRHR3THVlcXlvQlhrVzFpOWM5MTg4NFZCcjRBZEJiaTZDUE9EV1JZME1BVDdCdUN2dWVwYUhQUUsyTWlCUzVBZnpickVFTFlGL0Rlc1YxbC9aUjBFOUFwWVpPQnExRjVpODFsZHlicVV4WHVCRFN4Q2ZNRUc3SXRaQkw4NDZCVndrQU9Ya1VtSTEyRjlraXRoTThvN3hPaDlCZkJuWXRuSVhnVDBDaGl6Y0JYc1p3QTNjaVZrUmpFQk0yci9BSGdPNnlrc1B2K0RHTkdMZ1Q1N3dBbTRyTEZYMkVDOEFmQWxySy9oK2ptc2E4UjZiQUVyMkVkWkNudFIwRjNtZlUwcUF5NmpsV0MvQWZnZUhuOUZqR1RXYUdBRGcrbjVHUkhxZzF6NWVCZlEyMWs2Qi9MWlJ2QU1YRTNKQ3ZhZnNBajVDemFBV1ljN1RBTytEK3NCMDZBSjIrdTgyYVdqNjdwWnlHY0plS1piWmxvbVhJTDlOeXhDL29ZWXdWZllmSmJhWkRGRnB3QmIyTHlQYWQyQ3ZrQUVEV1E2WW1jSE9PRlFlV21aY0FuMkI0YVFHY0ZzdEFpWVl4SUJLMlFMOTg2OFJ0Qlg4QnN5L2JjemJRTlNvNjNPQ3JBRDEydW9QTGcvc1FIOEE4TVUvVGw4RFp1c0MwVEF0dEY2RFBjL1lQT0xjUitlRS9RTkl2aHJSTmpXTmV2Qzl3Wk04K1ZGOFZrQkRySnBtUVlHNFg3Qkp2MHlMUlB1VDhUby9ZWllmNi9EOTlBYWJHZmhtN0NlRUNHekM3K1Q5emxqYy9UaTkxWFBtMkt6dGUyeXU2N3JMT1N6QVN4MTE0TjdqVGdLS1Z4R0xlSCtpMDMwY2t4aTlCSUVwVzdXRlRhUnpNYU42OFk4bndLcjZwM0ZlODR6Z2hNZE05UHlIRnl0dlpxYTJWeHBlcVlZV1c4WWxnQmQxL0tZVUQyd0dxbk1ETW5SeVVaeDg0QXp4aUdGcXgyendyV2pFU09PWU5TTEJvWlErUE5lRVNFcVRBOG9RZXBtaFoyUHVmVC94Ni9mcW1uQUdSWmthaHl5SGJNMzk3SnoxdVpIMVlmWDMrUWVyYVVLUkpzeTdiNmZNTnpZNEMrR2ZpLytETmIvZ1pvRlBCRzVUSStmRUIycUhMaXN1UXAzcWdIcTVKcHFqbDR4M0pYU3FMYkxRdVVWR1A5eWJhVnBwUmxsV0pEYU1YL0gyTXpRYmxsckx1dm1GRnpJYTUzejNJdDA3WWJ0ZFZ0Zmc3eWZOWHdoam9QdEFaNXhxVmh6VTJuWnE3bTJvVXJCVFVXcVB0WkdTVk15WGF0bkRNME5hMW1PTmgvbTFCVGdESmRxcXVhbUdpb2J1YmFXUXE3MmcvY2FKdDFHZkVLMEt1L05VcDlhM2F4WCtGMzBWazEyMFRNdWxUWlVUTXVha3FjaVY3dmxWT1FDNDJoVnVCWXNiY3M3QUxjQWZvZjFuenkrRGU4VE5DR3prN1p3M2FodUJuQ1FUY3ZhVk5tYW00cGM2MUpadU5veHArQjZxZmdQb21XcGNQK1Q5UXNSOEIxOHdFVXB1d25BR1M2VldwQjJBMEVCSzF3OXJiRUxYSjFmMVk4bVhFYXNCL2NXTVUzcnBvUHVGelB0OTJZTmRQS0FNMTJxejVoMnFiUmpWcmc2dHRpMHJCK21UY25jVGRJdFE5WlpDL2VYUEU3QnpUa0lBQUNqRFllVEJselJwZEp4U0NPWFRaWFhJZXRqL2JDOWV2dUFjYjBsV0Y1dFd0WTk0UlRZMUNpMTFja0NydVJTc2VZcVhEMWo1WFhNMUZ3enhaUk11TnBJS2R5cHRHelRjV3BHN29GeDlBSW5DbmhCbDJxdVk2WnltaW1ia3IybzFjaWRndXRGcTliK2RrNTBaTHBVSGx4ZEtaZHFGN2hlTTBXNFRNbGFieFV1UjZGN0RHZGVDemNKRmtqREJVNE1jRUhONVVHNWZXZGRGVDlFVzI5Zk1UWXV0Tjdtd3MwWmd3WWc1dzdjQVNjRU9NT0N0S2NnNStCNnMrNlVpZUhWVzhMbHFVaTZVRHJmS2xqVzNKU0o4WUp4dmRXb3pZS3FPZ25BaFJZazRYcWpVRTdrMHI3dE1JeWNxWG83MVV3cFlOdE1FUzdISUoxdHQ1RmJDbFYxOUlBTExFZ0xsNUZyeDZFNWw0by95NE9iWTE3WVpvcVBiekhkS2F2SHZJM2FmZUFDSndBNHlLYmxsRXVsYVZrUHlWa0wwbk9wdE9acXZlMnhtM21ScXJjNjR5WmRxWDNCVWtjTnVOQ0M5RXdNQld6aFRuWExXbTlMelFzRlc5Sk1WWWNMSERIZ0NoYmtEd3pocGc3S2VkRnJtNm5jZXF0WHJiZjNpSTJZZGFhcTFWdFBSd200WUJ5YXN5Qzk0emFFUzM5Wk5kZE1sWmdYWHIwbFhLOVRyZzRYT0VMQWxTeklPU1BEcHVWZUZqLzhHdWFGNXltUG5La2x3RkpIQlhoQkM5SnVIbmhwZVNuell0S1pXaEl1Y0VTQU15M0lmUS9LZVhDMVV6NkVlYkZZdmZWMEZJQXpYS3A5RDhwNURkVSt6WlFDUHJoNVVhSjNCMXpvVXVYQTlRN0syVmxYNFo2Y2VWR2lkd1ZjNEZMVk9pZ0hqRHZsa3pNdlN2U3VnSU5zV3ZhTWpGMFB5bG13UUFSTHVDZG5YcFRvM1FBWHVsUWxCK1Z5SXJlazN1cjFLTXlMRXIwTDRBb3VsZTJZUFgvWnpybk5tQmNsT2pqZ2lpNlZkMURPUm02cVV6NXA4NkpFQndWY3lhWEtPU2lua2F2T1ZCUG1SWWtPQm5oaGwwb2pGL0RIb0diTWl4SWRCSENtUzdYdlFUbU4zRjJhS1FWODFPWkZpUllIWEZCemM4OVNwVndxSUVhdVYyK2JNUzlLdENqZ0RBdHkxNE55SGx6QWI2YWFNeTlLdEJqZ1Fnc3k5NkJjNnFpTjdaU2JOaTlLdEFqZ0FndHk3cURjbkVzRnBKdXBaczJMRWkwQ09NaW01WlJMcFduNUo2WXRTSGJNMXNBZ0NJNHdUWnNYSmFvT3VOQ0M5RXdNMnpFcjNGUmFwdWx3RnVaRmlhb0NybUJCL2tDNjdtcERwYnRCbm5seGgyRjluWUo3Y3VaRmlhb0JMaGlINWl4SVBRVnA5M1FCZnd5eXpaUUg5aGNhTVM5S1ZBVndKUXR5eWwvV2hrcmgydmsyQlZjajkrVE5peEx0RGZnQUZxUzZVem9HZWVaRkNtNnFtVHBKODZKRWV3SE90Q0JMRDhyWmhnb1l3cldkY3FxWllrcitqY2JNaXhMdEREakRwY285S09kMXk1cVdDZGZ1NGRxZElLMjFGdTVEV0UyWUZ5WGFDWENoU3pYWFVFMmxaVWFYWnp1V05sUE5tQmNsS2daYzRGS1ZISlJMd1UxMXlydnNCRFZqWHBTb0NMQ0JPMmRrekIyVXN6dERIbHoxbFBmWkNXckt2Q2hSTnVBRTNJOUl1MVE1QitYVXhPZ3dISU1JdDJRbjZCYXgzdHBtaXAxeXMvWFdVeGJnek1oVkl5TmxRZEpmOWh3cTFrUENmUXhyMTUyZ1p3enJiVFBtUllrbUFUdjFWc2NoYTBHbVpsMEM5allQMkMwVExqdGxiNXZQZHNxMjNqYTNFMVJEU2NBSnVIU292TWlkTXpJMExWOWpHTGs2NDNyTmxBWHJOVlBON1FUVmtBczRNUVpOcGVXY1U1QWF1WVRMdEttZGNtNjliWDRucUlhbVVuVEt4UERPVXFVT3lsbTQybENwZ2FGd1M3YjVtdDRKcXFFUjRFVDBsc3k2VE1rSzE4NjVUTXUyVTdiMVZsUHoyZTBFMVZBcWdsUFI2NDFEdXYzSGxkclBCWWI3dU53dzhKb3BCWnd5TDVyZUNhcWhBZUNacnBtMTl4UEdQck11UmkzaFhtTG9LK3RXbjdlSGE2TTNaVjQwdnhOVVExNEVkL0hoeU5Ed1VqVHJNSSsxV25lS2NCbGxkc1BBc3gzbm1xbXoyQW1xb2JrVWJWMHJiYkkrSVVZei83clBPNEh4SnN2YkRkS28xU2oybXFtejJnbXFvWnd1Mmh1VEZQYWx2TVlPR1lqcDJJNUNVMm5aMXR2VnZOaFRPVlpscWk3cmN5Qis0Qng5T214Z3NHT2VHNFdZcWxmem9xSnlBUGR5NVNKSTNkTGp5UE1oM0VQSU5uclpOQkV1UWEvbXhRSktBZTR4Qmtxb0JLb0doYzY0YktRK21PZFBpTkZMd0wvbCtXcGVMQ0FQY0k5b1JpaGNqZFFuYktEcTN3ajE0UjYrcDkzek15SmcxbDlkM3ViOGFsNVUwQUJ3My9kOW1JVTl1Qit3K2VEWlRPa2VManZrUjhTL0g5S05CTjBoSW1SR3JEZmZydVpGSlUzVllGdHZYeEM3YWUyVStkNFR4b2ZtR05WOG41QUptbnUrT3Q4UzdtcGVWTkJVRFFZMkh6RHI2RlRIL0FUL1R6c1p3YXpYSEhrZTViR1hrbGZ6b3BKR2dDVk5BeEhrNEJaRWNQWm9qVWF2cG0vZXgzdWY1YmxOeVdzelZWRnVCQWZJZkxxdGZ4alhaUVdzWm9jQ1pwclc3cHRMall1MTNpNmd1VGxZUDJCTm16b0h2MkRZZUtrSkFveEhMRjd0Ykx2VzJ3WFV6WDJPaWYxaGExOWVtTFVOZi9pL0ZHL3d3YTRwdWJKbUFRTWp5THhxcEY2WXE5N2JZd2o1VFI3cmUydlVMcUFzd0ZRQ3RGMzZQakNzMzNadDMxL2hMcU1pd0pTQUJzWlE5VDFWNzExWHNNdHFKOENVQWIxOTJYa05HRFpzSzlnRGFTL0FWZ25nVzYxUUQ2K3FnRmNkbnk2d3FtbjlEL1FHdkxnc0FUTDVBQUFBQUVsRlRrU3VRbUNDIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6c29mdC1saWdodCIvPg0KICAgICAgPHBhdGggZD0iTTk2LDE1Ljc3VjE2YTMuNzg2LDMuNzg2LDAsMCwwLTYuMTg3LTIuOTI4TDE1LjUsNzRsLTEuNjMxLDEuNGE0LDQsMCwwLDEtNC4xNDEuNjU1TDIuNDYyLDczLjAyNkE0LDQsMCwwLDEsMCw2OS4zMzNWNjlhMi4zMSwyLjMxLDAsMCwwLDQuMDI0LDEuNTQ5TDY2LDJBNS45NzksNS45NzksMCwwLDEsNzIuODIzLjg0MUw5Mi42LDEwLjM2NEE2LDYsMCwwLDEsOTYsMTUuNzdaIiBmaWxsPSIjNmMzM2FmIi8+DQogICAgICA8aW1hZ2Ugd2lkdGg9IjEyMCIgaGVpZ2h0PSIxMDEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMiA3KSIgb3BhY2l0eT0iMC4yNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFIZ0FBQUJsQ0FZQUFBQ0dMQ2VYQUFBQUNYQklXWE1BQUFzU0FBQUxFZ0hTM1g3OEFBQUw2MGxFUVZSNFh1MmQ2M2FiU0JDRWF5UWx6bVYzMy84OWQ1UFljV3oyQjVRb1N0MHpZRXV5THZRNWZXYTREUW9mMWRNMFBxRjBYWWZWYnRjMnJSMVd1MjdidFhaWVlxV1VVdHZlcmVIaTdGYmVjODBUb0Jua3lZbFcyT2V4TnluWXdKYWtkZXUwNVJBcjZOUGFJc0FDVmx0MzNRNU13YXIzTzY2Z1QycXpRblFDZG9NUjZzWmEzVmVodmc3ZVNic0h2a0krdmxVVkhJUmloN2tCc0pYK0JsUEl3QlR1eStDdjVoMkFycFN5UWo2eXBZQk50UXBYb2U2R1BsdXUxM0JOaUM4QS9rakxmcEY5MXBCOVpBc0JCM0JWbllUNWFlaC9rajYzRVRMVlM2RFA1Z29ha05CZFNpa3I1UGRiTFVSSHFsV2duOFVmWk4wT1U4Q3Y2Q0grSHZ4cGNDNXYwTU11bUlKZVEvWVI3QUN3cVZmaEt0aUh3YjhBK0RxMEQ4TzJUeGhWM0tHSDlvd2U2dVBndjRaVzFhNlFDUm9yNVBkWnBtQ2ZkeGwrUDZPSCtRWEFOL092R0NGVHhRelB6eGpCL2h6Mis0RnBTS2VTYWV1OGZBU2JBRTdtWGlaUFZDL2gvaVgrSFZQSW56QlY4RytNY0gvSVBxcDJUYzZlc2M3TFI3Rkl3UkZrenIwTXoxL1JRLzBMd0Q4QS9oNzYzOURmQUorSDQ0QWVGTVB6RDB5VlRzQ2FnVWNoRzFnaHY4bGFJVnJuWUZleFF2NGJQZWp2R0FGdU1RTDZqUjR3YndBcWVDZnV6OU9FckxZbVh3dXRWYW9rNUlJcFpGVXpRelpoZjhjSXNLQlhIcE1zd3RjUTdTcld4RXZEdGhaSTF1UnJwdTBCSjRXTi9XWk1MM2owNktUQXZ3N0xXNHp6OEJQRzBNendySUFqRlN0Z0Q5ZHI4alhEV2dwVzZ4SlgwMGVxQjR6WjlDdEc1U3Jjblhta1pJZE1XNU92R1JZQmppQnF1VkZMalY2VmVzRUlYUit2Z0JHc1ExVzREbG9CYjlEUDVhN21kVjZ1MkI1dzEzV2R2VndBRHVFK1k2eEdhZEdDWVprVktpMC9VdFZBRzJ5a1lwK1hQV1N2UlpHS1pTSGExVXUxN3RCRC9HVCsyVnkzT2JRTkR1SHRyQjhkazgzTHdGb1VTUzBMMGN4K3FaSU54bnF5WG1nSE5HZE8vWXdwTkFlZEhWY0R2QlpGRXBzQWxqRE5DMFBJZnhCbjFacGRSOHJNQVBsTlVqdDJqcHJYb2toaXRSRE45blhvKy94TXE0R09ZTENNdVV1T3lSUmRBMDNJYW12eWhRQ3dKVnNLK0EvaVJ5TmdDa3BWNlJDSzlMZkJjVFhZbVhNOFBjZGFGQmtzVlBBQWViOG9iUmNzQTdHS004RHFEaWdDWEp2ZmErZFlpeUxJUTdSRDVsdzgyV1ZvaTdRWllMOEJIUEpPMW1kcXprSzFoMndkbTNhM3lWY0tHRWlUcnJsaDJpKzRBdVQrYkFtcHBtUlZzZmNkOEFiOTQxeWs1cnVDWEFVTWpDRnRVRE1mb1lCK1RnYnE2blJWTzJodEZiS3FtNkJyY0JVeTI0SnA1WXUvOTY2U3J5WmdtcW1aaWRlK2lvUVlxc1BONE90K08rblhsTndDcmVlbDNWM3lOUnN3Y0FDNXd3ZzRnNmNBRmFJRG9QTVZveXA1RHVnTWJuUmozVlh5dFFnd01JSDhpdjRDcW9wcEdlZ01zSUp3eU5FeEdkeldzektkZHZQSjEyTEFRSnA4UlhPeUxpdk1HdUFJc283aDhKWUEzdURPM2tpOUNUQVFQa1lCNHhzbnRSYmdDSGpCOUZrM0c2T2w0QWgyTWIvcE4xSnZCZ3lra0dtdXZBaEtCamM2MXRmVlFFZkFhK2NDYnZTTjFMc0FBMDNJUUE2NmRjSFpNbHhIa04wejBITUEzK1FicVhjREJsTElCZU5ibmhaY1ZhUURMaGlWeUg0RXVnYVhnR3V3Yi9LTjFGRUFBeUhrMmlOVXBxWk5jZ3lOa05uNnNSRnNMNUJrcWlaa3RhdFB2bzRHR0dpR2F3Y1JnZWZCM3VyeEJGTzdhVnl4TGZjSVVuQWpSWkdqQWdZbWtCVndCak9DMU5wSDk2T1NnU2trVjdKWHZsVFZXVFFoWk9DS2s2K2pBeGJ6a3VaY2FBb3BnZzZNaVpkQzluRWlCV2VoMmtPMm53KzQwdVRySklBSEZSY2NsalRWaXJRWjZHaVpyb25YQm5ueUZhblpGUjBCM3VBR2lpSW5BUXpNcm5acFB3S2RlWFFEUkVwMndEVzQyYnlzZm5WRmtaTUJCbVpYdTk0S1ZtK1M3ZEJtKzZpQ28zQmR5NjU5VE9DS2lpSW5CUXcwTTJ1MXVZQ2pDKzdyZFR3Zk13TzlCUERWRkVWT0RoaG9RczRVcDBDeUM2NjJrMjBPSlJvdmd6dEh6VmRURkRrTFlLQUpHY2lCMU5Ua0R2UlFmRjAwOXRiNkhxNXJvQWxaN1NLVHI3TUJCbzVXMG94QXE3bUNIYmlQNHdwdU9jZldjMXhzVWVTc2dJR2psRFFqRmZ2eHNHUG1qTzBoZW1lZWhXdUZERnhZOG5WMndFQXpYRHVBQ0V3R2JUL28wTy9zT0NBK0psSnlGcW85WkVmbnZwams2ME1BQTR0TG1xcmVKZEJoMnhpdUVSeWZxVGw3blBLb2NwRkZrUThETExha3BMbXhmaFF1YXdvSFlpVkhnR3R3czNsWi9TS0tJaDhLZUZCeFFidWtHWUd1dVVPRnJkYytnckZyNFhyT1k1U08vYUZGa1E4RkRNd3VhVG9JRFkydEM2em1ZRGZvcjBGMjgyU2dsd0QrMEtMSWh3TUdacGMwZ2ZtQS9VSTcxT3pHaVFDemplRE9VZk9IRmtVdUFqQXdPN091S1N4VGtoci92ZHpPUGkrMDN4QU9Xcy9wYzNUMld3aFo3V3pKMThVQUJwcVFnUngwQmpkU2NBUzVCTXZST1Z6QkxlZVkrcHZPV2hTNUtNREEwYXBkZmxIZE9POUM5aWxvS3psU3NhcFpWVjI3NmM1V0ZMazR3TUJScWwxY3oyUDBXRjIzbFQ3WDF5SUF0N3RTczFEdElWdkhvWjAwK2JwSXdFQXpYRHZjREFhQ1phN1Rkb040SFpjZGNLYm03SEhLSTh2WmlpSVhDeGhZWE8yS1FMVDJjYWphWnVzaXdEVzQyYnlzZnJLaXlFVURGbHRTN1hLNDJUeEljTEJqdU53TjYzUmVacXRqMThMMW5NY28vUzFITDRwY1BPQkJ4YnpndFdvWDJ3eDBheHVOVUl1MEROZE16blFNRDlVT2VnbmdveGRGTGg0d01MdmFwZjBJWnVRUllHQUtXZGZWeHQxS0c4R2RvK2IwNnpPMHBiQ3ZBakNRSmwzSC9BTStQZDRWck50cDJibTIxdmR3M1FLdGtQY2hlekRlNlAzQ0ROaFhBeGhvWnRacWN3RXJhRWdMMlVaalA3bzVvbk81Z3VlNGo4MThvek1IQkhZTjlGVUJCcHFRV3hlK05nOUNXdG9tV0srZ2dmcTg3Q3BXTmM5Snd2amZNbFBKRGhsb3pOTlhCeGhvUWdicUZ6eUNxKzdXZ2h5Tm9lZDJsWHFvemg2cm5qRDlqOVlKdXNNVU5sQ0JmSldBZ1JSeXdiS1NaZ1NhcHNzWjVJS3BtaUxBbVpvakZUdG9mdjVQdi9INE9vd2R6czhPK1dvQkF5SGtwU1hOU0lHdy9nN1Q5V3oxK1ZqSDBlTWp3RkhDNWJEWjUrZi85TE1KK3RWV3dDREQ3S29CQTgxdzdSYzZnaDdkREJIMExRNnphMWpmbGU3bjhqQWNoZTNJbjNENGIzakJDSlFoRzY3aXF3Y01UQ0RQS1dtcWV1ZEExL0VnMi9VUktvSk01ZXU0V2NpdVFkL2FNZnFiT0EvcmI3MmRFQjBZRXhDZ1hkTGNXTjh2WWczMkhNaTE4L3E1dkIrMVBJY21XYS9EOXR0NVRNck1xbDBkVHZzSGZFWGFDREw3YXRFNGtQMDZjMTNIaE9yRlhNZUx6bms3Z0lFRHlFQmMwaXkyN0NySzRFYkhjVGtEcS9zQmRXRDZMYXJzaStucS9odjNwdlB3VFFFR2tDVmR4L29EUG1CNk1SMnlHdmRUK0J5WGM2eC9sdWdCMHkvRTZjZkVkQzZlQUszWnpRRUdVc2cwVmFUQzFBUW5VM0prQ3BuV0JhMkhYMXAwby9tTkZZMmRMVTlLbHpjSkdHaENCbkxRR1Z4M1dtZjdjVjBVamx1aFY2dFc3bHF1MUpibkMrMW1BUU1wNUlKbDFTNVhreHVmajZsa29BNVhQdzM0SzNGK05wQ1ZMQzkwUkxCRHlEY05HQWdoTDYxMktWaFhib2ZwL0Zod0NQY1BZcmlQR0Q5NS94K0FmOFgvRzliL3dBaWMzNFZVcFN2Y0VQTE5Bd2FhNGRyaFppR1o3aGVVU1pBRFptaWwraUxsRXFJRGR0QS9NU282ZXdFQkFBZXZEdThDTURDQlBLZmFGZmwrS0l3S2ZjSDRuZVN0N0tQS1paalZyN1greEZTOWtSTSs5NldLUFZTbjZnWHVDTENZSmlldGFwZkQxZU01ci83RytCVnpLcGpibm5Hb1dvZXJyVVBsTVU4WTRSS3d6c01BRHRVTDNCbmdCZFV1dHBGNkNaZGg5d3ZHTU0xOXVKMWdOQ1FUcm9MOUthMG1XanhlUTdQRDdkRC8wMVlGQXdlUWdUR2NBbE9ndGJCTTVUNmkvekEyUDVsYlpEd0NWdVdxUW10cTlRemFINSthb1psMmQ0QUJaRW1YVjd0cW9abnd2cUZYY0FTWVNWVUdXTUZ5ZnFaYVcyRDN6NytaY21sM0NSaElJV2ZHaTZyd0h0RURabG1SU1ZZTnNNN0JFVmgvM3RVQ3g0RnFXM0NCT3dZTXpJYXNGMVhWK3d0OWVIN0ErRTNrU01HYVBYc2hZeWxZWUFGYzRNNEJBNHNnUjNNcnd6T2ZnNEV4akRQVU1sRlNxQ2NIUzd0N3dNQXN5Tkg4K3d2VHR6MGNvTVBoTTNEa1d1RTZPbGphQ25pd0NtVFBvUGw0OUlocEZjc1Z6SDJwZW5WbTRpY0RTMXNCaXlXUUhiQVhON1FPemYwVnNpdFZNK0tUZ2FXdGdHUGp4WTBnLzBFUDlUZW1jQjB3L2NYYXM0Q2xsU09QZHhOV1JobDd5YkwyS3BIV1lYcER2RXBmMjVPQ3BhMkFLMVlCSFRsTndXVyszKzlVWUdrcjRJWUpaQ0F1WmVwMk5RZXQ2MDRPbHJZQ25ta0I2TGwyZHFocUsrQTNtTUd1MmtkQVZWc0IzN2pwSDRxdGRvUDJQNHFBMHNRajA0YUVBQUFBQUVsRlRrU3VRbUNDIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6c29mdC1saWdodCIvPg0KICAgICAgPHBhdGggZD0iTTk2LDgwdi4yM2E2LDYsMCwwLDEtMy40LDUuNDA2bC0xOS43OCw5LjUyM0E1Ljk3OSw1Ljk3OSwwLDAsMSw2Niw5NEw0LjAyNCwyNS40NTFBMi4zMSwyLjMxLDAsMCwwLDAsMjd2LS4zMzNhNCw0LDAsMCwxLDIuNDYyLTMuNjkzbDcuMjY2LTMuMDI3YTQsNCwwLDAsMSw0LjE0MS42NTVMMTUuNSwyMiw4OS44MTMsODIuOTI4QTMuNzg2LDMuNzg2LDAsMCwwLDk2LDgwWiIgZmlsbD0iIzg1NGNjNyIvPg0KICAgICAgPGltYWdlIHdpZHRoPSI1NCIgaGVpZ2h0PSIxMjAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDU0IC0xMikiIG9wYWNpdHk9IjAuMjUiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBRFlBQUFCNENBWUFBQUM1RndIQkFBQUFDWEJJV1hNQUFBc1NBQUFMRWdIUzNYNzhBQUFHTTBsRVFWUjRYdTJjVzNQaktoQ0VXODUxdmYvL2Y1N2R6VDJ4T1EraVRUTWFoSlNLSEpPaXE2YWt5TEt0enowTTVJRVpRZ2o0aWRyVmJtaFYxN1VicklaaEdHcjNBRUQ0NWxSWUJPYkFWT0gwTGQ4Qk9kUytVNkJLUjAraGNEd2I1S3hqQnFvVVZnRTVrTWJKeWEwQmkyQU8xRTZPTzhBRjVNTWU0N2s5bm1DMkJuVEJDbENNSzNOVU9ENzhzUktiQTg2bG9vVzZrcmlPY1NXdlV3UTdGT0lzZ0JNd1V3RVZUb0Z1SkFqTGUvblFCd0FmTWQ3ajhTREhUUUdYT0thdVhRTzRkZUlhS1MzcEdJRVliMGlBak0wQVMyRFdOVTFGd3QzSHVFTU9wNmxJb05jWWIwaUFDdm5sZ0xVSjJycEdzQnVNUUw5aUtCeGRvMk9FZWpISFRRRnJxV2ovSHBDN1JyZzlSdmR1a1FySkVRbnNKY1p6RFA2OUZqQU13ekFzZ1pzRDg5NnNjRmRJS2JsSGdydkJDTWR4OW9ZYzZpbkdVa0JDbmliNkplN1ZVbEhmbUUyd1NLbEp1TjhZNGU2UVB2ZUlOTVlJOVlnRVZ3SzBoY1k2Q0ZUY0s0RUZwRlJrTVFnb3c5MGdkKzQyWHVkWVl6b3EyQ055U0FWOHdiVFFjS29BRnNCTndFSUlJYzVsd2NUUkJEOXNRQ29vZEk0cHlYbE5YYk5nRnZBSjR3K2pMdTR3d2dFaklDQndjTFFrRlMyVVhVRUFhZHhwV3Q0aHVYWkFjdTAzcG1BUGNuNHZyOThnWCtFTVNJQkEvUDVoR0NianJWWTgxRGtQU3AzakJNNnBZSS9jTlp1U2U0eVFqL0djZ0wvaThSWnBaVU1vWUpwSkFRQnNTcnBnaFhSYzRwcXVUdTZRWEFQU2hQMktORVVRY0k4RVpTZDlnZ0UrRkdFV093YlV4NWtXRlNDSG8zTjBMV0IwalNzV1R1NTdPVmN3ZFFzb1B3TlRQVk1SYkVVUnNYQzZycnhCcXBCQXFtenE2TDBFblNJVTA0OVo4bUdDV1RNQWVZV3NPVWFWVXNDbUEyV1hZTHJVT3NCZlNETVVpaitrTHFSMUd2RCtId1N3SE14cURrNi95UDV6Q296ZmVVQUMxbitCdUpEVzFOVmxHZWM2V3lrbldnTm1BYXhMcWhJY0gvZ0tJeHdkNVJKTng1TXV4NTZRRnR2ZUZEQ0JXd09tVWpndkZWVVdFaGp2MzVuUThjU3A0UmxwRGFyanp3UDdrbFJjSXYwbEIwd2ZJQlN1YWVVa1VBMXFvaTNCckVwd0ZNY2V4OXN0cHVPdjVOUUpqcFh4SEdEdUx5clhkMGpqVHF1b0YxNzY2dWVmMHBFNXY3VXNYQ2xGN1lOclVTbkJVTm0xYzRGNXNyLzBtb0E1bitnN3dlWlVBeXNDVVpjR1poMzV0QzRKektZbWo0c2Nzcm9rc0M5VkIydE5IYXcxZGJEVzFNRmFVd2RyVFIyc05YV3cxdFRCV2xNSGEwMGRyRFYxc05iVXdWcFRCMnROSGF3MWRiRFcxTUZhVXdkclRSMnNOWFd3MXRUQldsTUhhMDBkckRWMXNOYlV3VnBUQjJ0Tmx3U211eWJXN0oxeGRVbGd3UExkVGxWZEdoZ1ZGc1NzdmhQTXB0NmFnRG1mNkZ4ZzlnRzhNUlF3M2Z0WjJwN3NBV1hYemdGVytsVXREQ0hzQmxPTnVVMnYvRXdBNTkwLzVxVlJ3QlRJZG13cHRjVHcwaE5yZDlWK1JpV1FFcFIyYXRGbUN0cTlSWHZ2ektYbHA4SFc3RDJ4WTJnT1N2dDlhTmllSHlYblRsb0Q1dTNmS3NuQ0hNMXJoR0thYVErQ2Z5YllPZUk1M3JmSXRUVmdxcms5WFI2VVBvUkMwU251Y1g3RUNQTW54dC80OXdQU0h1ZzNUTUVtV2dwVzI2Um0zU01VVTQxZnpyOUxVQThZUWY0QytDL0dYNlMyR0FSajQ1TDFxZWkwU1dQc25GQTR1a1FBZmthUWF5d0k3S09qZlQwSTlrZmlIL0t4NXFaaXRlMkZhQzBVblNMQUxwNERLZjNVSlJZSFFoR01vYW40R045akhkUHhlNUlMVm5CTFlleHVWeUNIZXNQNDJjZjRYbDdYSW1HaEZFN0RGZzl2akswcUhoYk9BNkpqUU83VWE3em10WmJSYmtkZWF4bnRxYU45ckVwUUFMQ3F0UXl3enEwRFV0TUR4SE82dGJZWjBGelBLbDAvQm94TWRjZFdGQTBkVjNUcUpiNzJqdVNXVFVFTDVzSE10VzhxclJNemxSd2IwdWtKU2lFcGxuU0ZPaUI5cm5WTHdlYmNxVFhjY2l1aGFra3E2cmtIeGZRakZNKzFrSEJzZlVtTE5GU2dnR1hGUThVUDVSY3FGTjNodURzaUwrOEt0d1RJYzZnS1JNMkIyVGNybEpaMGhib0dpcFB4aXptdUFRSldRQUgxVk5SZmlxbW5EOHlxOTQ0RXFmZnEwc21PbjAyQXFCSll3SFExd2ZTalcwQSs4YkxBQkxuK0xtRUx3aVpBVkMwVjFTMCtMT1FheHhrYi9kQkJkWmVBSDNKdE15QnFBaWFOdG9CcEd0cHJIOGduYXI2dUR0dllGSWlxT1FhTUQ2RFg5TUh0L0dZZExzVm1RRlN4eWJpekF0bkowUUtwdzBCNmVIdmNISWlhN1o0K3M3enlWaUdVZmZpekFsRS90aTE4RlF6STRFNlgzQnR6blIxR3RRaE01VUM2K2c0WTFXcXdWcVR6ejQvUy8xSEw3YllXaGVSYUFBQUFBRWxGVGtTdVFtQ0MiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpzb2Z0LWxpZ2h0Ii8+DQogICAgICA8cGF0aCBkPSJNNjYsOTRjLjEzOC4xMzguMjkyLjI1NC40NDEuMzc3QTMuNzUxLDMuNzUxLDAsMCwxLDY2LDk0Wm0uNDQxLTkyLjM3N0EzLjc1MSwzLjc1MSwwLDAsMCw2NiwyQzY2LjEzOSwxLjg2MSw2Ni4yOTIsMS43NDYsNjYuNDQxLDEuNjIzWk05Mi42LDEwLjM0OSw3Mi44MjMuODM5YTUuOTY4LDUuOTY4LDAsMCwwLTYuMzgyLjc4NEEzLjUxNywzLjUxNywwLDAsMSw3Miw0LjQ4NXY4Ny4wM2EzLjUxNywzLjUxNywwLDAsMS01LjU1OSwyLjg2Miw1Ljk2OCw1Ljk2OCwwLDAsMCw2LjM4Mi43ODRMOTIuNiw4NS42NTFBNiw2LDAsMCwwLDk2LDgwLjI0NFYxNS43NTdBNiw2LDAsMCwwLDkyLjYsMTAuMzQ5WiIgZmlsbD0iI2IxNzlmMSIvPg0KICAgIDwvZz4NCiAgPC9nPg0KPC9zdmc+"},"releaseNotes":"https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.9#16.9.4","supportsDownloadThenUpdate":true,"localizedResources":[{"language":"en-us","title":"Visual Studio Professional 2019","description":"Professional IDE best suited to small teams","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-cn","title":"Visual Studio Professional 2019","description":"为小型团队量身定制的专业 IDE","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-tw","title":"Visual Studio Professional 2019","description":"最適合小型小組的專業 IDE","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"cs-cz","title":"Visual Studio Professional 2019","description":"Profesionální integrované vývojové prostředí (IDE), které nejlépe vyhovuje malým týmům","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"de-de","title":"Visual Studio Professional 2019","description":"Professionelle IDE, optimal für kleine Teams","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"es-es","title":"Visual Studio Professional 2019","description":"IDE profesional ideal para equipos pequeños","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"fr-fr","title":"Visual Studio Professional 2019","description":"IDE professionnel, mieux adapté aux petites équipes","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"it-it","title":"Visual Studio Professional 2019","description":"IDE professionale ideale per piccoli team","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ja-jp","title":"Visual Studio Professional 2019","description":"小規模チームに最適なプロフェッショナルな IDE","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ko-kr","title":"Visual Studio Professional 2019","description":"소규모 팀에 가장 적합한 전문 IDE","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pl-pl","title":"Visual Studio Professional 2019","description":"Profesjonalne środowisko IDE najlepiej dopasowane do małych zespołów","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pt-br","title":"Visual Studio Professional 2019","description":"IDE profissional mais adequado para equipes pequenas","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ru-ru","title":"Visual Studio Professional 2019","description":"Профессиональная интегрированная среда разработки, оптимально подходящая для небольших команд.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"tr-tr","title":"Visual Studio Professional 2019","description":"Küçük takımlar için en uygun profesyonel IDE","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"}],"requirements":{"supportedOS":"6.1.1","conditions":{"expression":"not Win10ThresholdBuildNumber","conditions":[{"registryKey":"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion","id":"Win10ThresholdBuildNumber","registryValue":"CurrentBuildNumber","registryData":"[10240.0,14393.0)"}]}}},{"id":"Microsoft.VisualStudio.Product.TeamExplorer","version":"16.9.31205.134","type":"ChannelProduct","icon":{"mimeType":"image/svg+xml","base64":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgOTYgOTYiPg0KICA8ZGVmcz4NCiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImEiIHgxPSI0OCIgeTE9Ijk2IiB4Mj0iNDgiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2ZmZiIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZmIiBzdG9wLW9wYWNpdHk9IjAiLz4NCiAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDxjbGlwUGF0aCBpZD0iYiI+DQogICAgICA8cGF0aCBkPSJNNjguODkxLDk1LjZhNS45NzYsNS45NzYsMCwwLDAsMy45MzMtLjQzOUw5Mi42LDg1LjY1MUE2LDYsMCwwLDAsOTYsODAuMjQ0VjE1Ljc1N2E2LDYsMCwwLDAtMy40LTUuNDA4TDcyLjgyNC44MzlBNS45OCw1Ljk4LDAsMCwwLDY2LDJMMzQuMTE4LDM3LjI2NCwxNS41LDIybC0xLjYzMS0xLjRhNCw0LDAsMCwwLTMuNjEtLjgzNCwzLjk0NywzLjk0NywwLDAsMC0uNTMxLjE3OUwyLjQ2MiwyMi45NzRBNCw0LDAsMCwwLC4wMTEsMjYuMzY2Yy0uMDA3LjEtLjAxMS4yLS4wMTEuM1Y2OS4zMzNjMCwuMSwwLC4yLjAxMS4zYTQsNCwwLDAsMCwyLjQ1MSwzLjM5Mmw3LjI2NiwzLjAyN2EzLjk0NywzLjk0NywwLDAsMCwuNTMxLjE3OSw0LDQsMCwwLDAsMy42MS0uODM0TDE1LjUsNzQsMzQuMTE3LDU4LjczNiw2Niw5NEE1Ljk2NCw1Ljk2NCwwLDAsMCw2OC44OTEsOTUuNlpNNzIsMjcuNjc3LDQ3LjIxMiw0OCw3Miw2OC4zMjNabS02MCw2LjZMMjQuNDExLDQ4LDEyLDYxLjcyN1oiIGZpbGw9Im5vbmUiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPg0KICAgIDwvY2xpcFBhdGg+DQogIDwvZGVmcz4NCiAgPHRpdGxlPkJyYW5kVmlzdWFsU3R1ZGlvV2luMjAxOTwvdGl0bGU+DQogIDxnIHN0eWxlPSJpc29sYXRpb246aXNvbGF0ZSI+DQogICAgPHJlY3Qgd2lkdGg9Ijk2IiBoZWlnaHQ9Ijk2IiBvcGFjaXR5PSIwIiBmaWxsPSJ1cmwoI2EpIi8+DQogICAgPGcgY2xpcC1wYXRoPSJ1cmwoI2IpIj4NCiAgICAgIDxpbWFnZSB3aWR0aD0iNDAiIGhlaWdodD0iODIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMiA3KSIgb3BhY2l0eT0iMC4yNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFDZ0FBQUJTQ0FZQUFBQW1SNWJLQUFBQUNYQklXWE1BQUFzU0FBQUxFZ0hTM1g3OEFBQUVlMGxFUVZSb1ErMmFXM2ZiSUJDRUIxK2FTOU1tLy85M05va2RKMVlmWU1Sb3RTQnM2cllQbW5QMllFblkraklzcUJVYmhtSEEvNnpOVW9kL3JkMVNoMHNVUWdpbGE4T1ZROVVGV0FCeUlXM1hWdURRMkc4aUF4WUtyV29vdEl1Z0Z3RUttTFlhOXBvQ2FlaTVLbVRURUJmQU5zaGcrbGxoZ1FoeGR0cnhlZ2dobENDcmdBdGdteFJiK2F5Z1FJYlIrRXJYZWN4K3JvcUFCazZkSWxRcEZGQ2h2Z0I4cG10ZnlEb0RRQWpCSFdvWDBJR3pZTHNVZS9tOFEzYVQrVWU0VXdwZUEzSU9qcm5xRFhWdGlCVk93ZllwdnBrZ0pCZi9BUm51QThEUlhOTlE2SWxtZ01rOUM4ZWJFK1lld0YxcTd6R0ZKSVRDSFRDRnMzazV1bWcxQVhUV053VzhTL0dRNGxFK0U1S0FBMksrSFJIaHR1azNPZVNmeVBtbzk1ekpHMkxQdlcrSWNJOHBuZ0I4VHkxQjc1QW55Um5aT2Q1REhkV1pQNEcwZVZqS1FaMFVCTHhIaFBtUjRtZHFuekFGQktJN0I4UmM1ZkV4SFN2Y29rYkF3c3pkSWs4SUJYd0c4SUlNK1pqNmNIZy9rT0ZPeUxBNnl5SHRxS1ZaN0UyUVBYTHVjVmdKK0lJSStJQU1TTGNDc3BOMkNhSUcwODVVR21JdkIrbmdFN0tMejhnT3FtTUVmVWVHczI0dHdnSHRrNFF1S2lSQmZ5STZ1RU84MlJGeGtuRDJlb3V6UmxVdFEweEF1c2lsNVhzS3p1SXQ4aVBzaUF3SHpLSE96cm5sZFZCVWc5UUZtbkdYK3ZLWjZ6bW5DN01MVlh3V093dDB5VWtGMWNmZEh0a3RYVUk4NXp6SXBrbmlRVzVNTU9FSnVwTUl5UCtVVWltWUIxWE5RN3RZenRZbHpHRjFvV1ZZMXlrdno1cWNvMG81Q0V4djVnMjd3dkVQOWR5N0dFcGxIZlJrNGJ4VThGUURhWVp0QWZUa2dkVkFiK3BnaTBwdzNib0c4R1l3bnE0Qi9LdGFBWHUxQXZacUJlelZDdGlyRmJCWEsyQ3ZWc0JlcllDOVdnRjd0UUwyYWdYczFRcllxMnNBTDNxMzBxdHJBRDNkRFBwYVFPOHRWUW15OXZwdVVTMkF0ZGRudGRkcE5aQm0yQnFnM3R3QzJwZmgrdjVaNWJtM0NLV3lyNEE5Tnl6VUY2YUFoTk8rVkFtd0dWWUJCK1F2ZUU2ZEViZTN1Tjk3UXQ3MzVZdDFoYVZDdXJhQkQxZUYzQUhBTUF5RDdKVjR3Nm1iME56elpmdUJES0R1QW5PZ0RjcXdiaTZYM3ZKN2NGcDdjSkRZSTk2QVcyRjBXZDJzd1kxcUthb29PYWUxQis4cFhoRjNtYmFwdjI0bUtpVGdPK25sNDh4Rno4RVNJSjE3QS9BTGNYK08zLy9FZkFQN0JOOUpMdytMcWcyeEF0SzlOMFF3RmxDRTFPOEQ4dzN0UXpwUE43M0pvNjJybGlHbWUrL0lOVE5hUEhHQ1h4THdscjVqaDl3dVE5ck9OQUxLVFBZQXRZS0RoUk1Ec3J0MDFoWlZ2S1gyQ0gvSVp6blhXdlZ4UnZ5cmRJZzVBNEg0dzRRL29GNlc4b284M0FySkNWUlZhWklBMlVHYjBIU1g0TzlZTHV5aGs1cVQraFFhSGFzdU04NkNyWkQybk03c2x0SW81aU9mUURNNFR6TUhFeVFQZFJocXVkbGFYSGJFM01VcVpHMi9XSWVheDNiNW9ZdUUwMW9GKzhkb2VJQXVwQXRvWmpRdy9TSG1EaUczSmpSZnRhOCt6MTMzdktLS2FwSHREVXRFOVhoQVpITkJtcXFBRjBEdFp3VUVNcVRYamc1MkFWSUZVQXVsTFg5OE1LSG5xbVhLRndGU1RwMk4xNnFHUXZ0bkM3MnRET2g0MmprSG1GbTZCRVoxQVZvVmdBRzBBMW45VWNCYnFPWC94ZjlVdndFRXhuUTEwSmJTZmdBQUFBQkpSVTVFcmtKZ2dnPT0iIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpzb2Z0LWxpZ2h0Ii8+DQogICAgICA8cGF0aCBkPSJNMTUuNSw3NGwtMS42MzEsMS40YTQsNCwwLDAsMS00LjE0MS42NTVMMi40NjIsNzMuMDI2QTQsNCwwLDAsMSwwLDY5LjMzM1YyNi42NjdhNCw0LDAsMCwxLDIuNDYyLTMuNjkzbDcuMjY2LTMuMDI3YTQsNCwwLDAsMSw0LjE0MS42NTVMMTUuNSwyMkEyLjIxMywyLjIxMywwLDAsMCwxMiwyMy44VjcyLjJBMi4yMTQsMi4yMTQsMCwwLDAsMTUuNSw3NFoiIGZpbGw9IiM1MjIxOGEiLz4NCiAgICAgIDxpbWFnZSB3aWR0aD0iMTIwIiBoZWlnaHQ9IjEwMSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEyIC0xMikiIG9wYWNpdHk9IjAuMjUiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBSGdBQUFCbENBWUFBQUNHTENlWEFBQUFDWEJJV1hNQUFBc1NBQUFMRWdIUzNYNzhBQUFMVDBsRVFWUjRYdTJiNjFiYk9oU0V4N1JBNmUyYzl2MGZzcWZjQzhYblJ6VFJlSHZMbGhJNUpJcG5MUzNuWXFETng3Nk5STmYzUFZhMXE0dTVHMWFkdGo3TzNiQnFySzdydXJsN3FQNmRVK1FLT0ZNTzFDekkrbVh2QVhzRm5DR0JxMWY3bWxVdjF4NTRIOWdyNEFrWnNGTUxjclZndmJXRnZUVG9GWEJDRHR3THVlcXlvQlhrVzFpOWM5MTg4NFZCcjRBZEJiaTZDUE9EV1JZME1BVDdCdUN2dWVwYUhQUUsyTWlCUzVBZnpickVFTFlGL0Rlc1YxbC9aUjBFOUFwWVpPQnExRjVpODFsZHlicVV4WHVCRFN4Q2ZNRUc3SXRaQkw4NDZCVndrQU9Ya1VtSTEyRjlraXRoTThvN3hPaDlCZkJuWXRuSVhnVDBDaGl6Y0JYc1p3QTNjaVZrUmpFQk0yci9BSGdPNnlrc1B2K0RHTkdMZ1Q1N3dBbTRyTEZYMkVDOEFmQWxySy9oK2ptc2E4UjZiQUVyMkVkWkNudFIwRjNtZlUwcUF5NmpsV0MvQWZnZUhuOUZqR1RXYUdBRGcrbjVHUkhxZzF6NWVCZlEyMWs2Qi9MWlJ2QU1YRTNKQ3ZhZnNBajVDemFBV1ljN1RBTytEK3NCMDZBSjIrdTgyYVdqNjdwWnlHY0plS1piWmxvbVhJTDlOeXhDL29ZWXdWZllmSmJhWkRGRnB3QmIyTHlQYWQyQ3ZrQUVEV1E2WW1jSE9PRlFlV21aY0FuMkI0YVFHY0ZzdEFpWVl4SUJLMlFMOTg2OFJ0Qlg4QnN5L2JjemJRTlNvNjNPQ3JBRDEydW9QTGcvc1FIOEE4TVUvVGw4RFp1c0MwVEF0dEY2RFBjL1lQT0xjUitlRS9RTkl2aHJSTmpXTmV2Qzl3Wk04K1ZGOFZrQkRySnBtUVlHNFg3Qkp2MHlMUlB1VDhUby9ZWllmNi9EOTlBYWJHZmhtN0NlRUNHekM3K1Q5emxqYy9UaTkxWFBtMkt6dGUyeXU2N3JMT1N6QVN4MTE0TjdqVGdLS1Z4R0xlSCtpMDMwY2t4aTlCSUVwVzdXRlRhUnpNYU42OFk4bndLcjZwM0ZlODR6Z2hNZE05UHlIRnl0dlpxYTJWeHBlcVlZV1c4WWxnQmQxL0tZVUQyd0dxbk1ETW5SeVVaeDg0QXp4aUdGcXgyendyV2pFU09PWU5TTEJvWlErUE5lRVNFcVRBOG9RZXBtaFoyUHVmVC94Ni9mcW1uQUdSWmthaHl5SGJNMzk3SnoxdVpIMVlmWDMrUWVyYVVLUkpzeTdiNmZNTnpZNEMrR2ZpLytETmIvZ1pvRlBCRzVUSStmRUIycUhMaXN1UXAzcWdIcTVKcHFqbDR4M0pYU3FMYkxRdVVWR1A5eWJhVnBwUmxsV0pEYU1YL0gyTXpRYmxsckx1dm1GRnpJYTUzejNJdDA3WWJ0ZFZ0Zmc3eWZOWHdoam9QdEFaNXhxVmh6VTJuWnE3bTJvVXJCVFVXcVB0WkdTVk15WGF0bkRNME5hMW1PTmgvbTFCVGdESmRxcXVhbUdpb2J1YmFXUXE3MmcvY2FKdDFHZkVLMEt1L05VcDlhM2F4WCtGMzBWazEyMFRNdWxUWlVUTXVha3FjaVY3dmxWT1FDNDJoVnVCWXNiY3M3QUxjQWZvZjFuenkrRGU4VE5DR3prN1p3M2FodUJuQ1FUY3ZhVk5tYW00cGM2MUpadU5veHArQjZxZmdQb21XcGNQK1Q5UXNSOEIxOHdFVXB1d25BR1M2VldwQjJBMEVCSzF3OXJiRUxYSjFmMVk4bVhFYXNCL2NXTVUzcnBvUHVGelB0OTJZTmRQS0FNMTJxejVoMnFiUmpWcmc2dHRpMHJCK21UY25jVGRJdFE5WlpDL2VYUEU3QnpUa0lBQUNqRFllVEJselJwZEp4U0NPWFRaWFhJZXRqL2JDOWV2dUFjYjBsV0Y1dFd0WTk0UlRZMUNpMTFja0NydVJTc2VZcVhEMWo1WFhNMUZ3enhaUk11TnBJS2R5cHRHelRjV3BHN29GeDlBSW5DbmhCbDJxdVk2WnltaW1ia3IybzFjaWRndXRGcTliK2RrNTBaTHBVSGx4ZEtaZHFGN2hlTTBXNFRNbGFieFV1UjZGN0RHZGVDemNKRmtqREJVNE1jRUhONVVHNWZXZGRGVDlFVzI5Zk1UWXV0Tjdtd3MwWmd3WWc1dzdjQVNjRU9NT0N0S2NnNStCNnMrNlVpZUhWVzhMbHFVaTZVRHJmS2xqVzNKU0o4WUp4dmRXb3pZS3FPZ25BaFJZazRYcWpVRTdrMHI3dE1JeWNxWG83MVV3cFlOdE1FUzdISUoxdHQ1RmJDbFYxOUlBTExFZ0xsNUZyeDZFNWw0by95NE9iWTE3WVpvcVBiekhkS2F2SHZJM2FmZUFDSndBNHlLYmxsRXVsYVZrUHlWa0wwbk9wdE9acXZlMnhtM21ScXJjNjR5WmRxWDNCVWtjTnVOQ0M5RXdNQld6aFRuWExXbTlMelFzRlc5Sk1WWWNMSERIZ0NoYmtEd3pocGc3S2VkRnJtNm5jZXF0WHJiZjNpSTJZZGFhcTFWdFBSd200WUJ5YXN5Qzk0emFFUzM5Wk5kZE1sWmdYWHIwbFhLOVRyZzRYT0VMQWxTeklPU1BEcHVWZUZqLzhHdWFGNXltUG5La2x3RkpIQlhoQkM5SnVIbmhwZVNuell0S1pXaEl1Y0VTQU15M0lmUS9LZVhDMVV6NkVlYkZZdmZWMEZJQXpYS3A5RDhwNURkVSt6WlFDUHJoNVVhSjNCMXpvVXVYQTlRN0syVmxYNFo2Y2VWR2lkd1ZjNEZMVk9pZ0hqRHZsa3pNdlN2U3VnSU5zV3ZhTWpGMFB5bG13UUFSTHVDZG5YcFRvM1FBWHVsUWxCK1Z5SXJlazN1cjFLTXlMRXIwTDRBb3VsZTJZUFgvWnpybk5tQmNsT2pqZ2lpNlZkMURPUm02cVV6NXA4NkpFQndWY3lhWEtPU2lua2F2T1ZCUG1SWWtPQm5oaGwwb2pGL0RIb0diTWl4SWRCSENtUzdYdlFUbU4zRjJhS1FWODFPWkZpUllIWEZCemM4OVNwVndxSUVhdVYyK2JNUzlLdENqZ0RBdHkxNE55SGx6QWI2YWFNeTlLdEJqZ1Fnc3k5NkJjNnFpTjdaU2JOaTlLdEFqZ0FndHk3cURjbkVzRnBKdXBaczJMRWkwQ09NaW01WlJMcFduNUo2WXRTSGJNMXNBZ0NJNHdUWnNYSmFvT3VOQ0M5RXdNMnpFcjNGUmFwdWx3RnVaRmlhb0NybUJCL2tDNjdtcERwYnRCbm5seGgyRjluWUo3Y3VaRmlhb0JMaGlINWl4SVBRVnA5M1FCZnd5eXpaUUg5aGNhTVM5S1ZBVndKUXR5eWwvV2hrcmgydmsyQlZjajkrVE5peEx0RGZnQUZxUzZVem9HZWVaRkNtNnFtVHBKODZKRWV3SE90Q0JMRDhyWmhnb1l3cldkY3FxWllrcitqY2JNaXhMdEREakRwY285S09kMXk1cVdDZGZ1NGRxZElLMjFGdTVEV0UyWUZ5WGFDWENoU3pYWFVFMmxaVWFYWnp1V05sUE5tQmNsS2daYzRGS1ZISlJMd1UxMXlydnNCRFZqWHBTb0NMQ0JPMmRrekIyVXN6dERIbHoxbFBmWkNXckt2Q2hSTnVBRTNJOUl1MVE1QitYVXhPZ3dISU1JdDJRbjZCYXgzdHBtaXAxeXMvWFdVeGJnek1oVkl5TmxRZEpmOWh3cTFrUENmUXhyMTUyZ1p3enJiVFBtUllrbUFUdjFWc2NoYTBHbVpsMEM5allQMkMwVExqdGxiNXZQZHNxMjNqYTNFMVJEU2NBSnVIU292TWlkTXpJMExWOWpHTGs2NDNyTmxBWHJOVlBON1FUVmtBczRNUVpOcGVXY1U1QWF1WVRMdEttZGNtNjliWDRucUlhbVVuVEt4UERPVXFVT3lsbTQybENwZ2FGd1M3YjVtdDRKcXFFUjRFVDBsc3k2VE1rSzE4NjVUTXUyVTdiMVZsUHoyZTBFMVZBcWdsUFI2NDFEdXYzSGxkclBCWWI3dU53dzhKb3BCWnd5TDVyZUNhcWhBZUNacnBtMTl4UEdQck11UmkzaFhtTG9LK3RXbjdlSGE2TTNaVjQwdnhOVVExNEVkL0hoeU5Ed1VqVHJNSSsxV25lS2NCbGxkc1BBc3gzbm1xbXoyQW1xb2JrVWJWMHJiYkkrSVVZei83clBPNEh4SnN2YkRkS28xU2oybXFtejJnbXFvWnd1Mmh1VEZQYWx2TVlPR1lqcDJJNUNVMm5aMXR2VnZOaFRPVlpscWk3cmN5Qis0Qng5T214Z3NHT2VHNFdZcWxmem9xSnlBUGR5NVNKSTNkTGp5UE1oM0VQSU5uclpOQkV1UWEvbXhRSktBZTR4Qmtxb0JLb0doYzY0YktRK21PZFBpTkZMd0wvbCtXcGVMQ0FQY0k5b1JpaGNqZFFuYktEcTN3ajE0UjYrcDkzek15SmcxbDlkM3ViOGFsNVUwQUJ3My9kOW1JVTl1Qit3K2VEWlRPa2VManZrUjhTL0g5S05CTjBoSW1SR3JEZmZydVpGSlUzVllGdHZYeEM3YWUyVStkNFR4b2ZtR05WOG41QUptbnUrT3Q4UzdtcGVWTkJVRFFZMkh6RHI2RlRIL0FUL1R6c1p3YXpYSEhrZTViR1hrbGZ6b3BKR2dDVk5BeEhrNEJaRWNQWm9qVWF2cG0vZXgzdWY1YmxOeVdzelZWRnVCQWZJZkxxdGZ4alhaUVdzWm9jQ1pwclc3cHRMall1MTNpNmd1VGxZUDJCTm16b0h2MkRZZUtrSkFveEhMRjd0Ykx2VzJ3WFV6WDJPaWYxaGExOWVtTFVOZi9pL0ZHL3d3YTRwdWJKbUFRTWp5THhxcEY2WXE5N2JZd2o1VFI3cmUydlVMcUFzd0ZRQ3RGMzZQakNzMzNadDMxL2hMcU1pd0pTQUJzWlE5VDFWNzExWHNNdHFKOENVQWIxOTJYa05HRFpzSzlnRGFTL0FWZ25nVzYxUUQ2K3FnRmNkbnk2d3FtbjlEL1FHdkxnc0FUTDVBQUFBQUVsRlRrU3VRbUNDIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6c29mdC1saWdodCIvPg0KICAgICAgPHBhdGggZD0iTTk2LDE1Ljc3VjE2YTMuNzg2LDMuNzg2LDAsMCwwLTYuMTg3LTIuOTI4TDE1LjUsNzRsLTEuNjMxLDEuNGE0LDQsMCwwLDEtNC4xNDEuNjU1TDIuNDYyLDczLjAyNkE0LDQsMCwwLDEsMCw2OS4zMzNWNjlhMi4zMSwyLjMxLDAsMCwwLDQuMDI0LDEuNTQ5TDY2LDJBNS45NzksNS45NzksMCwwLDEsNzIuODIzLjg0MUw5Mi42LDEwLjM2NEE2LDYsMCwwLDEsOTYsMTUuNzdaIiBmaWxsPSIjNmMzM2FmIi8+DQogICAgICA8aW1hZ2Ugd2lkdGg9IjEyMCIgaGVpZ2h0PSIxMDEiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0xMiA3KSIgb3BhY2l0eT0iMC4yNSIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFIZ0FBQUJsQ0FZQUFBQ0dMQ2VYQUFBQUNYQklXWE1BQUFzU0FBQUxFZ0hTM1g3OEFBQUw2MGxFUVZSNFh1MmQ2M2FiU0JDRWF5UWx6bVYzMy84OWQ1UFljV3oyQjVRb1N0MHpZRXV5THZRNWZXYTREUW9mMWRNMFBxRjBYWWZWYnRjMnJSMVd1MjdidFhaWVlxV1VVdHZlcmVIaTdGYmVjODBUb0Jua3lZbFcyT2V4TnluWXdKYWtkZXUwNVJBcjZOUGFJc0FDVmx0MzNRNU13YXIzTzY2Z1QycXpRblFDZG9NUjZzWmEzVmVodmc3ZVNic0h2a0krdmxVVkhJUmloN2tCc0pYK0JsUEl3QlR1eStDdjVoMkFycFN5UWo2eXBZQk50UXBYb2U2R1BsdXUxM0JOaUM4QS9rakxmcEY5MXBCOVpBc0JCM0JWbllUNWFlaC9rajYzRVRMVlM2RFA1Z29ha05CZFNpa3I1UGRiTFVSSHFsV2duOFVmWk4wT1U4Q3Y2Q0grSHZ4cGNDNXYwTU11bUlKZVEvWVI3QUN3cVZmaEt0aUh3YjhBK0RxMEQ4TzJUeGhWM0tHSDlvd2U2dVBndjRaVzFhNlFDUm9yNVBkWnBtQ2ZkeGwrUDZPSCtRWEFOL092R0NGVHhRelB6eGpCL2h6Mis0RnBTS2VTYWV1OGZBU2JBRTdtWGlaUFZDL2gvaVgrSFZQSW56QlY4RytNY0gvSVBxcDJUYzZlc2M3TFI3Rkl3UkZrenIwTXoxL1JRLzBMd0Q4QS9oNzYzOURmQUorSDQ0QWVGTVB6RDB5VlRzQ2FnVWNoRzFnaHY4bGFJVnJuWUZleFF2NGJQZWp2R0FGdU1RTDZqUjR3YndBcWVDZnV6OU9FckxZbVh3dXRWYW9rNUlJcFpGVXpRelpoZjhjSXNLQlhIcE1zd3RjUTdTcld4RXZEdGhaSTF1UnJwdTBCSjRXTi9XWk1MM2owNktUQXZ3N0xXNHp6OEJQRzBNendySUFqRlN0Z0Q5ZHI4alhEV2dwVzZ4SlgwMGVxQjR6WjlDdEc1U3Jjblhta1pJZE1XNU92R1JZQmppQnF1VkZMalY2VmVzRUlYUit2Z0JHc1ExVzREbG9CYjlEUDVhN21kVjZ1MkI1dzEzV2R2VndBRHVFK1k2eEdhZEdDWVprVktpMC9VdFZBRzJ5a1lwK1hQV1N2UlpHS1pTSGExVXUxN3RCRC9HVCsyVnkzT2JRTkR1SHRyQjhkazgzTHdGb1VTUzBMMGN4K3FaSU54bnF5WG1nSE5HZE8vWXdwTkFlZEhWY0R2QlpGRXBzQWxqRE5DMFBJZnhCbjFacGRSOHJNQVBsTlVqdDJqcHJYb2toaXRSRE45blhvKy94TXE0R09ZTENNdVV1T3lSUmRBMDNJYW12eWhRQ3dKVnNLK0EvaVJ5TmdDa3BWNlJDSzlMZkJjVFhZbVhNOFBjZGFGQmtzVlBBQWViOG9iUmNzQTdHS004RHFEaWdDWEp2ZmErZFlpeUxJUTdSRDVsdzgyV1ZvaTdRWllMOEJIUEpPMW1kcXprSzFoMndkbTNhM3lWY0tHRWlUcnJsaDJpKzRBdVQrYkFtcHBtUlZzZmNkOEFiOTQxeWs1cnVDWEFVTWpDRnRVRE1mb1lCK1RnYnE2blJWTzJodEZiS3FtNkJyY0JVeTI0SnA1WXUvOTY2U3J5WmdtcW1aaWRlK2lvUVlxc1BONE90K08rblhsTndDcmVlbDNWM3lOUnN3Y0FDNXd3ZzRnNmNBRmFJRG9QTVZveXA1RHVnTWJuUmozVlh5dFFnd01JSDhpdjRDcW9wcEdlZ01zSUp3eU5FeEdkeldzektkZHZQSjEyTEFRSnA4UlhPeUxpdk1HdUFJc283aDhKWUEzdURPM2tpOUNUQVFQa1lCNHhzbnRSYmdDSGpCOUZrM0c2T2w0QWgyTWIvcE4xSnZCZ3lra0dtdXZBaEtCamM2MXRmVlFFZkFhK2NDYnZTTjFMc0FBMDNJUUE2NmRjSFpNbHhIa04wejBITUEzK1FicVhjREJsTElCZU5ibmhaY1ZhUURMaGlWeUg0RXVnYVhnR3V3Yi9LTjFGRUFBeUhrMmlOVXBxWk5jZ3lOa05uNnNSRnNMNUJrcWlaa3RhdFB2bzRHR0dpR2F3Y1JnZWZCM3VyeEJGTzdhVnl4TGZjSVVuQWpSWkdqQWdZbWtCVndCak9DMU5wSDk2T1NnU2trVjdKWHZsVFZXVFFoWk9DS2s2K2pBeGJ6a3VaY2FBb3BnZzZNaVpkQzluRWlCV2VoMmtPMm53KzQwdVRySklBSEZSY2NsalRWaXJRWjZHaVpyb25YQm5ueUZhblpGUjBCM3VBR2lpSW5BUXpNcm5acFB3S2RlWFFEUkVwMndEVzQyYnlzZm5WRmtaTUJCbVpYdTk0S1ZtK1M3ZEJtKzZpQ28zQmR5NjU5VE9DS2lpSW5CUXcwTTJ1MXVZQ2pDKzdyZFR3Zk13TzlCUERWRkVWT0RoaG9RczRVcDBDeUM2NjJrMjBPSlJvdmd6dEh6VmRURkRrTFlLQUpHY2lCMU5Ua0R2UlFmRjAwOXRiNkhxNXJvQWxaN1NLVHI3TUJCbzVXMG94QXE3bUNIYmlQNHdwdU9jZldjMXhzVWVTc2dJR2psRFFqRmZ2eHNHUG1qTzBoZW1lZWhXdUZERnhZOG5WMndFQXpYRHVBQ0V3R2JUL28wTy9zT0NBK0psSnlGcW85WkVmbnZwams2ME1BQTR0TG1xcmVKZEJoMnhpdUVSeWZxVGw3blBLb2NwRkZrUThETExha3BMbXhmaFF1YXdvSFlpVkhnR3R3czNsWi9TS0tJaDhLZUZCeFFidWtHWUd1dVVPRnJkYytnckZyNFhyT1k1U08vYUZGa1E4RkRNd3VhVG9JRFkydEM2em1ZRGZvcjBGMjgyU2dsd0QrMEtMSWh3TUdacGMwZ2ZtQS9VSTcxT3pHaVFDemplRE9VZk9IRmtVdUFqQXdPN091S1N4VGtoci92ZHpPUGkrMDN4QU9Xcy9wYzNUMld3aFo3V3pKMThVQUJwcVFnUngwQmpkU2NBUzVCTXZST1Z6QkxlZVkrcHZPV2hTNUtNREEwYXBkZmxIZE9POUM5aWxvS3psU3NhcFpWVjI3NmM1V0ZMazR3TUJScWwxY3oyUDBXRjIzbFQ3WDF5SUF0N3RTczFEdElWdkhvWjAwK2JwSXdFQXpYRHZjREFhQ1phN1Rkb040SFpjZGNLYm03SEhLSTh2WmlpSVhDeGhZWE8yS1FMVDJjYWphWnVzaXdEVzQyYnlzZnJLaXlFVURGbHRTN1hLNDJUeEljTEJqdU53TjYzUmVacXRqMThMMW5NY28vUzFITDRwY1BPQkJ4YnpndFdvWDJ3eDBheHVOVUl1MEROZE16blFNRDlVT2VnbmdveGRGTGg0d01MdmFwZjBJWnVRUllHQUtXZGZWeHQxS0c4R2RvK2IwNnpPMHBiQ3ZBakNRSmwzSC9BTStQZDRWck50cDJibTIxdmR3M1FLdGtQY2hlekRlNlAzQ0ROaFhBeGhvWnRacWN3RXJhRWdMMlVaalA3bzVvbk81Z3VlNGo4MThvek1IQkhZTjlGVUJCcHFRV3hlK05nOUNXdG9tV0srZ2dmcTg3Q3BXTmM5Snd2amZNbFBKRGhsb3pOTlhCeGhvUWdicUZ6eUNxKzdXZ2h5Tm9lZDJsWHFvemg2cm5qRDlqOVlKdXNNVU5sQ0JmSldBZ1JSeXdiS1NaZ1NhcHNzWjVJS3BtaUxBbVpvakZUdG9mdjVQdi9INE9vd2R6czhPK1dvQkF5SGtwU1hOU0lHdy9nN1Q5V3oxK1ZqSDBlTWp3RkhDNWJEWjUrZi85TE1KK3RWV3dDREQ3S29CQTgxdzdSYzZnaDdkREJIMExRNnphMWpmbGU3bjhqQWNoZTNJbjNENGIzakJDSlFoRzY3aXF3Y01UQ0RQS1dtcWV1ZEExL0VnMi9VUktvSk01ZXU0V2NpdVFkL2FNZnFiT0EvcmI3MmRFQjBZRXhDZ1hkTGNXTjh2WWczMkhNaTE4L3E1dkIrMVBJY21XYS9EOXR0NVRNck1xbDBkVHZzSGZFWGFDREw3YXRFNGtQMDZjMTNIaE9yRlhNZUx6bms3Z0lFRHlFQmMwaXkyN0NySzRFYkhjVGtEcS9zQmRXRDZMYXJzaStucS9odjNwdlB3VFFFR2tDVmR4L29EUG1CNk1SMnlHdmRUK0J5WGM2eC9sdWdCMHkvRTZjZkVkQzZlQUszWnpRRUdVc2cwVmFUQzFBUW5VM0prQ3BuV0JhMkhYMXAwby9tTkZZMmRMVTlLbHpjSkdHaENCbkxRR1Z4M1dtZjdjVjBVamx1aFY2dFc3bHF1MUpibkMrMW1BUU1wNUlKbDFTNVhreHVmajZsa29BNVhQdzM0SzNGK05wQ1ZMQzkwUkxCRHlEY05HQWdoTDYxMktWaFhib2ZwL0Zod0NQY1BZcmlQR0Q5NS94K0FmOFgvRzliL3dBaWMzNFZVcFN2Y0VQTE5Bd2FhNGRyaFppR1o3aGVVU1pBRFptaWwraUxsRXFJRGR0QS9NU282ZXdFQkFBZXZEdThDTURDQlBLZmFGZmwrS0l3S2ZjSDRuZVN0N0tQS1paalZyN1greEZTOWtSTSs5NldLUFZTbjZnWHVDTENZSmlldGFwZkQxZU01ci83RytCVnpLcGpibm5Hb1dvZXJyVVBsTVU4WTRSS3d6c01BRHRVTDNCbmdCZFV1dHBGNkNaZGg5d3ZHTU0xOXVKMWdOQ1FUcm9MOUthMG1XanhlUTdQRDdkRC8wMVlGQXdlUWdUR2NBbE9ndGJCTTVUNmkvekEyUDVsYlpEd0NWdVdxUW10cTlRemFINSthb1psMmQ0QUJaRW1YVjd0cW9abnd2cUZYY0FTWVNWVUdXTUZ5ZnFaYVcyRDN6NytaY21sM0NSaElJV2ZHaTZyd0h0RURabG1SU1ZZTnNNN0JFVmgvM3RVQ3g0RnFXM0NCT3dZTXpJYXNGMVhWK3d0OWVIN0ErRTNrU01HYVBYc2hZeWxZWUFGYzRNNEJBNHNnUjNNcnd6T2ZnNEV4akRQVU1sRlNxQ2NIUzd0N3dNQXN5Tkg4K3d2VHR6MGNvTVBoTTNEa1d1RTZPbGphQ25pd0NtVFBvUGw0OUlocEZjc1Z6SDJwZW5WbTRpY0RTMXNCaXlXUUhiQVhON1FPemYwVnNpdFZNK0tUZ2FXdGdHUGp4WTBnLzBFUDlUZW1jQjB3L2NYYXM0Q2xsU09QZHhOV1JobDd5YkwyS3BIV1lYcER2RXBmMjVPQ3BhMkFLMVlCSFRsTndXVyszKzlVWUdrcjRJWUpaQ0F1WmVwMk5RZXQ2MDRPbHJZQ25ta0I2TGwyZHFocUsrQTNtTUd1MmtkQVZWc0IzN2pwSDRxdGRvUDJQNHFBMHNRajA0YUVBQUFBQUVsRlRrU3VRbUNDIiBzdHlsZT0ibWl4LWJsZW5kLW1vZGU6c29mdC1saWdodCIvPg0KICAgICAgPHBhdGggZD0iTTk2LDgwdi4yM2E2LDYsMCwwLDEtMy40LDUuNDA2bC0xOS43OCw5LjUyM0E1Ljk3OSw1Ljk3OSwwLDAsMSw2Niw5NEw0LjAyNCwyNS40NTFBMi4zMSwyLjMxLDAsMCwwLDAsMjd2LS4zMzNhNCw0LDAsMCwxLDIuNDYyLTMuNjkzbDcuMjY2LTMuMDI3YTQsNCwwLDAsMSw0LjE0MS42NTVMMTUuNSwyMiw4OS44MTMsODIuOTI4QTMuNzg2LDMuNzg2LDAsMCwwLDk2LDgwWiIgZmlsbD0iIzg1NGNjNyIvPg0KICAgICAgPGltYWdlIHdpZHRoPSI1NCIgaGVpZ2h0PSIxMjAiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDU0IC0xMikiIG9wYWNpdHk9IjAuMjUiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2UvcG5nO2Jhc2U2NCxpVkJPUncwS0dnb0FBQUFOU1VoRVVnQUFBRFlBQUFCNENBWUFBQUM1RndIQkFBQUFDWEJJV1hNQUFBc1NBQUFMRWdIUzNYNzhBQUFHTTBsRVFWUjRYdTJjVzNQaktoQ0VXODUxdmYvL2Y1N2R6VDJ4T1EraVRUTWFoSlNLSEpPaXE2YWt5TEt0enowTTVJRVpRZ2o0aWRyVmJtaFYxN1VicklaaEdHcjNBRUQ0NWxSWUJPYkFWT0gwTGQ4Qk9kUytVNkJLUjAraGNEd2I1S3hqQnFvVVZnRTVrTWJKeWEwQmkyQU8xRTZPTzhBRjVNTWU0N2s5bm1DMkJuVEJDbENNSzNOVU9ENzhzUktiQTg2bG9vVzZrcmlPY1NXdlV3UTdGT0lzZ0JNd1V3RVZUb0Z1SkFqTGUvblFCd0FmTWQ3ajhTREhUUUdYT0thdVhRTzRkZUlhS1MzcEdJRVliMGlBak0wQVMyRFdOVTFGd3QzSHVFTU9wNmxJb05jWWIwaUFDdm5sZ0xVSjJycEdzQnVNUUw5aUtCeGRvMk9FZWpISFRRRnJxV2ovSHBDN1JyZzlSdmR1a1FySkVRbnNKY1p6RFA2OUZqQU13ekFzZ1pzRDg5NnNjRmRJS2JsSGdydkJDTWR4OW9ZYzZpbkdVa0JDbmliNkplN1ZVbEhmbUUyd1NLbEp1TjhZNGU2UVB2ZUlOTVlJOVlnRVZ3SzBoY1k2Q0ZUY0s0RUZwRlJrTVFnb3c5MGdkKzQyWHVkWVl6b3EyQ055U0FWOHdiVFFjS29BRnNCTndFSUlJYzVsd2NUUkJEOXNRQ29vZEk0cHlYbE5YYk5nRnZBSjR3K2pMdTR3d2dFaklDQndjTFFrRlMyVVhVRUFhZHhwV3Q0aHVYWkFjdTAzcG1BUGNuNHZyOThnWCtFTVNJQkEvUDVoR0NianJWWTgxRGtQU3AzakJNNnBZSS9jTlp1U2U0eVFqL0djZ0wvaThSWnBaVU1vWUpwSkFRQnNTcnBnaFhSYzRwcXVUdTZRWEFQU2hQMktORVVRY0k4RVpTZDlnZ0UrRkdFV093YlV4NWtXRlNDSG8zTjBMV0IwalNzV1R1NTdPVmN3ZFFzb1B3TlRQVk1SYkVVUnNYQzZycnhCcXBCQXFtenE2TDBFblNJVTA0OVo4bUdDV1RNQWVZV3NPVWFWVXNDbUEyV1hZTHJVT3NCZlNETVVpaitrTHFSMUd2RCtId1N3SE14cURrNi95UDV6Q296ZmVVQUMxbitCdUpEVzFOVmxHZWM2V3lrbldnTm1BYXhMcWhJY0gvZ0tJeHdkNVJKTng1TXV4NTZRRnR2ZUZEQ0JXd09tVWpndkZWVVdFaGp2MzVuUThjU3A0UmxwRGFyanp3UDdrbFJjSXYwbEIwd2ZJQlN1YWVVa1VBMXFvaTNCckVwd0ZNY2V4OXN0cHVPdjVOUUpqcFh4SEdEdUx5clhkMGpqVHF1b0YxNzY2dWVmMHBFNXY3VXNYQ2xGN1lOclVTbkJVTm0xYzRGNXNyLzBtb0E1bitnN3dlWlVBeXNDVVpjR1poMzV0QzRKektZbWo0c2Nzcm9rc0M5VkIydE5IYXcxZGJEVzFNRmFVd2RyVFIyc05YV3cxdFRCV2xNSGEwMGRyRFYxc05iVXdWcFRCMnROSGF3MWRiRFcxTUZhVXdkclRSMnNOWFd3MXRUQldsTUhhMDBkckRWMXNOYlV3VnBUQjJ0Tmx3U211eWJXN0oxeGRVbGd3UExkVGxWZEdoZ1ZGc1NzdmhQTXB0NmFnRG1mNkZ4ZzlnRzhNUlF3M2Z0WjJwN3NBV1hYemdGVytsVXREQ0hzQmxPTnVVMnYvRXdBNTkwLzVxVlJ3QlRJZG13cHRjVHcwaE5yZDlWK1JpV1FFcFIyYXRGbUN0cTlSWHZ2ektYbHA4SFc3RDJ4WTJnT1N2dDlhTmllSHlYblRsb0Q1dTNmS3NuQ0hNMXJoR0thYVErQ2Z5YllPZUk1M3JmSXRUVmdxcms5WFI2VVBvUkMwU251Y1g3RUNQTW54dC80OXdQU0h1ZzNUTUVtV2dwVzI2Um0zU01VVTQxZnpyOUxVQThZUWY0QytDL0dYNlMyR0FSajQ1TDFxZWkwU1dQc25GQTR1a1FBZmthUWF5d0k3S09qZlQwSTlrZmlIL0t4NXFaaXRlMkZhQzBVblNMQUxwNERLZjNVSlJZSFFoR01vYW40R045akhkUHhlNUlMVm5CTFlleHVWeUNIZXNQNDJjZjRYbDdYSW1HaEZFN0RGZzl2akswcUhoYk9BNkpqUU83VWE3em10WmJSYmtkZWF4bnRxYU45ckVwUUFMQ3F0UXl3enEwRFV0TUR4SE82dGJZWjBGelBLbDAvQm94TWRjZFdGQTBkVjNUcUpiNzJqdVNXVFVFTDVzSE10VzhxclJNemxSd2IwdWtKU2lFcGxuU0ZPaUI5cm5WTHdlYmNxVFhjY2l1aGFra3E2cmtIeGZRakZNKzFrSEJzZlVtTE5GU2dnR1hGUThVUDVSY3FGTjNodURzaUwrOEt0d1RJYzZnS1JNMkIyVGNybEpaMGhib0dpcFB4aXptdUFRSldRQUgxVk5SZmlxbW5EOHlxOTQ0RXFmZnEwc21PbjAyQXFCSll3SFExd2ZTalcwQSs4YkxBQkxuK0xtRUx3aVpBVkMwVjFTMCtMT1FheHhrYi9kQkJkWmVBSDNKdE15QnFBaWFOdG9CcEd0cHJIOGduYXI2dUR0dllGSWlxT1FhTUQ2RFg5TUh0L0dZZExzVm1RRlN4eWJpekF0bkowUUtwdzBCNmVIdmNISWlhN1o0K3M3enlWaUdVZmZpekFsRS90aTE4RlF6STRFNlgzQnR6blIxR3RRaE01VUM2K2c0WTFXcXdWcVR6ejQvUy8xSEw3YllXaGVSYUFBQUFBRWxGVGtTdVFtQ0MiIHN0eWxlPSJtaXgtYmxlbmQtbW9kZTpzb2Z0LWxpZ2h0Ii8+DQogICAgICA8cGF0aCBkPSJNNjYsOTRjLjEzOC4xMzguMjkyLjI1NC40NDEuMzc3QTMuNzUxLDMuNzUxLDAsMCwxLDY2LDk0Wm0uNDQxLTkyLjM3N0EzLjc1MSwzLjc1MSwwLDAsMCw2NiwyQzY2LjEzOSwxLjg2MSw2Ni4yOTIsMS43NDYsNjYuNDQxLDEuNjIzWk05Mi42LDEwLjM0OSw3Mi44MjMuODM5YTUuOTY4LDUuOTY4LDAsMCwwLTYuMzgyLjc4NEEzLjUxNywzLjUxNywwLDAsMSw3Miw0LjQ4NXY4Ny4wM2EzLjUxNywzLjUxNywwLDAsMS01LjU1OSwyLjg2Miw1Ljk2OCw1Ljk2OCwwLDAsMCw2LjM4Mi43ODRMOTIuNiw4NS42NTFBNiw2LDAsMCwwLDk2LDgwLjI0NFYxNS43NTdBNiw2LDAsMCwwLDkyLjYsMTAuMzQ5WiIgZmlsbD0iI2IxNzlmMSIvPg0KICAgIDwvZz4NCiAgPC9nPg0KPC9zdmc+"},"isHidden":true,"releaseNotes":"https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.9#16.9.4","localizedResources":[{"language":"en-us","title":"Visual Studio Team Explorer 2019","description":"Interact with Team Foundation Server and Visual Studio Team Services without a Visual Studio developer toolset","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-cn","title":"Visual Studio 团队资源管理器 2019","description":"无需 Visual Studio 开发人员工具集,即可与 Team Foundation Server 和 Visual Studio Team Services 进行交互","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-tw","title":"Visual Studio Team Explorer 2019","description":"與 Team Foundation Server 和 Visual Studio Team Services 互動而不使用 Visual Studio 開發人員工具","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"cs-cz","title":"Visual Studio Team Explorer 2019","description":"Interakce s Team Foundation Serverem a službou Visual Studio Team Services bez sady vývojářských nástrojů, které nabízí Visual Studio","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"de-de","title":"Visual Studio Team Explorer 2019","description":"Interagieren Sie mit Team Foundation Server und Visual Studio Team Services ohne eine Visual Studio-Entwicklertoolset.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"es-es","title":"Visual Studio Team Explorer 2019","description":"Interactúe con Team Foundation Server y Visual Studio Team Services sin un conjunto de herramientas de desarrollador de Visual Studio","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"fr-fr","title":"Visual Studio Team Explorer 2019","description":"Interagissez avec Team Foundation Server et Visual Studio Team Services sans l'ensemble d'outils de développement Visual Studio","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"it-it","title":"Visual Studio Team Explorer 2019","description":"Consente di interagire con Team Foundation Server e Visual Studio Team Services senza usare un set di strumenti di sviluppo di Visual Studio","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ja-jp","title":"Visual Studio Team Explorer 2019","description":"Visual Studio 開発者ツールセットを使用せずに Team Foundation Server および Visual Studio Team Services と対話します","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ko-kr","title":"Visual Studio Team Explorer 2019","description":"Visual Studio 개발자 도구 집합 없이 Team Foundation Server 및 Visual Studio Team Services 조작","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pl-pl","title":"Visual Studio Team Explorer 2019","description":"Interakcja z serwerem Team Foundation Server i usługami Visual Studio Team Services bez korzystania z zestawu narzędzi dewelopera programu Visual Studio","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pt-br","title":"Team Explorer para Visual Studio 2019","description":"Interaja com o Team Foundation Server e o Visual Studio Team Services sem um conjunto de ferramentas de desenvolvedor do Visual Studio","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ru-ru","title":"Visual Studio Team Explorer 2019","description":"Взаимодействие с Team Foundation Server и Visual Studio Team Services без набора инструментов разработчика Visual Studio","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"tr-tr","title":"Visual Studio Takım Gezgini 2019","description":"Visual Studio geliştirici araç seti olmadan Team Foundation Server ve Visual Studio Team Services ile etkileşim kurun","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"}],"requirements":{"supportedOS":"6.1.1","conditions":{"expression":"not Win10ThresholdBuildNumber","conditions":[{"registryKey":"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion","id":"Win10ThresholdBuildNumber","registryValue":"CurrentBuildNumber","registryData":"[10240.0,14393.0)"}]}}},{"id":"Microsoft.VisualStudio.Product.TestAgent","version":"16.9.31205.134","type":"ChannelProduct","icon":{"mimeType":"image/svg+xml","base64":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+DQogIDxzdHlsZT4uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO2ZpbGw6I2Y2ZjZmNn0uYnJhbmQtdnNpZGV7ZmlsbDojODY1ZmM1fTwvc3R5bGU+DQogIDxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTQwIDQwSDBWMGg0MHY0MHoiIGlkPSJjYW52YXMiLz4NCiAgPHBhdGggY2xhc3M9ImJyYW5kLXZzaWRlIiBkPSJNMzAuMjIxLS4wMDJMMTMuODg3IDE2LjE2IDQuMDUyIDguNzQ2IDAgMTAuMTAyVjI5LjlsNC4wNTIgMS4zNTYgOS44MzUtNy40MTQgMTYuMzM0IDE2LjE2TDQwIDM1Ljg0MlY0LjE1OGwtOS43NzktNC4xNnpNNC4wNTIgMjUuODlWMTQuMTExTDEwLjAwNCAyMGwtNS45NTIgNS44OXpNMzAgMjguNDcyTDE4Ljk4MyAyMCAzMCAxMS41Mjh2MTYuOTQ0eiIvPg0KPC9zdmc+"},"isHidden":true,"releaseNotes":"https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.9#16.9.4","localizedResources":[{"language":"en-us","title":"Visual Studio Test Agent 2019","description":"Supports running automated tests and load tests remotely","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-cn","title":"Visual Studio Test Agent 2019","description":"支持运行自动测试和远程下载测试","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-tw","title":"Visual Studio Test Agent 2019","description":"支援在遠端執行自動化的測試與負載測試","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"cs-cz","title":"Visual Studio Test Agent 2019","description":"Podporuje vzdálené spouštění automatizovaných a zátěžových testů.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"de-de","title":"Visual Studio Test Agent 2019","description":"Unterstützt die Remoteausführung von automatisierten Tests und Auslastungstests.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"es-es","title":"Visual Studio Test Agent 2019","description":"Admite la ejecución de pruebas automatizadas y pruebas de carga de forma remota.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"fr-fr","title":"Visual Studio Test Agent 2019","description":"Prend en charge l'exécution de tests automatisés et de tests de charge à distance","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"it-it","title":"Visual Studio Test Agent 2019","description":"Supporta l'esecuzione di test automatizzati e test di carico in remoto","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ja-jp","title":"Visual Studio Test Agent 2019","description":"自動テストとロード テストのリモートでの実行をサポートする","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ko-kr","title":"Visual Studio Test Agent 2019","description":"자동화된 테스트 및 부하 테스트 원격 실행을 지원합니다.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pl-pl","title":"Agent testowy programu Visual Studio 2019","description":"Obsługuje zdalne uruchamianie zautomatyzowanych testów i testów obciążeniowych","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pt-br","title":"Test Agent do Visual Studio 2019","description":"Dá suporte à execução de testes automatizados e carrega testes remotamente","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ru-ru","title":"Агент тестирования Visual Studio 2019","description":"Поддерживает удаленное выполнение автоматических тестов и нагрузочных тестов","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"tr-tr","title":"Visual Studio Test Aracısı 2019","description":"Otomatik testler çalıştırmayı ve testleri uzaktan yüklemeyi destekler","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"}]},{"id":"Microsoft.VisualStudio.Product.TestController","version":"16.9.31205.134","type":"ChannelProduct","icon":{"mimeType":"image/svg+xml","base64":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+DQogIDxzdHlsZT4uaWNvbi1jYW52YXMtdHJhbnNwYXJlbnR7b3BhY2l0eTowO2ZpbGw6I2Y2ZjZmNn0uYnJhbmQtdnNpZGV7ZmlsbDojODY1ZmM1fTwvc3R5bGU+DQogIDxwYXRoIGNsYXNzPSJpY29uLWNhbnZhcy10cmFuc3BhcmVudCIgZD0iTTQwIDQwSDBWMGg0MHY0MHoiIGlkPSJjYW52YXMiLz4NCiAgPHBhdGggY2xhc3M9ImJyYW5kLXZzaWRlIiBkPSJNMzAuMjIxLS4wMDJMMTMuODg3IDE2LjE2IDQuMDUyIDguNzQ2IDAgMTAuMTAyVjI5LjlsNC4wNTIgMS4zNTYgOS44MzUtNy40MTQgMTYuMzM0IDE2LjE2TDQwIDM1Ljg0MlY0LjE1OGwtOS43NzktNC4xNnpNNC4wNTIgMjUuODlWMTQuMTExTDEwLjAwNCAyMGwtNS45NTIgNS44OXpNMzAgMjguNDcyTDE4Ljk4MyAyMCAzMCAxMS41Mjh2MTYuOTQ0eiIvPg0KPC9zdmc+"},"isHidden":true,"releaseNotes":"https://docs.microsoft.com/en-us/visualstudio/releases/2019/release-notes-v16.9#16.9.4","localizedResources":[{"language":"en-us","title":"Visual Studio Load Test Controller 2019","description":"Distribute automated tests to multiple machines","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-cn","title":"Visual Studio Load Test Controller 2019","description":"将自动测试分发到多台计算机","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"zh-tw","title":"Visual Studio Load Test Controller 2019","description":"將自動化的測試散發至多部電腦","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"cs-cz","title":"Visual Studio Load Test Controller 2019","description":"Distribuce automatizovaných testů na více počítačů","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"de-de","title":"Visual Studio Load Test Controller 2019","description":"Verteilt automatisierte Tests auf mehrere Computer.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"es-es","title":"Visual Studio Load Test Controller 2019","description":"Distribuye pruebas automatizadas a varias máquinas.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"fr-fr","title":"Visual Studio Load Test Controller 2019","description":"Distribuer des tests automatisés à plusieurs machines","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"it-it","title":"Visual Studio Load Test Controller 2019","description":"Consente di distribuire i test automatizzati in più computer","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ja-jp","title":"Visual Studio Load Test Controller 2019","description":"自動テストを複数のマシンに配布する","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ko-kr","title":"Visual Studio Load Test Controller 2019","description":"자동화된 테스트를 여러 컴퓨터에 배포합니다.","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pl-pl","title":"Visual Studio Load Test Controller 2019","description":"Dystrybuuj zautomatyzowane testy na wielu maszynach","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"pt-br","title":"Visual Studio Load Test Controller 2019","description":"Distribuir testes automatizados para vários computadores","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"ru-ru","title":"Visual Studio Load Test Controller 2019","description":"Распределение автоматических тестов на несколько компьютеров","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"},{"language":"tr-tr","title":"Visual Studio Yük Testi Denetleyicisi 2019","description":"Otomatik testleri birden fazla makineye dağıt","license":"https://go.microsoft.com/fwlink/?LinkId=2086101"}]},{"id":"VisualStudio.16.Release","version":"16.0.31205.134","type":"Channel","localizedResources":[{"language":"en-us","title":"Release","description":"Release","channelSuffix":""},{"language":"zh-cn","title":"Release","description":"Release","channelSuffix":""},{"language":"zh-tw","title":"Release","description":"Release","channelSuffix":""},{"language":"cs-cz","title":"Release","description":"Release","channelSuffix":""},{"language":"de-de","title":"Release","description":"Release","channelSuffix":""},{"language":"es-es","title":"Release","description":"Release","channelSuffix":""},{"language":"fr-fr","title":"Release","description":"Release","channelSuffix":""},{"language":"it-it","title":"Release","description":"Release","channelSuffix":""},{"language":"ja-jp","title":"Release","description":"Release","channelSuffix":""},{"language":"ko-kr","title":"Release","description":"Release","channelSuffix":""},{"language":"pl-pl","title":"Release","description":"Release","channelSuffix":""},{"language":"pt-br","title":"Release","description":"Release","channelSuffix":""},{"language":"ru-ru","title":"Release","description":"Release","channelSuffix":""},{"language":"tr-tr","title":"Release","description":"Release","channelSuffix":""}]},{"id":"VisualStudio.16.Release.Bootstrappers.Setup","version":"2.9.3365.38425","type":"Bootstrapper","payloads":[{"fileName":"vs_Setup.exe","sha256":"90284bdae989a0d752e2221341b86e735fd937bab060745f5577b517678aff76","size":1464568,"url":"https://download.visualstudio.microsoft.com/download/pr/3105fcfe-e771-41d6-9a1c-fc971e7d03a7/90284bdae989a0d752e2221341b86e735fd937bab060745f5577b517678aff76/vs_Setup.exe"}]}], + "signature" : { + "signInfo" : { + "signatureMethod" : "sha256RSA_cng", + "digestMethod" : "sha1", + "digestValue" : "qZXjnTdICzdwybnSN3+ef7c5Fc0=", + "canonicalization" : "" + }, + "signatureValue" : "GDxKpxaj5GESvZu3SbK72GZZoUSU4MOOhvJhLTXS9FqnaMx03UDt/oZyclZCP4ADTYYd1tg8VrgWi+4X33gt4KOwjdthA4corcAvSd5WGFKd2g8LTDyT81rJ+KWIJwcqAnu1sxqYgke8ds3O4xb3DH/q+UxkeJFehTSSgmNXJQD/91NKwFKvyg/aEmPwE7SyRjzEPul+5BZ5wuFbGCwWeLw2NZfOkz+026ibtn+tN4Ufn5vrezK4EdFRpZ010iXUdyVV9pGLVHa/1wKUOrZFmLmIHtV0D/8RhT+0r0UHPmMwBdtqp1wB7PBcH5YeW0bo19ukNRuI0VNjYVm4/9Nipg==", + "keyInfo" : { + "keyValue" : { + "rsaKeyValue" : { + "modulus" : "trsZWRAAo6nx5LhcqAsHy9uaHyPQ2VireMBI9yQUOPBj7dVLA7/N+AnKFFDzJ7P+grT6GkOE4cv5GzjoP8yQJ6yXojEKkXti7HW/zUiNoF11/ZWndf8j1Azl6OBjcD416tSWYvh2VfdW1K+mY83j49YPm3qbKnfxwtV0nI9H092gMS0cpCUsxMRAZlPXksrjsFLqvgq4rnULVhjHSVOudL/yps3zOOmOpaPzAp56b898xC+zzHVHcKo/52IRht1FSC8V+7QHTG8+yzfuljiKU9QONa8GqDlZ7/vFGveB8IY2ZrtUu98nle0WWTcaIRHoCYvWGLLF2u1GVFJAggPipw==", + "exponent" : "AQAB" + } + }, + "x509Data" : [ + "MIIF/zCCA+egAwIBAgITMwAAAd9r8C6Sp0q00AAAAAAB3zANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMB4XDTIwMTIxNTIxMzE0NVoXDTIxMTIwMjIxMzE0NVowdDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEeMBwGA1UEAxMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrsZWRAAo6nx5LhcqAsHy9uaHyPQ2VireMBI9yQUOPBj7dVLA7/N+AnKFFDzJ7P+grT6GkOE4cv5GzjoP8yQJ6yXojEKkXti7HW/zUiNoF11/ZWndf8j1Azl6OBjcD416tSWYvh2VfdW1K+mY83j49YPm3qbKnfxwtV0nI9H092gMS0cpCUsxMRAZlPXksrjsFLqvgq4rnULVhjHSVOudL/yps3zOOmOpaPzAp56b898xC+zzHVHcKo/52IRht1FSC8V+7QHTG8+yzfuljiKU9QONa8GqDlZ7/vFGveB8IY2ZrtUu98nle0WWTcaIRHoCYvWGLLF2u1GVFJAggPipwIDAQABo4IBfjCCAXowHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0OBBYEFDj2zC/CHZDRrQnzJlT7byOlWfPjMFAGA1UdEQRJMEekRTBDMSkwJwYDVQQLEyBNaWNyb3NvZnQgT3BlcmF0aW9ucyBQdWVydG8gUmljbzEWMBQGA1UEBRMNMjMwMDEyKzQ2MzAwOTAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAJ56h7Q8mFBWlQJLwCtHqqup4aC/eUmULt0Z6We7XUPPUEd/vuwPuIa6+1eMcZpAeQTm0tGCvjACxNNmrY8FoD3aWEOvFnSxq6CWR5G2XYBERvu7RExZd2iheCqaEmhjrJGV6Uz5wmjKNj16ADFTBqbEBELMIpmatyEN50UHwZSdD6DDHDf/j5LPGUy9QaD2LCaaJLenKpefaugsqWWCMIMifPdh6bbcmxyoNWbUC1JUl3HETJboD4BHDWSWoDxID2J4uG9dbJ40QIH9HckNMyPWi16k8VlFOaQiBYj09G9sLMc0agrchqqZBjPD/RmszvHmqJlSLQmAXCUgcgcf6UtHEmMAQRwGcSTg1KsUl6Ehg75k36lCV57Z1pC+KJKJNRYgg2eI6clzkLp2+noCF75IEO429rjtujsNJvEcJXg74TjK5x7LqYjj26Myq6EmuqWhbVUofPWm1EqKEfEHWXInppqBYXFpBMBYOLKc72DT+JyLNfd9utVsk2kTGaHHhrp+xgk9kZeud7lI/hfoPeHOtwIc0quJIXS+B5RSD9nj79vbJn1Jx7RqusmBQy509Kv2Pg4t48JaBfBFpJB0bUrl5RVG05sK/5Qw4G6WYioS0uwgUw499iNC+Yud9vrh3M8PNqGQ5mJmJiFEjG2ToEuuYe/e64+SSejpHhFCaAFc", + "MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzQ==", + "MIIF7TCCA9WgAwIBAgIQP4vItfyfspZDtWnWbELhRDANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwMzIyMjIwNTI4WhcNMzYwMzIyMjIxMzA0WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCygEGqNThNE3IyaCJNuLLx/9VSvGzH9dJKjDbu0cJcfoyKrq8TKG/Ac+M6ztAlqFo6be+ouFmrEyNozQwph9FvgFyPRH9dkAFSWKxRxV8qh9zc2AodwQO5e7BW6KPeZGHCnvjzfLnsDbVU/ky2ZU+I8JxImQxCCwl8MVkXeQZ4KI2JOkwDJb5xalwL54RgpJki49KvhKSn+9GY7Qyp3pSJ4Q6g3MDOmT3qCFK7VnnkH4S6Hri0xElcTzFLh93dBWcmmYDgcRGjuKVB4qRTufcyKYMME782XgSzS0NHL2vikR7TmE/dQgfI6B0S/Jmpaz6SfsjWaTr8ZL22CZ3K/QwLopt3YEsDlKQwaRLWQi3BQUzK3Kr9j1uDRprZ/LHR47PJf0h6zSTwQY9cdNCssBAgBkm3xy0hyFfj0IbzA2j70M5xwYmZSmQBbP3sMJHPQTySx+W6hh1hhMdfgzlirrSSL0fzC/hV66AfWdC7dJse0Hbm8ukG1xDo+mTeacY1logC8Ea4PyeZb8txiSk190gWAjWP1Xl8TQLPX+uKg09FcYj5qQ1OcunCnAfPSRtOBA5jUYxe2ADBVSy2xuDCZU7JNDn1nLPEfuhhbhNfFcRf2X7tHc7uROzLLoax7Dj2cO2rXBPB2Q8Nx4CyVe0096yb5MPa50c8prWPMd/FS6/r8QIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUci06AjGQQ7kUBU7h6qfHMdEjiTQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIBAH9yzw+3xRXbm8BJyiZb/p4T5tPw0tuXX/JLP02zrhmu7deXoKzvqTqjwkGw5biRnhOBJAPmCf0/V0A5ISRW0RAvS0CpNoZLtFNXmvvxfomPEf4YbFGq6O0JlbXlccmh6Yd1phV/yX43VF50k8XDZ8wNT2uoFwxtCJJ+i92Bqi1wIcM9BhS7vyRep4TXPw8hIr1LAAbblxzYXtTFC1yHblCk6MM4pPvLLMWSZpuFXst6bJN8gClYW1e1QGm6CHmmZGIVnYeWRbVmIyADixxzoNOieTPgUFmG2y/lAiXqcyqfABTINseSO+lOAOzYVgm5M0kS0lQLAausR7aRKX1MtHWAUgHoyoL2n8ysnI8X6i8msKtyrAv+nlEex0NVZ09Rs1fWtuzuUrc66U7h14GIvE+OdbtLqPA1qibUZ2dJsnBMO5PcHd94kIZysjik0dySTclY6ysSXNQ7roxrsIPlAT/4CTL2kzU0Iq/dNw13CYArzUgA8YyZGUcFAenRv9FO0OYoQzeZpApKCNmacXPSqs0xE2N2oTdvkjgefRI8ZjLny23h/FKJ3crWZgWalmG+oijHHKOnNlA8OqTfSm7mhzvO6/DggTedEzxSjr25HTTGHdUKaj2YKXCMiSrRq4IQSB/c9O+lxbtVGjhjhE63bK2VVOxlIhBJF7jAHscPrFRH" + ] + }, + "counterSign" : { + "x509Data" : [ + "MIIE9TCCA92gAwIBAgITMwAAAVt8sLo0ZzfBpwAAAAABWzANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMTAxMTQxOTAyMTZaFw0yMjA0MTExOTAyMTZaMIHOMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3NvZnQgT3BlcmF0aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046MEE1Ni1FMzI5LTRENEQxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIJH+l7PXaoXrLpi5bZ5epcI4g9Y4fiKc/+o+auQkM0p22lbqOCogokqa+VraqlZQ+50/91l+ler3KTUFeXHbVVcGnzaS598hfn0TaFFodUPbvFxokl/GM1UvKuvCTxYkTuBzMzKSwmko3H0GSHegorpMi0K7ip0hcHRoTMROxgmsmkPGQ8hDx7PwtseAAGDBbFTrLEnUfI2/H8wHpN0jZWbVSndCm/IqPt15EOeDL1F1fXFS9f3g3V1VQQajoR86CbMvnNsv7N1voBF/EG/Tv24wZEeoSGjsBAMOzbuNP0zFX8Fye4OUfxzVwre3OCGozTeFvgroHsrC52G6kZlvpAgMBAAGjggEbMIIBFzAdBgNVHQ4EFgQUZectNYhtt1MgXUx/9eU5yZi6qy4wHwYDVR0jBBgwFoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENBXzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0BAQsFAAOCAQEApzNrO6YTGpnOEHVaJaztWV0YgzFFXYLvf8qvIO5CFZfn5JVFdlZaLrevn6TqgBp3sDLcHpxbWoFYVSfB2rvDcJPiAIQdAdOA6GzQ8O7+ChEwEX/CjfIEx+ge0Yx4a3jA1oO4nFdA7KI/DCAPAIq1pcH+J6/KSh9J9qxE7HgSQ1nN3W1NCEyRB9UcxYRpFuyMzT0AjteuU6ezS516eJmmc6FcfD8ojjTun8g2a9MqlbofTqlh/nz2WEP2GBcoccvoR1jrqmKXPNz4Z9bwNAHtflp+G53umRoz8USOrMbDCJHQVw9ByS8je2H0q2zlQGMI2Fjh63rBmbr6BGhIA0VlKw==", + "MIIGcTCCBFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcNMjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEwRA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQedGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKxXf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4GkbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEAAaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0gAQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOhIW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS+7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlKkVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon/VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOiPPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCIIYdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7aKLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQcdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+NR4Iuto229Nfj950iEkS", + "MIIF7TCCA9WgAwIBAgIQKMw6Jb+6RKxEmptYa0M5qjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNjIzMjE1NzI0WhcNMzUwNjIzMjIwNDAxWjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC5CJ4o5OTsBk5QaLNBxXvrrraOr4G6IkQfZTRpTL5wQBfyFnvief2G7Q059BuorZKQHss9do9a2bWREC48BY2KbSRU5x/tVq2DtFCcFaUXdIhZIPwIxYR202jUbyh4zly481CQRP/jY1++oZoslhUE1gf+HoQh4EIxEcQoNpTPUKRinsnWq3EAslsM5pbUCiSW9f/G1bcb18u3IWKvEtyhXTfjGvsaRpjAm8DnYx8qCJMCfh5qjvKfGInkIoWisYRXQP/1DthvnO3iRTEBzRfpf7CBReOqIUAmoXKqp088AQV+7oNYsV4GY5likXiCtw2TDCRqtBvbJ+xflQQ/k0ow9ZcYs6f5GaeTMx0ByNsiUlzXJclG+aL7h1lDvptisY0thkQaRqx4YX4wCfquicRBKiJmA5E5RZzHiwyoyg0v+1LqDPdjMyOd/rAfrWfWp1ADxgRwY7UssYZaQ7f7rvluKW4hIUEmBozJw+6wwoWTobmF2eYybEtMP9Zdo+W1nXfDnMBVt3QA47g4q4OXUOGaQiQdxsCjMNEaWshSNPdz8ccYHzOteuzLQWDzI5QgwkhFrFxRxi6AwuJ3Fb2Fh+02nZaR7gC1o3Dsn+ONgGiDdrqvXXBSIhbiZvu6s8XC9z4vd6bK3sGmxkhMwzdRI9Mn17hOcJbwoUR2r3jPmuFmEwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU1fZWy4/oolxiaNE9lJBb186aGMQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIBAKylloy/u66m9tdxh0MxVoj9HDJxWzW31PCR8q834hTx8wImBT4WFH8UurhP+4mysufUCcxtuVs7ZGVwZrfysVrfGgLz9VG4Z215879We+SEuSsem0CcJjT5RxiYadgc17bRv49hwmfEte9gQ44QGzZJ5CDKrafBsSdlCfjN9Vsq0IQz8+8f8vWcC1iTN6B1oN5y3mx1KmYi9YwGMFafQLkwqkB3FYLXi+zA07K9g8V3DB6urxlToE15cZ8PrzDOZ/nWLMwiQXoH8pdCGM5ZeRBV3m8Q5Ljag2ZAFgloI1uXLiaaArtXjMW4umliMoCJnqH9wJJ8eyszGYQqY8UAaGL6n0eNmXpFOqfp7e5pQrXzgZtHVhB7/HA2hBhz6u/5l02eMyPdJgu6Krc/RNyDJ/+9YVkrEbfKT9vFiwwcMa4y+Pi5Qvd/3GGadrFaBOERPWZFtxhxvskkhdbz1LpBNF0SLSW5jaYTSG1LsAd9mZMJYYF0VyaKq2nj5NnHiMwk2OxSJFwevJEU4pbe6wrant1fs1vb1ILsxiBQhyVAOvvH7s3+M+Vuw4QJVQMlOcDpNV1lMaj2v6AJzSnHszYyLtyV84PBWs+LjfbqsyH4pO0eMQ62TBGrYAukEiMiF6M2ZIKRBBLgq28ey1AFYbRA/1mGcdHVM2l8qXOKONdkDPFp" + ], + "timestamp" : "2021-04-05-02:57:11", + "counterSignatureMethod" : "timeStamp", + "counterSignature" : "MIIS3AYJKoZIhvcNAQcCoIISzTCCEskCAQMxDzANBglghkgBZQMEAgEFADCCAVgGCyqGSIb3DQEJEAEEoIIBRwSCAUMwggE/AgEBBgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEIHgfen2ihBL6GliHM4R0pt80chTRtjGhmz/Ay1oeW2CDAgZgYyp8Nw0YEzIwMjEwNDA1MjE1NzExLjQ5NVowBIACAfQCAUKggdSkgdEwgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjowQTU2LUUzMjktNEQ0RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCDkQwggT1MIID3aADAgECAhMzAAABW3ywujRnN8GnAAAAAAFbMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTIxMDExNDE5MDIxNloXDTIyMDQxMTE5MDIxNlowgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjowQTU2LUUzMjktNEQ0RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMgkf6Xs9dqhesumLltnl6lwjiD1jh+Ipz/6j5q5CQzSnbaVuo4KiCiSpr5WtqqVlD7nT/3WX6V6vcpNQV5cdtVVwafNpLn3yF+fRNoUWh1Q9u8XGiSX8YzVS8q68JPFiRO4HMzMpLCaSjcfQZId6CiukyLQruKnSFwdGhMxE7GCayaQ8ZDyEPHs/C2x4AAYMFsVOssSdR8jb8fzAek3SNlZtVKd0Kb8io+3XkQ54MvUXV9cVL1/eDdXVVBBqOhHzoJsy+c2y/s3W+gEX8Qb9O/bjBkR6hIaOwEAw7Nu40/TMVfwXJ7g5R/HNXCt7c4IajNN4W+CugeysLnYbqRmW+kCAwEAAaOCARswggEXMB0GA1UdDgQWBBRl5y01iG23UyBdTH/15TnJmLqrLjAfBgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQCnM2s7phMamc4QdVolrO1ZXRiDMUVdgu9/yq8g7kIVl+fklUV2Vlout6+fpOqAGnewMtwenFtagVhVJ8Hau8Nwk+IAhB0B04DobNDw7v4KETARf8KN8gTH6B7RjHhreMDWg7icV0Dsoj8MIA8AirWlwf4nr8pKH0n2rETseBJDWc3dbU0ITJEH1RzFhGkW7IzNPQCO165Tp7NLnXp4maZzoVx8PyiONO6fyDZr0yqVuh9OqWH+fPZYQ/YYFyhxy+hHWOuqYpc83Phn1vA0Ae1+Wn4bne6ZGjPxRI6sxsMIkdBXD0HJLyN7YfSrbOVAYwjYWOHresGZuvoEaEgDRWUrMIIGcTCCBFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcNMjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEwRA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQedGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKxXf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4GkbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEAAaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0gAQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOhIW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS+7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlKkVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon/VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOiPPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCIIYdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7aKLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQcdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+NR4Iuto229Nfj950iEkSoYIC0jCCAjsCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjowQTU2LUUzMjktNEQ0RDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUACrtBbqYy0r+YGLtUaFVRW/Yh7qaggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOQVkd0wIhgPMjAyMTA0MDUxNzQwNDVaGA8yMDIxMDQwNjE3NDA0NVowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA5BWR3QIBADAKAgEAAgIc3wIB/zAHAgEAAgIRjDAKAgUA5BbjXQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAIUUvuZ5gMAQc99J4GoucB9VorvOWz6lIghZTXyZO66ptastSnzkd1ke6CbIYWj6l3Y3Q6p6SWxBL0wKC0NG3C9dMj3TwY0WhDluk/C/IC55JUHUm1L2U25iXbvaePJZUHaUJWwH7UgZeyqq2KZKgybDy822y+yRbn1tQEPqOiloMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAFbfLC6NGc3wacAAAAAAVswDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQg1Ax6WhkuTEzlNp+NIiOA98nSf23IIn5tSNJbuyDbyBMwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDJIuCpKGMRh4lCGucGPHCNJ7jq9MTbe3mQ2FtSZLCFGTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABW3ywujRnN8GnAAAAAAFbMCIEIDj3c3lbg37JfugbYUsoZNzLAAJR6vYzOSuYDER0OejBMA0GCSqGSIb3DQEBCwUABIIBAKmzMM2EIcXljufTvVFe2WpPs65/QP/GonrFObMGtWV4szvXJjrLAByvWHKKmO/+k50EBhi891CDetkRWHMeQIxx7Yz01ddnNRm4lbcs3PwRiB7UnaiI1Vw0zrrNx2ciObUF+CJD7gfvROr4P0NiLi3Fh3LOh3FGfpmuKfCXFUeRJKzKdLhizQTP4OuARnNuco3ycyoZT2xEOCEwot5LxbVoAfOZmIveXgw9XYRXVbdtqLiebaC9AQYgbN+h49wnMhZq35Rf70LFbAQTSC46paTrbz+LZXNiUWib7PGTYd2dvYrB/GrMNMotbG6+xH5ZiLv6DoW+g6UeZe1SwY8KzBkA" + } + } + +} diff --git a/ce/test/resources/2021-05-06-VisualStudio.vsman b/ce/test/resources/2021-05-06-VisualStudio.vsman new file mode 100644 index 0000000000..aa4e7d877c --- /dev/null +++ b/ce/test/resources/2021-05-06-VisualStudio.vsman @@ -0,0 +1,891 @@ +{ + "manifestVersion": "1.1", + "engineVersion": "2.9.3365.38425", + "info": { + "id": "VisualStudio/16.9.4+31205.134", + "buildBranch": "d16.9", + "buildVersion": "16.9.31205.134", + "localBuild": "build-lab", + "manifestName": "VisualStudio", + "manifestType": "installer", + "productDisplayVersion": "16.9.4", + "productLine": "Dev16", + "productLineVersion": "2019", + "productMilestone": "RTW", + "productMilestoneIsPreRelease": "False", + "productName": "Visual Studio", + "productPatchVersion": "4", + "productPreReleaseMilestoneSuffix": "1.0", + "productSemanticVersion": "16.9.4+31205.134" + }, + "signers": [ + { + "$id": "1", + "subjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US" + } + ], + "packages": [ + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86", + "version": "14.28.29914", + "type": "Vsix", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.vsix", + "sha256": "5b9e9d7f332e79bcc2342c49e7f99ddf43ecf6d130e5e6c6908dc2c7a39b15c6", + "size": 16126219, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/5b9e9d7f332e79bcc2342c49e7f99ddf43ecf6d130e5e6c6908dc2c7a39b15c6/Microsoft.VisualCpp.Tools.HostX86.TargetX86.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 35276532 + }, + "nonCriticalProcesses": [ + "vctip", + "mspdbsrv" + ], + "dependencies": { + "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources": "14.28.29914", + "Microsoft.VisualCpp.Props.x86": "14.28.29914", + "Microsoft.VisualCpp.Servicing.Compilers": "14.28.29914" + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "cs-CZ", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.csy.vsix", + "sha256": "47db023518b45bab6e7b9bba9aa2b0254895f3eb21221a70bdeb453e2747bafd", + "size": 229756, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/47db023518b45bab6e7b9bba9aa2b0254895f3eb21221a70bdeb453e2747bafd/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.csy.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 906288 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "de-DE", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.deu.vsix", + "sha256": "5e128ff82600b53cebd63afaa40d3714d52c539104bf8ad4b44d61f261e933a1", + "size": 234819, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/5e128ff82600b53cebd63afaa40d3714d52c539104bf8ad4b44d61f261e933a1/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.deu.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 1026640 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "en-US", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.enu.vsix", + "sha256": "4752b7e2053d1db888b43309d604ae7ed807d363ecbb39a89937ca051a10bdc8", + "size": 204190, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/4752b7e2053d1db888b43309d604ae7ed807d363ecbb39a89937ca051a10bdc8/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.enu.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 862328 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "es-ES", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.esn.vsix", + "sha256": "b9c8500e4f49df54292c7aab3bae930a940cd264877b4feee7cc26618f99edf4", + "size": 224027, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/b9c8500e4f49df54292c7aab3bae930a940cd264877b4feee7cc26618f99edf4/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.esn.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 1001560 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "fr-FR", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.fra.vsix", + "sha256": "fe41ec5957aaa14e5a613cd2352bc1171b1e4b550e980bf4dcd0b11fce1d7192", + "size": 225466, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/fe41ec5957aaa14e5a613cd2352bc1171b1e4b550e980bf4dcd0b11fce1d7192/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.fra.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 1012800 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "it-IT", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.ita.vsix", + "sha256": "8c540bc9953a7f1c26072e16748926a73818ae5fb5b8288bedbc0841e789bfac", + "size": 222846, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/8c540bc9953a7f1c26072e16748926a73818ae5fb5b8288bedbc0841e789bfac/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.ita.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 1012272 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "ja-JP", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.jpn.vsix", + "sha256": "91e4c239726a104a38c7ae69d1590abc20249354c0fd5a5ae580ceef8e5149eb", + "size": 200323, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/91e4c239726a104a38c7ae69d1590abc20249354c0fd5a5ae580ceef8e5149eb/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.jpn.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 636488 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "ko-KR", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.kor.vsix", + "sha256": "3b1fa4d736a7d82bef9054fbe5f52919d34a8c5ec614e3d957e7665b94ae9ece", + "size": 195053, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/3b1fa4d736a7d82bef9054fbe5f52919d34a8c5ec614e3d957e7665b94ae9ece/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.kor.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 640568 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "pl-PL", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.plk.vsix", + "sha256": "13b2331b7157e21006b79c873d532e5e256ae049f41b11be972e316eae6a8a1b", + "size": 233518, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/13b2331b7157e21006b79c873d532e5e256ae049f41b11be972e316eae6a8a1b/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.plk.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 982080 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "pt-BR", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.ptb.vsix", + "sha256": "2d4133d05cf9adec2bdbc73cbdb266c359d7bb1fe6e012cc346b1dff334df860", + "size": 219322, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/2d4133d05cf9adec2bdbc73cbdb266c359d7bb1fe6e012cc346b1dff334df860/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.ptb.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 947784 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "ru-RU", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.rus.vsix", + "sha256": "54d321dc5471c42d655aa5b613b55dab3f1129df179bef8597e470d46f464445", + "size": 233022, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/54d321dc5471c42d655aa5b613b55dab3f1129df179bef8597e470d46f464445/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.rus.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 968256 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "tr-TR", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.trk.vsix", + "sha256": "7b1cbe7df387e5132d8e639e407afe8c83b45f624cde06fc9aa6af0bf896f32d", + "size": 216776, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7b1cbe7df387e5132d8e639e407afe8c83b45f624cde06fc9aa6af0bf896f32d/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.trk.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 911416 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "zh-CN", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.chs.vsix", + "sha256": "7538c083be1f37b91217b8690a72746fc57bcd4c0e8c21f553ac4f88f8c60c98", + "size": 183804, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/7538c083be1f37b91217b8690a72746fc57bcd4c0e8c21f553ac4f88f8c60c98/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.chs.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 478256 + } + }, + { + "id": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources", + "version": "14.28.29914", + "type": "Vsix", + "language": "zh-TW", + "payloads": [ + { + "fileName": "Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.cht.vsix", + "sha256": "f0c019c004dcca5f7f5ce3a9b97730967cfff6f960113a55f7f317a41ade3df7", + "size": 187675, + "url": "https://download.visualstudio.microsoft.com/download/pr/3c309edd-88c5-4207-ab8d-fc1fda49d203/f0c019c004dcca5f7f5ce3a9b97730967cfff6f960113a55f7f317a41ade3df7/Microsoft.VisualCpp.Tools.HostX86.TargetX86.Resources.cht.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 503864 + } + }, + { + "id": "Microsoft.VisualStudio.Android.DeviceManager", + "version": "16.9.0.17", + "type": "Vsix", + "payloads": [ + { + "fileName": "Android.DeviceManager.16.9.0.17.vsix", + "sha256": "55d30f294f6d1ed1344043c22136a386f85e80b7159522366a9446c08bd8bc0a", + "size": 19848858, + "url": "https://download.visualstudio.microsoft.com/download/pr/a1e17755-863b-469f-bdce-fc29a2c42219/55d30f294f6d1ed1344043c22136a386f85e80b7159522366a9446c08bd8bc0a/Android.DeviceManager.16.9.0.17.vsix", + "signer": { + "$ref": "2" + } + } + ], + "vsixId": "Microsoft.VisualStudio.Android.DeviceManager", + "extensionDir": "[installdir]\\Common7\\IDE\\Extensions\\Xamarin\\AndroidDeviceManager", + "installSizes": { + "targetDrive": 34775879 + } + }, + { + "id": "Microsoft.VisualStudio.Android.SdkManager", + "version": "16.9.0.22", + "type": "Vsix", + "payloads": [ + { + "fileName": "Android.SDKManager.16.9.0.22.vsix", + "sha256": "1f6f970565a9eb2df163e86b375f109ab08477559098cafb9434ce062b105414", + "size": 8153124, + "url": "https://download.visualstudio.microsoft.com/download/pr/ec524651-cb3a-42ed-bf81-479abf7241ef/1f6f970565a9eb2df163e86b375f109ab08477559098cafb9434ce062b105414/Android.SDKManager.16.9.0.22.vsix", + "signer": { + "$ref": "2" + } + } + ], + "vsixId": "Microsoft.VisualStudio.Android.SdkManager", + "extensionDir": "[installdir]\\Common7\\IDE\\Extensions\\Xamarin\\AndroidSdkManager", + "installSizes": { + "targetDrive": 21015080 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis", + "version": "16.9.31004.209", + "type": "Vsix", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "1281710a2b68d239b03b0cfb6fc3fa10b0cbff55bf321f646bad04d455b4acd8", + "size": 119787, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/1281710a2b68d239b03b0cfb6fc3fa10b0cbff55bf321f646bad04d455b4acd8/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 300389 + }, + "dependencies": { + "Microsoft.VisualStudio.AppAnalysis.Resources": "[16.0,17.0)", + "Microsoft.VisualStudio.AppAnalysis.Targeted": "[16.0,17.0)" + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "cs-CZ", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "4ff4b9a4a66b8a48c754c66000cfb15c8741da110373b2ee6a4dc02541f91031", + "size": 10441, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/4ff4b9a4a66b8a48c754c66000cfb15c8741da110373b2ee6a4dc02541f91031/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16776 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "de-DE", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "cde590789e9e14a5ac01bd733bf5d5b68e3428f095d58b29b679d3151943bc61", + "size": 10390, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/cde590789e9e14a5ac01bd733bf5d5b68e3428f095d58b29b679d3151943bc61/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16776 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "en-US", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "1d290f4c97ccb407efbd6e980c7817d837a53c444347802deda9902a2b90bc1f", + "size": 11445, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/1d290f4c97ccb407efbd6e980c7817d837a53c444347802deda9902a2b90bc1f/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 18312 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "es-ES", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "95a58dab307d5af0018eb23b3ea4bfeedfb6f73941167903077b56721de43cad", + "size": 10414, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/95a58dab307d5af0018eb23b3ea4bfeedfb6f73941167903077b56721de43cad/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16776 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "fr-FR", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "87b16f9106fb48573b2defe49daffc9b6aa6014909f6416cf5a12c926d719d1e", + "size": 10405, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/87b16f9106fb48573b2defe49daffc9b6aa6014909f6416cf5a12c926d719d1e/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16776 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "it-IT", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "c9aa885be50b937aaba0f6237019bdbdcc439f8cd8b5f7442ec50227c4535da2", + "size": 10400, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/c9aa885be50b937aaba0f6237019bdbdcc439f8cd8b5f7442ec50227c4535da2/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16768 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "ja-JP", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "84db96d689ec1f2b8713402da16fa6b0ce5a421ff04c3a2213523fecd33132dd", + "size": 10452, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/84db96d689ec1f2b8713402da16fa6b0ce5a421ff04c3a2213523fecd33132dd/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16776 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "ko-KR", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "5e165cc99730dec170c9d80d2a88061faa6dc85b8735fd7ba17c02b835da7a02", + "size": 10398, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/5e165cc99730dec170c9d80d2a88061faa6dc85b8735fd7ba17c02b835da7a02/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16264 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "pl-PL", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "191e2795334e8e2268a948b82aa7a4da855b344d401929df69c04feb1958c17c", + "size": 10449, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/191e2795334e8e2268a948b82aa7a4da855b344d401929df69c04feb1958c17c/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16776 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "pt-BR", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "1ef2e571e7fdc6505bbf85084050a6be18777affd8a5d3d197e142459ecec55d", + "size": 10438, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/1ef2e571e7fdc6505bbf85084050a6be18777affd8a5d3d197e142459ecec55d/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16768 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "ru-RU", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "c98df39a4d2b00eef7104d758d0ce8eb306df1d0625bb740adce89b2f06e5fce", + "size": 10602, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/c98df39a4d2b00eef7104d758d0ce8eb306df1d0625bb740adce89b2f06e5fce/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 17288 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "tr-TR", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "073d1b0d65fcf30c018e329db198b76d518dd8725c7a0c7dd7cb3d573454a27d", + "size": 10380, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/073d1b0d65fcf30c018e329db198b76d518dd8725c7a0c7dd7cb3d573454a27d/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16264 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "zh-CN", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "c44ec3a437d681c201e3337cc5f5851aa9bbc60318098dc0a76b4b023c6882c2", + "size": 10369, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/c44ec3a437d681c201e3337cc5f5851aa9bbc60318098dc0a76b4b023c6882c2/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16256 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "zh-TW", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "50cf10f28fb36d9eab105ebf2839978456083135db2cf138e187b56514d486f4", + "size": 10372, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/50cf10f28fb36d9eab105ebf2839978456083135db2cf138e187b56514d486f4/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 16256 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Targeted", + "version": "16.9.31004.209", + "type": "Vsix", + "chip": "x64", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "ee2e695eda6f3ac617819639b908a72d21f06fa964bde402b62c2eba76af786c", + "size": 132073, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/ee2e695eda6f3ac617819639b908a72d21f06fa964bde402b62c2eba76af786c/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 320904 + } + }, + { + "id": "Microsoft.VisualStudio.AppAnalysis.Targeted", + "version": "16.9.31004.209", + "type": "Vsix", + "chip": "x86", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "bef150ba88f7b96ca711c03a2023f9d48cc947eff172e2b13fd12d32ada3c5c9", + "size": 100383, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/bef150ba88f7b96ca711c03a2023f9d48cc947eff172e2b13fd12d32ada3c5c9/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 212360 + } + }, + { + "id": "Microsoft.VisualStudio.AppCapDesigner", + "version": "16.9.227.58203", + "type": "Vsix", + "payloads": [ + { + "fileName": "Microsoft.VisualStudio.TabDesigner.vsix", + "sha256": "4f90596bc77ab8fe2d3e488ff245b069ee18bae9b7249d1ccc6c19c0313f17f0", + "size": 43466, + "url": "https://download.visualstudio.microsoft.com/download/pr/a1e17755-863b-469f-bdce-fc29a2c42219/4f90596bc77ab8fe2d3e488ff245b069ee18bae9b7249d1ccc6c19c0313f17f0/Microsoft.VisualStudio.TabDesigner.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 503306 + } + }, + { + "id": "Microsoft.VisualStudio.ApplicationInsights", + "version": "16.9.693.2781", + "type": "Vsix", + "payloads": [ + { + "fileName": "Microsoft.VisualStudio.ApplicationInsights.vsix", + "sha256": "71e3d86e3aeba09243db599a086ac013eb8a1d87aafa49a42ed14c04d6425ff7", + "size": 4248134, + "url": "https://download.visualstudio.microsoft.com/download/pr/4a5b18ed-7a52-478c-acb3-27c4dd4976fe/71e3d86e3aeba09243db599a086ac013eb8a1d87aafa49a42ed14c04d6425ff7/Microsoft.VisualStudio.ApplicationInsights.vsix", + "signer": { + "$ref": "2" + } + } + ], + "vsixId": "6784BA9A-FE1E-4000-8357-DC3D9B2C27F9", + "extensionDir": "[installdir]\\Common7\\IDE\\Extensions\\Microsoft\\AppInsights\\Core", + "installSizes": { + "targetDrive": 10949663 + } + }, + { + "id": "Microsoft.VisualStudio.ApplicationInsights.Interfaces", + "version": "16.9.693.2781", + "type": "Vsix", + "payloads": [ + { + "fileName": "Microsoft.VisualStudio.ApplicationInsights.Interfaces.vsix", + "sha256": "73e328d3c1e2f83c9efb78e8955db766a0a6aa66580620eb2474cccaf6abc824", + "size": 130256, + "url": "https://download.visualstudio.microsoft.com/download/pr/4a5b18ed-7a52-478c-acb3-27c4dd4976fe/73e328d3c1e2f83c9efb78e8955db766a0a6aa66580620eb2474cccaf6abc824/Microsoft.VisualStudio.ApplicationInsights.Interfaces.vsix", + "signer": { + "$ref": "2" + } + } + ], + "vsixId": "7B66689B-28BD-4D2E-96CD-A76F120CA567", + "extensionDir": "[installdir]\\Common7\\IDE\\Extensions\\Microsoft\\AppInsights\\Contracts", + "folderMappings": { + "$Licenses": "[installdir]\\Licenses", + "$MSBuild": "[installdir]\\MSBuild", + "$PublicAssemblies": "[installdir]\\Common7\\IDE\\PublicAssemblies", + "$ReferenceAssemblies": "[installdir]\\Common7\\IDE\\ReferenceAssemblies", + "$RemoteDebugger": "[installdir]\\Common7\\IDE\\Remote Debugger", + "$Schemas": "[installdir]\\Xml\\Schemas" + }, + "installSizes": { + "targetDrive": 317417 + } + }, + { + "id": "Microsoft.VisualStudio.AppResponsiveness", + "version": "16.9.31004.209", + "type": "Vsix", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "d3bbf06bf22eb3264e4c3731150a69e0fedb7905259f5289cd6ba147a3b1dad8", + "size": 635669, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/d3bbf06bf22eb3264e4c3731150a69e0fedb7905259f5289cd6ba147a3b1dad8/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 2469136 + }, + "dependencies": { + "Microsoft.VisualStudio.AppResponsiveness.Resources": "[16.0,17.0)", + "Microsoft.VisualStudio.AppResponsiveness.Targeted": "[16.0,17.0)" + } + }, + { + "id": "Microsoft.VisualStudio.AppResponsiveness.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "cs-CZ", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "5d7ca8fd8a1b55f454c415b47326ac7af7d561de898c3506a1c95b10df95cb61", + "size": 31237, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/5d7ca8fd8a1b55f454c415b47326ac7af7d561de898c3506a1c95b10df95cb61/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 81000 + } + }, + { + "id": "Microsoft.VisualStudio.AppResponsiveness.Resources", + "version": "16.9.31004.209", + "type": "Vsix", + "language": "de-DE", + "payloads": [ + { + "fileName": "payload.vsix", + "sha256": "b26bcd8f9b233583090287fd8946ebe306ea7f408e3678e9afe19eb47bc5874f", + "size": 31078, + "url": "https://download.visualstudio.microsoft.com/download/pr/f4fa30c9-f388-4b84-bba6-bd9e451dcdbc/b26bcd8f9b233583090287fd8946ebe306ea7f408e3678e9afe19eb47bc5874f/payload.vsix", + "signer": { + "$ref": "2" + } + } + ], + "installSizes": { + "targetDrive": 80884 + } + } + ], + "deprecate": { + "Component.Microsoft.VisualStudio.TaskStatusCenter": "0.0", + "Component.Microsoft.VisualStudio.ASALExtensionOOB": "0.0", + "Component.Microsoft.VisualStudio.LanguageServer.Client.Preview": "0.0" + }, + "signature": { + "signInfo": { + "signatureMethod": "sha256RSA_cng", + "digestMethod": "sha1", + "digestValue": "wf1oGQWxPvicAO36d3KqvWOB8ek=", + "canonicalization": "" + }, + "signatureValue": "RVm7286coztlPMpvl7E+eNkrMs1nL/uO4M1hkPDqT6KDjBugERoAdBd3Lrwu5NvjM15GL17sGCtvWjqsfb5+X6/TrBS6jcfOw4uKipIiD/bnG5GDQYglqndUutxic8Uso4wjXYvyO9IwS0njyNV9R8RrzHZwtQeCauHmX/tUDtjw0L5NTRlSAR11mKKKnaARFXlM7bwqojleJlACMSQzV5m06UoXRytMgt+WYuYoMuThZLDEPi47jByvfgENfO4YxL/IAJz2Xn2blTwi67resnOgUONyzk1XT2XaU0JfJ9zUNs34xr7cl4mMqO7br0nvGRpQfQY7jNlRUa1dkFzlYg==", + "keyInfo": { + "keyValue": { + "rsaKeyValue": { + "modulus": "trsZWRAAo6nx5LhcqAsHy9uaHyPQ2VireMBI9yQUOPBj7dVLA7/N+AnKFFDzJ7P+grT6GkOE4cv5GzjoP8yQJ6yXojEKkXti7HW/zUiNoF11/ZWndf8j1Azl6OBjcD416tSWYvh2VfdW1K+mY83j49YPm3qbKnfxwtV0nI9H092gMS0cpCUsxMRAZlPXksrjsFLqvgq4rnULVhjHSVOudL/yps3zOOmOpaPzAp56b898xC+zzHVHcKo/52IRht1FSC8V+7QHTG8+yzfuljiKU9QONa8GqDlZ7/vFGveB8IY2ZrtUu98nle0WWTcaIRHoCYvWGLLF2u1GVFJAggPipw==", + "exponent": "AQAB" + } + }, + "x509Data": [ + "MIIF/zCCA+egAwIBAgITMwAAAd9r8C6Sp0q00AAAAAAB3zANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMB4XDTIwMTIxNTIxMzE0NVoXDTIxMTIwMjIxMzE0NVowdDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEeMBwGA1UEAxMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtrsZWRAAo6nx5LhcqAsHy9uaHyPQ2VireMBI9yQUOPBj7dVLA7/N+AnKFFDzJ7P+grT6GkOE4cv5GzjoP8yQJ6yXojEKkXti7HW/zUiNoF11/ZWndf8j1Azl6OBjcD416tSWYvh2VfdW1K+mY83j49YPm3qbKnfxwtV0nI9H092gMS0cpCUsxMRAZlPXksrjsFLqvgq4rnULVhjHSVOudL/yps3zOOmOpaPzAp56b898xC+zzHVHcKo/52IRht1FSC8V+7QHTG8+yzfuljiKU9QONa8GqDlZ7/vFGveB8IY2ZrtUu98nle0WWTcaIRHoCYvWGLLF2u1GVFJAggPipwIDAQABo4IBfjCCAXowHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0OBBYEFDj2zC/CHZDRrQnzJlT7byOlWfPjMFAGA1UdEQRJMEekRTBDMSkwJwYDVQQLEyBNaWNyb3NvZnQgT3BlcmF0aW9ucyBQdWVydG8gUmljbzEWMBQGA1UEBRMNMjMwMDEyKzQ2MzAwOTAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAJ56h7Q8mFBWlQJLwCtHqqup4aC/eUmULt0Z6We7XUPPUEd/vuwPuIa6+1eMcZpAeQTm0tGCvjACxNNmrY8FoD3aWEOvFnSxq6CWR5G2XYBERvu7RExZd2iheCqaEmhjrJGV6Uz5wmjKNj16ADFTBqbEBELMIpmatyEN50UHwZSdD6DDHDf/j5LPGUy9QaD2LCaaJLenKpefaugsqWWCMIMifPdh6bbcmxyoNWbUC1JUl3HETJboD4BHDWSWoDxID2J4uG9dbJ40QIH9HckNMyPWi16k8VlFOaQiBYj09G9sLMc0agrchqqZBjPD/RmszvHmqJlSLQmAXCUgcgcf6UtHEmMAQRwGcSTg1KsUl6Ehg75k36lCV57Z1pC+KJKJNRYgg2eI6clzkLp2+noCF75IEO429rjtujsNJvEcJXg74TjK5x7LqYjj26Myq6EmuqWhbVUofPWm1EqKEfEHWXInppqBYXFpBMBYOLKc72DT+JyLNfd9utVsk2kTGaHHhrp+xgk9kZeud7lI/hfoPeHOtwIc0quJIXS+B5RSD9nj79vbJn1Jx7RqusmBQy509Kv2Pg4t48JaBfBFpJB0bUrl5RVG05sK/5Qw4G6WYioS0uwgUw499iNC+Yud9vrh3M8PNqGQ5mJmJiFEjG2ToEuuYe/e64+SSejpHhFCaAFc", + "MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzQ==", + "MIIF7TCCA9WgAwIBAgIQP4vItfyfspZDtWnWbELhRDANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwMzIyMjIwNTI4WhcNMzYwMzIyMjIxMzA0WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCygEGqNThNE3IyaCJNuLLx/9VSvGzH9dJKjDbu0cJcfoyKrq8TKG/Ac+M6ztAlqFo6be+ouFmrEyNozQwph9FvgFyPRH9dkAFSWKxRxV8qh9zc2AodwQO5e7BW6KPeZGHCnvjzfLnsDbVU/ky2ZU+I8JxImQxCCwl8MVkXeQZ4KI2JOkwDJb5xalwL54RgpJki49KvhKSn+9GY7Qyp3pSJ4Q6g3MDOmT3qCFK7VnnkH4S6Hri0xElcTzFLh93dBWcmmYDgcRGjuKVB4qRTufcyKYMME782XgSzS0NHL2vikR7TmE/dQgfI6B0S/Jmpaz6SfsjWaTr8ZL22CZ3K/QwLopt3YEsDlKQwaRLWQi3BQUzK3Kr9j1uDRprZ/LHR47PJf0h6zSTwQY9cdNCssBAgBkm3xy0hyFfj0IbzA2j70M5xwYmZSmQBbP3sMJHPQTySx+W6hh1hhMdfgzlirrSSL0fzC/hV66AfWdC7dJse0Hbm8ukG1xDo+mTeacY1logC8Ea4PyeZb8txiSk190gWAjWP1Xl8TQLPX+uKg09FcYj5qQ1OcunCnAfPSRtOBA5jUYxe2ADBVSy2xuDCZU7JNDn1nLPEfuhhbhNfFcRf2X7tHc7uROzLLoax7Dj2cO2rXBPB2Q8Nx4CyVe0096yb5MPa50c8prWPMd/FS6/r8QIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUci06AjGQQ7kUBU7h6qfHMdEjiTQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIBAH9yzw+3xRXbm8BJyiZb/p4T5tPw0tuXX/JLP02zrhmu7deXoKzvqTqjwkGw5biRnhOBJAPmCf0/V0A5ISRW0RAvS0CpNoZLtFNXmvvxfomPEf4YbFGq6O0JlbXlccmh6Yd1phV/yX43VF50k8XDZ8wNT2uoFwxtCJJ+i92Bqi1wIcM9BhS7vyRep4TXPw8hIr1LAAbblxzYXtTFC1yHblCk6MM4pPvLLMWSZpuFXst6bJN8gClYW1e1QGm6CHmmZGIVnYeWRbVmIyADixxzoNOieTPgUFmG2y/lAiXqcyqfABTINseSO+lOAOzYVgm5M0kS0lQLAausR7aRKX1MtHWAUgHoyoL2n8ysnI8X6i8msKtyrAv+nlEex0NVZ09Rs1fWtuzuUrc66U7h14GIvE+OdbtLqPA1qibUZ2dJsnBMO5PcHd94kIZysjik0dySTclY6ysSXNQ7roxrsIPlAT/4CTL2kzU0Iq/dNw13CYArzUgA8YyZGUcFAenRv9FO0OYoQzeZpApKCNmacXPSqs0xE2N2oTdvkjgefRI8ZjLny23h/FKJ3crWZgWalmG+oijHHKOnNlA8OqTfSm7mhzvO6/DggTedEzxSjr25HTTGHdUKaj2YKXCMiSrRq4IQSB/c9O+lxbtVGjhjhE63bK2VVOxlIhBJF7jAHscPrFRH" + ] + }, + "counterSign": { + "x509Data": [ + "MIIE9TCCA92gAwIBAgITMwAAAVhwWiL3vpbmAwAAAAABWDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMTAxMTQxOTAyMTRaFw0yMjA0MTExOTAyMTRaMIHOMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3NvZnQgT3BlcmF0aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046NDYyRi1FMzE5LTNGMjAxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChHwuXYPWrsNCgBRsL9e8jBRvEn6oFFBQvA88GvJq6bNHsoUUNjb/Su/7M/31RNaP9X2aeKuEhorXLIzxrTp41seOVSBUyDUKXaDoZrD3Zxct4AV6TBrU316i551BOPlZigtrwITmdOlOr7eQnNHCaKhCbczlkcBGs/AaF9pwl9UQV5B9z4gLu7Vib91fM4UUjyxZnoifgiMGstOAFIJq8FxEB7yR4G+j4iwsYBNlQAQgzU+QlconjWqXGYisdekGw5XuyjsJIzBCCpHMUft9nQzLcwraSFA4KysZo8fhpveIx4nqITh1LoZd7t4ZQGH79kgP/Ok9VDQIgUIN1rvcbAgMBAAGjggEbMIIBFzAdBgNVHQ4EFgQUS3DZG32dHBgf7ud+oHuTJ9Oi+VgwHwYDVR0jBBgwFoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljVGltU3RhUENBXzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0BAQsFAAOCAQEAOd8oA1qL0K4fH7pYjV1tAlAU83wOEpeIfiDxIeZTXa4Qxcuk+DAPY7qdc85RZKWK1HNLE30AgDpwI5rpz4J5mkuW0n9lR/DIN+FNqoDyyJzAJBmgbPwc2myeuWCntT+SCmTe1o9m0XwitNxEvJEu4OmEB+u4sTAkAiw63lgyiWLDbNHITaSTgM8iXhn8kVHvk1FGxcI7Av9fCpmDg1YKUUmGcdFu46xqpSVRHobsKUiLBjmAgTJyQzXSpz/tdwoOvHFbQjV+pCXb1BR9GYrjzJQWA+xqwj6gEZUp/r8X3zIr7tgzCSS5HssMUnw+drA1fjQX+SJ4rihXBPctJvZtow==", + "MIIGcTCCBFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcNMjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEwRA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQedGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKxXf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4GkbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEAAaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0gAQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOhIW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS+7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlKkVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon/VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOiPPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCIIYdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7aKLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQcdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+NR4Iuto229Nfj950iEkS", + "MIIF7TCCA9WgAwIBAgIQKMw6Jb+6RKxEmptYa0M5qjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNjIzMjE1NzI0WhcNMzUwNjIzMjIwNDAxWjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC5CJ4o5OTsBk5QaLNBxXvrrraOr4G6IkQfZTRpTL5wQBfyFnvief2G7Q059BuorZKQHss9do9a2bWREC48BY2KbSRU5x/tVq2DtFCcFaUXdIhZIPwIxYR202jUbyh4zly481CQRP/jY1++oZoslhUE1gf+HoQh4EIxEcQoNpTPUKRinsnWq3EAslsM5pbUCiSW9f/G1bcb18u3IWKvEtyhXTfjGvsaRpjAm8DnYx8qCJMCfh5qjvKfGInkIoWisYRXQP/1DthvnO3iRTEBzRfpf7CBReOqIUAmoXKqp088AQV+7oNYsV4GY5likXiCtw2TDCRqtBvbJ+xflQQ/k0ow9ZcYs6f5GaeTMx0ByNsiUlzXJclG+aL7h1lDvptisY0thkQaRqx4YX4wCfquicRBKiJmA5E5RZzHiwyoyg0v+1LqDPdjMyOd/rAfrWfWp1ADxgRwY7UssYZaQ7f7rvluKW4hIUEmBozJw+6wwoWTobmF2eYybEtMP9Zdo+W1nXfDnMBVt3QA47g4q4OXUOGaQiQdxsCjMNEaWshSNPdz8ccYHzOteuzLQWDzI5QgwkhFrFxRxi6AwuJ3Fb2Fh+02nZaR7gC1o3Dsn+ONgGiDdrqvXXBSIhbiZvu6s8XC9z4vd6bK3sGmxkhMwzdRI9Mn17hOcJbwoUR2r3jPmuFmEwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU1fZWy4/oolxiaNE9lJBb186aGMQwEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQELBQADggIBAKylloy/u66m9tdxh0MxVoj9HDJxWzW31PCR8q834hTx8wImBT4WFH8UurhP+4mysufUCcxtuVs7ZGVwZrfysVrfGgLz9VG4Z215879We+SEuSsem0CcJjT5RxiYadgc17bRv49hwmfEte9gQ44QGzZJ5CDKrafBsSdlCfjN9Vsq0IQz8+8f8vWcC1iTN6B1oN5y3mx1KmYi9YwGMFafQLkwqkB3FYLXi+zA07K9g8V3DB6urxlToE15cZ8PrzDOZ/nWLMwiQXoH8pdCGM5ZeRBV3m8Q5Ljag2ZAFgloI1uXLiaaArtXjMW4umliMoCJnqH9wJJ8eyszGYQqY8UAaGL6n0eNmXpFOqfp7e5pQrXzgZtHVhB7/HA2hBhz6u/5l02eMyPdJgu6Krc/RNyDJ/+9YVkrEbfKT9vFiwwcMa4y+Pi5Qvd/3GGadrFaBOERPWZFtxhxvskkhdbz1LpBNF0SLSW5jaYTSG1LsAd9mZMJYYF0VyaKq2nj5NnHiMwk2OxSJFwevJEU4pbe6wrant1fs1vb1ILsxiBQhyVAOvvH7s3+M+Vuw4QJVQMlOcDpNV1lMaj2v6AJzSnHszYyLtyV84PBWs+LjfbqsyH4pO0eMQ62TBGrYAukEiMiF6M2ZIKRBBLgq28ey1AFYbRA/1mGcdHVM2l8qXOKONdkDPFp" + ], + "timestamp": "2021-04-05-02:57:12", + "counterSignatureMethod": "timeStamp", + "counterSignature": "MIIS3AYJKoZIhvcNAQcCoIISzTCCEskCAQMxDzANBglghkgBZQMEAgEFADCCAVgGCyqGSIb3DQEJEAEEoIIBRwSCAUMwggE/AgEBBgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEIKdn60lVh3n8t71dZc7vzAehDb4AE3PBd27W30VuU5MlAgZgYyqNCDMYEzIwMjEwNDA1MjE1NzEyLjI4MlowBIACAfQCASeggdSkgdEwgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0NjJGLUUzMTktM0YyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCDkQwggT1MIID3aADAgECAhMzAAABWHBaIve+luYDAAAAAAFYMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTIxMDExNDE5MDIxNFoXDTIyMDQxMTE5MDIxNFowgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0NjJGLUUzMTktM0YyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKEfC5dg9auw0KAFGwv17yMFG8SfqgUUFC8Dzwa8mrps0eyhRQ2Nv9K7/sz/fVE1o/1fZp4q4SGitcsjPGtOnjWx45VIFTINQpdoOhmsPdnFy3gBXpMGtTfXqLnnUE4+VmKC2vAhOZ06U6vt5Cc0cJoqEJtzOWRwEaz8BoX2nCX1RBXkH3PiAu7tWJv3V8zhRSPLFmeiJ+CIway04AUgmrwXEQHvJHgb6PiLCxgE2VABCDNT5CVyieNapcZiKx16QbDle7KOwkjMEIKkcxR+32dDMtzCtpIUDgrKxmjx+Gm94jHieohOHUuhl3u3hlAYfv2SA/86T1UNAiBQg3Wu9xsCAwEAAaOCARswggEXMB0GA1UdDgQWBBRLcNkbfZ0cGB/u536ge5Mn06L5WDAfBgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1RpbVN0YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQA53ygDWovQrh8fuliNXW0CUBTzfA4Sl4h+IPEh5lNdrhDFy6T4MA9jup1zzlFkpYrUc0sTfQCAOnAjmunPgnmaS5bSf2VH8Mg34U2qgPLInMAkGaBs/BzabJ65YKe1P5IKZN7Wj2bRfCK03ES8kS7g6YQH67ixMCQCLDreWDKJYsNs0chNpJOAzyJeGfyRUe+TUUbFwjsC/18KmYODVgpRSYZx0W7jrGqlJVEehuwpSIsGOYCBMnJDNdKnP+13Cg68cVtCNX6kJdvUFH0ZiuPMlBYD7GrCPqARlSn+vxffMivu2DMJJLkeywxSfD52sDV+NBf5IniuKFcE9y0m9m2jMIIGcTCCBFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAwNzAxMjEzNjU1WhcNMjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6IOz8E5f1+n9plGt0VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsalR3OCROOfGEwWbEwRA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8kYDJYYEbyWEeGMoQedGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRnEnIaIYqvS2SJUGKxXf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWayrGo8noqCjHw2k4GkbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURdXhacAQVPIk0CAwEAAaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBTVYzpcijGQ80N7fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDCBoAYDVR0gAQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUHAgEWMWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVsdC5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkAXwBTAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN4sbgmD+BcQM9naOhIW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj+bzta1RXCCtRgkQS+7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/xn/yN31aPxzymXlKkVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaPWSm8tv0E4XCfMkon/VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jnsGUpxY517IW3DnKOiPPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWAmyI4ILUl5WTs9/S/fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99g/DhO3EJ3110mCIIYdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mBy6cJrDm77MbL2IK0cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4SKfXAL1QnIffIrE7aKLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYovG8chr1m1rtxEPJdQcdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7vDaBQNdrvCScc1bN+NR4Iuto229Nfj950iEkSoYIC0jCCAjsCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjo0NjJGLUUzMTktM0YyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAqckrcxrn0Qshpuozjp+l+DSfNL+ggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOQVkdMwIhgPMjAyMTA0MDUxNzQwMzVaGA8yMDIxMDQwNjE3NDAzNVowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA5BWR0wIBADAKAgEAAgIe2QIB/zAHAgEAAgIRVTAKAgUA5BbjUwIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBACteO17KZoGggSXnvNxXDqDM9ukmZPZzMqS4VXb+0wJxkHQD4VyhT3B+w/Po7rFrc8mpvjJrB6w9cQQGQfkCKn3pzsySHa9Gfoipeb97vQdUPlERy1We+1KtF6+WQDE9q4+NBItWsOW8Kqlfyvff2lv6f+H4d4RQMEP44jJrL4SIMYIDDTCCAwkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAFYcFoi976W5gMAAAAAAVgwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgCTaEt/pdo23lscmfpBrJYQDtXi+gl8/gZbR5mRySSnIwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCDySjONbIY1l2zKT4ba4sCI4WkBC6sIfR9uSVNVx3DTBzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABWHBaIve+luYDAAAAAAFYMCIEIJlv9DUO3M/Y9dTNx4LrPW4ECvXaHwAijGJ08VXg5ImaMA0GCSqGSIb3DQEBCwUABIIBABcAVKWp1qEgULA+7qz6XF0a+gphpUyCHnrd+d9Poc5JuJ6NeK3wk41XxLwpA029zJgWjKFfMwm8tb7x2gQ2zRwkMrRj5MJSicFAkfA+qnaR4h4kjyLaAG+a4fYTrkgXPsLeJUvFw36TZDUiyVkVGIE9IusJht0SQiAb8OZXnDbts8cAMoPUezbOwfmq4/vocvlLqj5EnDvP1bUias9veVSW5wP6PmFDAsWv1zeMKILQxJ0aXbC6EDuc5py8RIW83REpHi5LMTOFUmbPAgwoiZzBWL1JmGy9SVuowUZsiV1AiiFExEC3j1tXwkdKb599qlYtQgB3y02zDiBkbDUn35AA" + } + } +} diff --git a/ce/test/resources/big-compression.zip b/ce/test/resources/big-compression.zip new file mode 100644 index 0000000000..2e41234e47 Binary files /dev/null and b/ce/test/resources/big-compression.zip differ diff --git a/ce/test/resources/cmake.yaml b/ce/test/resources/cmake.yaml new file mode 100644 index 0000000000..3166caaaa1 --- /dev/null +++ b/ce/test/resources/cmake.yaml @@ -0,0 +1,67 @@ +# Kitware CMake Toolsuite +# install with `ce add cmake` + +info: + id: tools/kitware/cmake + version: 3.20.1 + description: CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice. The suite of CMake tools were created by Kitware in response to the need for a powerful, cross-platform build environment for open-source projects such as ITK and VTK. + summary: Kitware's cmake tool + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + + Kitware: + email: kitware@kitware.com + role: originator + +windows and x64: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-windows-x86_64.zip + sha256: 056378cb599353479c3a8aa2654454b8a3eaa3c8c0872928ba7e09c3ec50774c + strip: 1 # + +windows and x86: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-windows-i386.zip + sha256: 650026534e66dabe0ed6be3422e86fabce5fa86d43927171ea8b8dfd0877fc9d + strip: 1 # + +windows: + settings: + tools: + cmake: bin/cmake.exe + cmake_gui: bin/cmake-gui.exe + ctest: bin/ctest.exe + + paths: + path: bin + +osx: + install: + untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-macos-universal.tar.gz + sha256: 89afcb79f58bb1f0bb840047c146c3fac8051829b6025c3dbe9b75799b27deb4 + strip: 3 + +linux and x64: + install: + untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-linux-x86_64.tar.gz + sha256: B8C141BD7A6D335600AB0A8A35E75AF79F95B837F736456B5532F4D717F20A09 + strip: 1 + +linux and arm64: + install: + untar: https://github.com/Kitware/CMake/releases/download/v3.20.1/cmake-3.20.1-linux-aarch64.tar.gz + sha256: 2761a222c14a15b9bdf1bdb4a17c10806757b7ed3bc26a84523f042ec212b76c + strip: 1 + +not windows: + settings: + tools: + cmake: bin/cmake + cmake_gui: bin/cmake-gui + ctest: bin/ctest + + paths: + path: bin \ No newline at end of file diff --git a/ce/test/resources/empty.yaml b/ce/test/resources/empty.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ce/test/resources/environment.yaml b/ce/test/resources/environment.yaml new file mode 100644 index 0000000000..b5e3745c38 --- /dev/null +++ b/ce/test/resources/environment.yaml @@ -0,0 +1,10 @@ +# Environment configuration +info: + id: NAME + version: 1.0.0 + summary: My Project + +requires: + compilers/arm/gcc: 2020.10.0 + tools/kitware/cmake: 3.20.1 + \ No newline at end of file diff --git a/ce/test/resources/errors.yaml b/ce/test/resources/errors.yaml new file mode 100644 index 0000000000..e76c92b23b --- /dev/null +++ b/ce/test/resources/errors.yaml @@ -0,0 +1,10 @@ + +top: foo +top: foobar + +info: + id: bob + version: 1.0.2 + summary: none + +$*$*$*$* diff --git a/ce/test/resources/example-tar.tar b/ce/test/resources/example-tar.tar new file mode 100644 index 0000000000..8d1fe3c50e Binary files /dev/null and b/ce/test/resources/example-tar.tar differ diff --git a/ce/test/resources/example-tar.tar.bz2 b/ce/test/resources/example-tar.tar.bz2 new file mode 100644 index 0000000000..fda3fafaab Binary files /dev/null and b/ce/test/resources/example-tar.tar.bz2 differ diff --git a/ce/test/resources/example-tar.tar.gz b/ce/test/resources/example-tar.tar.gz new file mode 100644 index 0000000000..5f79984fb7 Binary files /dev/null and b/ce/test/resources/example-tar.tar.gz differ diff --git a/ce/test/resources/example-tar.tar.xz b/ce/test/resources/example-tar.tar.xz new file mode 100644 index 0000000000..ec41a7d51f Binary files /dev/null and b/ce/test/resources/example-tar.tar.xz differ diff --git a/ce/test/resources/example-zip.zip b/ce/test/resources/example-zip.zip new file mode 100644 index 0000000000..ff9f3ed1b0 Binary files /dev/null and b/ce/test/resources/example-zip.zip differ diff --git a/ce/test/resources/large-file.txt b/ce/test/resources/large-file.txt new file mode 100644 index 0000000000..e90232410d --- /dev/null +++ b/ce/test/resources/large-file.txt @@ -0,0 +1,559 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? + +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore +eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt +in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et +dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex +ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur? \ No newline at end of file diff --git a/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.04.0.yaml b/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.04.0.yaml new file mode 100644 index 0000000000..4962c6ee0c --- /dev/null +++ b/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.04.0.yaml @@ -0,0 +1,28 @@ +info: + id: compilers/gnu/gcc/arm-none-eabi + version: 2019.4.0 + description: sample yaml file that might work for gcc + summary: GCC compiler for ARM CPUs. from early 2019 + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + +windows: + install: + unzip: https://developer.arm.com/-/media/Files/downloads/gnu-rm/10-2020q4/gcc-arm-none-eabi-10-2020-q4-major-win32.zip + sha256: 90057B8737B888C53CA5AEE332F1F73C401D6D3873124D2C2906DF4347EBEF9E + strip: 1 # + +not windows: + error: not yet supported + +settings: + tools: + CC: bin/arm-none-eabi-gcc.exe + CXX: bin/arm-none-eabi-g++.exe + + paths: + - bin + - gcc-arm-eabi-none/bin diff --git a/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.10.0.yaml b/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.10.0.yaml new file mode 100644 index 0000000000..e6548af53a --- /dev/null +++ b/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2019.10.0.yaml @@ -0,0 +1,28 @@ +info: + id: compilers/gnu/gcc/arm-none-eabi + version: 2019.10.0 + description: sample yaml file that might work for gcc from late 2019 + summary: GCC compiler for ARM CPUs. This is an older one, but newer than the oldest + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + +windows: + install: + unzip: https://developer.arm.com/-/media/Files/downloads/gnu-rm/10-2020q4/gcc-arm-none-eabi-10-2020-q4-major-win32.zip + sha256: 90057B8737B888C53CA5AEE332F1F73C401D6D3873124D2C2906DF4347EBEF9E + strip: 1 # + +not windows: + error: not yet supported + +settings: + tools: + CC: bin/arm-none-eabi-gcc.exe + CXX: bin/arm-none-eabi-g++.exe + + paths: + - bin + - gcc-arm-eabi-none/bin diff --git a/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2020-10.0.yaml b/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2020-10.0.yaml new file mode 100644 index 0000000000..1900ea48f3 --- /dev/null +++ b/ce/test/resources/repo/compilers/gnu/gcc-arm-none-eabi-2020-10.0.yaml @@ -0,0 +1,28 @@ +info: + id: compilers/gnu/gcc/arm-none-eabi + version: 2020.10.0 + description: sample yaml file that might work for gcc + summary: GCC compiler for ARM CPUs. + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + +windows: + install: + unzip: https://developer.arm.com/-/media/Files/downloads/gnu-rm/10-2020q4/gcc-arm-none-eabi-10-2020-q4-major-win32.zip + sha256: 90057B8737B888C53CA5AEE332F1F73C401D6D3873124D2C2906DF4347EBEF9E + strip: 1 # + +not windows: + error: not yet supported + +settings: + tools: + CC: bin/arm-none-eabi-gcc.exe + CXX: bin/arm-none-eabi-g++.exe + + paths: + - bin + - gcc-arm-eabi-none/bin diff --git a/ce/test/resources/repo/sdks/microsoft/windows.arm.yaml b/ce/test/resources/repo/sdks/microsoft/windows.arm.yaml new file mode 100644 index 0000000000..78e60d059d --- /dev/null +++ b/ce/test/resources/repo/sdks/microsoft/windows.arm.yaml @@ -0,0 +1,18 @@ +# Windows SDK +# install with `ce add sdks/microsoft/windows` + +info: + id: sdks/microsoft/windows/arm + version: 10.0.19041 + description: The Windows SDK available as a NuGet package for more seamless acquisition and CI/CD integration. This package is designed for C++ applications (targeting arm) + summary: Microsoft Windows SDK. (targeting arm) + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + +install: + nupkg: Microsoft.Windows.SDK.cpp.arm/10.0.19041.5 + sha256: fluffyKittenBunnies + \ No newline at end of file diff --git a/ce/test/resources/repo/sdks/microsoft/windows.arm64.yaml b/ce/test/resources/repo/sdks/microsoft/windows.arm64.yaml new file mode 100644 index 0000000000..b31e2ca44d --- /dev/null +++ b/ce/test/resources/repo/sdks/microsoft/windows.arm64.yaml @@ -0,0 +1,18 @@ +# Windows SDK +# install with `ce add sdks/microsoft/windows` + +info: + id: sdks/microsoft/windows/arm64 + version: 10.0.19041 + description: The Windows SDK available as a NuGet package for more seamless acquisition and CI/CD integration. This package is designed for C++ applications (targeting arm64) + summary: Microsoft Windows SDK. (targeting arm64) + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + +install: + nupkg: Microsoft.Windows.SDK.cpp.arm64/10.0.19041.5 + sha256: fluffyKittenBunnies + \ No newline at end of file diff --git a/ce/test/resources/repo/sdks/microsoft/windows.x64.yaml b/ce/test/resources/repo/sdks/microsoft/windows.x64.yaml new file mode 100644 index 0000000000..017e1eb7e6 --- /dev/null +++ b/ce/test/resources/repo/sdks/microsoft/windows.x64.yaml @@ -0,0 +1,27 @@ +# Windows SDK +# install with `ce add sdks/microsoft/windows` + +info: + id: sdks/microsoft/windows/x64 + version: 10.0.19041 + description: The Windows SDK available as a NuGet package for more seamless acquisition and CI/CD integration. This package is designed for C++ applications (targeting x64) + summary: Microsoft Windows SDK. (targeting x64) + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + +install: + nupkg: Microsoft.Windows.SDK.cpp.x64/10.0.19041.5 + sha256: fluffyKittenBunnies + +x64: + settings: + paths: + - ./**/bin/x64 + +not x64: + settings: + paths: + - ./**/bin/x64 diff --git a/ce/test/resources/repo/sdks/microsoft/windows.x86.yaml b/ce/test/resources/repo/sdks/microsoft/windows.x86.yaml new file mode 100644 index 0000000000..555c63c18a --- /dev/null +++ b/ce/test/resources/repo/sdks/microsoft/windows.x86.yaml @@ -0,0 +1,18 @@ +# Windows SDK +# install with `ce add sdks/microsoft/windows` + +info: + id: sdks/microsoft/windows/x86 + version: 10.0.19041 + description: The Windows SDK available as a NuGet package for more seamless acquisition and CI/CD integration. This package is designed for C++ applications (targeting x86) + summary: Microsoft Windows SDK. (targeting x86) + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + +install: + nupkg: Microsoft.Windows.SDK.cpp.x86/10.0.19041.5 + sha256: fluffyKittenBunnies + \ No newline at end of file diff --git a/ce/test/resources/repo/sdks/microsoft/windows.yaml b/ce/test/resources/repo/sdks/microsoft/windows.yaml new file mode 100644 index 0000000000..179a5afea8 --- /dev/null +++ b/ce/test/resources/repo/sdks/microsoft/windows.yaml @@ -0,0 +1,34 @@ +# Windows SDK +# install with `ce add sdks/microsoft/windows` + +info: + id: sdks/microsoft/windows + version: 10.0.19041 + description: The Windows SDK available as a NuGet package for more seamless acquisition and CI/CD integration. This package is designed for C++ applications + summary: Microsoft Windows SDK. + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + +install: + nupkg: Microsoft.Windows.SDK.cpp/10.0.19041.5 + sha256: fluffyKittenBunnies + # https://www.nuget.org/api/v2/package/Microsoft.Windows.SDK.CPP/10.0.19041.5 + +windows and target:x64: + requires: + sdks/microsoft/windows/x64: 10.0.19041 + +windows and target:x86: + requires: + sdks/microsoft/windows/x86: 10.0.19041 + +windows and target:arm: + requires: + sdks/microsoft/windows/arm: 10.0.19041 + +windows and target:arm64: + requires: + sdks/microsoft/windows/arm64: 10.0.19041 diff --git a/ce/test/resources/repo/tools/kitware/cmake-3.15.0.yaml b/ce/test/resources/repo/tools/kitware/cmake-3.15.0.yaml new file mode 100644 index 0000000000..2ab2831086 --- /dev/null +++ b/ce/test/resources/repo/tools/kitware/cmake-3.15.0.yaml @@ -0,0 +1,34 @@ + +info: + id: tools/kitware/cmake + version: 3.15.0 + description: sample yaml file that might work + summary: Kitware's cmake tool + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + Kitware: + email: kitware@kitware.com + role: originator + +requires: + compilers/arm/gcc: 2020.10.0 + compilers/arm/other: 1.2.3 + +windows: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-windows-x86_64.zip + sha256: 056378cb599353479c3a8aa2654454b8a3eaa3c8c0872928ba7e09c3ec50774c + strip: 1 # + +not windows: + error: not yet supported + +settings: + tools: + cmake: bin/cmake.exe + + paths: + - bin diff --git a/ce/test/resources/repo/tools/kitware/cmake-3.15.1.yaml b/ce/test/resources/repo/tools/kitware/cmake-3.15.1.yaml new file mode 100644 index 0000000000..ac4700f47b --- /dev/null +++ b/ce/test/resources/repo/tools/kitware/cmake-3.15.1.yaml @@ -0,0 +1,30 @@ + +info: + id: tools/kitware/cmake + version: 3.15.1 + description: sample yaml file that might work + summary: Kitware's cmake tool + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + Kitware: + email: kitware@kitware.com + role: originator + +windows: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-windows-x86_64.zip + sha256: 056378cb599353479c3a8aa2654454b8a3eaa3c8c0872928ba7e09c3ec50774c + strip: 1 # + +not windows: + error: not yet supported + +settings: + tools: + cmake: bin/cmake.exe + + paths: + - bin diff --git a/ce/test/resources/repo/tools/kitware/cmake-3.17.0.yaml b/ce/test/resources/repo/tools/kitware/cmake-3.17.0.yaml new file mode 100644 index 0000000000..ec76399253 --- /dev/null +++ b/ce/test/resources/repo/tools/kitware/cmake-3.17.0.yaml @@ -0,0 +1,30 @@ + +info: + id: tools/kitware/cmake + version: 3.17.0 + description: sample yaml file that might work + summary: Kitware's cmake tool + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + Kitware: + email: kitware@kitware.com + role: originator + +windows: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-windows-x86_64.zip + sha256: 056378cb599353479c3a8aa2654454b8a3eaa3c8c0872928ba7e09c3ec50774c + strip: 1 # + +not windows: + error: not yet supported + +settings: + tools: + cmake: bin/cmake.exe + + paths: + - bin diff --git a/ce/test/resources/repo/tools/kitware/cmake-3.19.0.yaml b/ce/test/resources/repo/tools/kitware/cmake-3.19.0.yaml new file mode 100644 index 0000000000..08208d5c76 --- /dev/null +++ b/ce/test/resources/repo/tools/kitware/cmake-3.19.0.yaml @@ -0,0 +1,30 @@ + +info: + id: tools/kitware/cmake + version: 3.19.0 + description: sample yaml file that might work + summary: Kitware's cmake tool + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + Kitware: + email: kitware@kitware.com + role: originator + +windows: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-windows-x86_64.zip + sha256: 056378cb599353479c3a8aa2654454b8a3eaa3c8c0872928ba7e09c3ec50774c + strip: 1 # + +not windows: + error: not yet supported + +settings: + tools: + cmake: bin/cmake.exe + + paths: + - bin diff --git a/ce/test/resources/repo/tools/kitware/cmake-3.20.0.yaml b/ce/test/resources/repo/tools/kitware/cmake-3.20.0.yaml new file mode 100644 index 0000000000..e17ff34b30 --- /dev/null +++ b/ce/test/resources/repo/tools/kitware/cmake-3.20.0.yaml @@ -0,0 +1,30 @@ + +info: + id: tools/kitware/cmake + version: 3.20.0 + description: sample yaml file that might work + summary: Kitware's cmake tool + +contacts: + Garrett Serack: + email: garretts@microsoft.com + role: publisher + Kitware: + email: kitware@kitware.com + role: originator + +windows: + install: + unzip: https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0-windows-x86_64.zip + sha256: 056378cb599353479c3a8aa2654454b8a3eaa3c8c0872928ba7e09c3ec50774c + strip: 1 # + +not windows: + error: not yet supported + +settings: + tools: + cmake: bin/cmake.exe + + paths: + - bin diff --git a/ce/test/resources/sample1.yaml b/ce/test/resources/sample1.yaml new file mode 100644 index 0000000000..5334034924 --- /dev/null +++ b/ce/test/resources/sample1.yaml @@ -0,0 +1,43 @@ +info: + id: sample1 + version: 1.2.3 + +contacts: + Garrett Serack: + email: garrett@serack.org + role: developer + + Bob Smith: + email: bob@smith.com + role: + - fallguy + - otherguy # cool, right? + +requires: + foo/bar/bin: ~2.0.0 + bar/bin:baz: '* 1.2.3' + bar/bin/buz: ~* # with a comment. + weird/range: '>= 1.0 <= 2.0 2.0.0' + nuget/range: (2.0,3.0] 2.3.4 + +settings: + tools: + CC: foo/bar/cl.exe + CXX: bin/baz/cl.exe + Whatever: some/tool/path/foo + + variables: + test: abc + cxxflags: + - foo=bar + - bar=baz + + paths: + bin: + - foo/bar/bin/baz + - foo/bar/bin/waz +demands: + windows and arm: + install: + nupkg: floobaloo/1.2.3 + sha256: fluffyKittenBunnies diff --git a/ce/test/resources/small-file.txt b/ce/test/resources/small-file.txt new file mode 100644 index 0000000000..c2f590c383 --- /dev/null +++ b/ce/test/resources/small-file.txt @@ -0,0 +1,2 @@ +this is a small file. + diff --git a/ce/test/resources/validation-errors.yaml b/ce/test/resources/validation-errors.yaml new file mode 100644 index 0000000000..b6cccddf09 --- /dev/null +++ b/ce/test/resources/validation-errors.yaml @@ -0,0 +1,14 @@ +info: + nothing: here + +goober ): + install: bugger me + +goober: + install: not correct + +floopy: floo + +windows and target:x64: + install: + nupkg: floobaloo/1.2.3 diff --git a/ce/test/resources/wrong-entry-sizes.zip b/ce/test/resources/wrong-entry-sizes.zip new file mode 100644 index 0000000000..a8ba209ed4 Binary files /dev/null and b/ce/test/resources/wrong-entry-sizes.zip differ diff --git a/ce/test/sequence-equal.ts b/ce/test/sequence-equal.ts new file mode 100644 index 0000000000..2b938e3e43 --- /dev/null +++ b/ce/test/sequence-equal.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { fail, strict } from 'assert'; + +// I like my collections to be easier to compare. +declare module 'assert' { + namespace assert { + function sequenceEqual(actual: Iterable | undefined, expected: Iterable, message?: string | Error): void; + function throws(block: () => any, message?: string | Error): void; + } +} + +(strict).sequenceEqual = (a: Iterable, e: Iterable, message: string) => { + a && e ? strict.deepEqual([...a], [...e], message) : fail(message); +}; diff --git a/ce/test/tsconfig.json b/ce/test/tsconfig.json new file mode 100644 index 0000000000..2b1910fd9d --- /dev/null +++ b/ce/test/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../common/tsconfig.json", + "compilerOptions": { + "experimentalDecorators": true, + "outDir": "./dist", + "rootDir": ".", + "types": [ + "node", + "mocha" + ], + "inlineSourceMap": true, + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "node_modules/**", + "**/*.d.ts" + ] +} \ No newline at end of file