diff --git a/sources/Engine.ts b/sources/Engine.ts index e08bb1ee1..9525552da 100644 --- a/sources/Engine.ts +++ b/sources/Engine.ts @@ -127,7 +127,7 @@ export class Engine { throw new UsageError(`This package manager (${descriptor.name}) isn't supported by this corepack build`); let finalDescriptor = descriptor; - if (descriptor.range.match(/^[a-z-]+$/)) { + if (/^[a-z-]+$/.test(descriptor.range)) { if (!allowTags) throw new UsageError(`Packages managers can't be referended via tags in this context`); @@ -151,6 +151,11 @@ export class Engine { if (cachedVersion !== null && useCache) return {name: finalDescriptor.name, reference: cachedVersion}; + // If the user asked for a specific version, no need to request the list of + // available versions from the registry. + if (semver.valid(finalDescriptor.range)) + return {name: finalDescriptor.name, reference: finalDescriptor.range}; + const candidateRangeDefinitions = Object.keys(definition.ranges).filter(range => { return semverUtils.satisfiesWithPrereleases(finalDescriptor.range, range); }); diff --git a/sources/corepackUtils.ts b/sources/corepackUtils.ts index 94d90d615..93cc79018 100644 --- a/sources/corepackUtils.ts +++ b/sources/corepackUtils.ts @@ -1,3 +1,4 @@ +import {createHash} from 'crypto'; import {once} from 'events'; import fs from 'fs'; import type {Dir} from 'fs'; @@ -81,21 +82,22 @@ export async function findInstalledVersion(installTarget: string, descriptor: De export async function installVersion(installTarget: string, locator: Locator, {spec}: {spec: PackageManagerSpec}) { const {default: tar} = await import(/* webpackMode: 'eager' */ `tar`); + const {version, build} = semver.parse(locator.reference)!; - const installFolder = path.join(installTarget, locator.name, locator.reference); + const installFolder = path.join(installTarget, locator.name, version); if (fs.existsSync(installFolder)) { debugUtils.log(`Reusing ${locator.name}@${locator.reference}`); return installFolder; } - const url = spec.url.replace(`{}`, locator.reference); + const url = spec.url.replace(`{}`, version); // Creating a temporary folder inside the install folder means that we // are sure it'll be in the same drive as the destination, so we can // just move it there atomically once we are done const tmpFolder = folderUtils.getTemporaryFolder(installTarget); - debugUtils.log(`Installing ${locator.name}@${locator.reference} from ${url} to ${tmpFolder}`); + debugUtils.log(`Installing ${locator.name}@${version} from ${url} to ${tmpFolder}`); const stream = await httpUtils.fetchUrlStream(url); const parsedUrl = new URL(url); @@ -113,8 +115,16 @@ export async function installVersion(installTarget: string, locator: Locator, {s stream.pipe(sendTo); + const hash = build[0] + ? stream.pipe(createHash(build[0])) + : null; + await once(sendTo, `finish`); + const actualHash = hash?.digest(`hex`); + if (actualHash !== build[1]) + throw new Error(`Mismatch hashes. Expected ${build[1]}, got ${actualHash}`); + await fs.promises.mkdir(path.dirname(installFolder), {recursive: true}); try { await fs.promises.rename(tmpFolder, installFolder); diff --git a/tests/main.test.ts b/tests/main.test.ts index 24e9be813..a8010cf83 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -8,13 +8,38 @@ beforeEach(async () => { process.env.COREPACK_HOME = npath.fromPortablePath(await xfs.mktempPromise()); }); +it(`should refuse to download a package manager if the hash doesn't match`, async () => { + await xfs.mktempPromise(async cwd => { + await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), { + packageManager: `yarn@1.22.4+sha1.deadbeef`, + }); + + await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ + exitCode: 1, + stdout: /Mismatch hashes/, + }); + }); +}); + const testedPackageManagers: Array<[string, string]> = [ [`yarn`, `1.22.4`], + [`yarn`, `1.22.4+sha1.01c1197ca5b27f21edc8bc472cd4c8ce0e5a470e`], + [`yarn`, `1.22.4+sha224.0d6eecaf4d82ec12566fdd97143794d0f0c317e0d652bd4d1b305430`], [`yarn`, `2.0.0-rc.30`], + [`yarn`, `2.0.0-rc.30+sha1.4f0423b01bcb57f8e390b4e0f1990831f92dd1da`], + [`yarn`, `2.0.0-rc.30+sha224.0e7a64468c358596db21c401ffeb11b6534fce7367afd3ae640eadf1`], [`yarn`, `3.0.0-rc.2`], + [`yarn`, `3.0.0-rc.2+sha1.694bdad81703169e203febd57f9dc97d3be867bd`], + [`yarn`, `3.0.0-rc.2+sha224.f83f6d1cbfac10ba6b516a62ccd2a72ccd857aa6c514d1cd7185ec60`], [`pnpm`, `4.11.6`], + [`pnpm`, `4.11.6+sha1.7cffc04295f4db4740225c6c37cc345eb923c06a`], + [`pnpm`, `4.11.6+sha224.7783c4b01916b7a69e6ff05d328df6f83cb7f127e9c96be88739386d`], [`pnpm`, `6.6.2`], + [`pnpm`, `6.6.2+sha1.7b4d6b176c1b93b5670ed94c24babb7d80c13854`], + [`pnpm`, `6.6.2+sha224.eb5c0acad3b0f40ecdaa2db9aa5a73134ad256e17e22d1419a2ab073`], [`npm`, `6.14.2`], + [`npm`, `6.14.2+sha1.f057d35cd4792c4c511bb1fa332edb43143d07b0`], + [`npm`, `6.14.2+sha224.50512c1eb404900ee78586faa6d756b8d867ff46a328e6fb4cdf3a87`], ]; for (const [name, version] of testedPackageManagers) { @@ -26,7 +51,7 @@ for (const [name, version] of testedPackageManagers) { await expect(runCli(cwd, [name, `--version`])).resolves.toMatchObject({ exitCode: 0, - stdout: `${version}\n`, + stdout: `${version.split(`+`, 1)[0]}\n`, }); }); }); @@ -136,17 +161,17 @@ it(`should use the pinned version when local projects don't list any spec`, asyn }); await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({ - stdout: `${config.definitions.yarn.default}\n`, + stdout: `${config.definitions.yarn.default.split(`+`, 1)[0]}\n`, exitCode: 0, }); await expect(runCli(cwd, [`pnpm`, `--version`])).resolves.toMatchObject({ - stdout: `${config.definitions.pnpm.default}\n`, + stdout: `${config.definitions.pnpm.default.split(`+`, 1)[0]}\n`, exitCode: 0, }); await expect(runCli(cwd, [`npm`, `--version`])).resolves.toMatchObject({ - stdout: `${config.definitions.npm.default}\n`, + stdout: `${config.definitions.npm.default.split(`+`, 1)[0]}\n`, exitCode: 0, }); });