Skip to content

Commit 4672162

Browse files
zhyupeaduh95arcanis
authored
fix: hash check when downloading Yarn Berry from npm (#439)
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com> Co-authored-by: Maël Nison <nison.mael@gmail.com>
1 parent 14b8a01 commit 4672162

File tree

5 files changed

+88
-33
lines changed

5 files changed

+88
-33
lines changed

config.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@
149149
},
150150
"npmRegistry": {
151151
"type": "npm",
152-
"package": "@yarnpkg/cli-dist"
152+
"package": "@yarnpkg/cli-dist",
153+
"bin": "bin/yarn.js"
153154
},
154155
"commands": {
155156
"use": [

sources/corepackUtils.ts

+75-28
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import * as httpUtils from './httpUtils
1515
import * as nodeUtils from './nodeUtils';
1616
import * as npmRegistryUtils from './npmRegistryUtils';
1717
import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types';
18-
import {BinList, BinSpec, InstallSpec} from './types';
18+
import {BinList, BinSpec, InstallSpec, DownloadSpec} from './types';
1919

2020
export function getRegistryFromPackageManagerSpec(spec: PackageManagerSpec) {
2121
return process.env.COREPACK_NPM_REGISTRY
@@ -132,6 +132,66 @@ function isValidBinSpec(x: unknown): x is BinSpec {
132132
return typeof x === `object` && x !== null && !Array.isArray(x) && Object.keys(x).length > 0;
133133
}
134134

135+
async function download(installTarget: string, url: string, algo: string, binPath: string | null = null): Promise<DownloadSpec> {
136+
// Creating a temporary folder inside the install folder means that we
137+
// are sure it'll be in the same drive as the destination, so we can
138+
// just move it there atomically once we are done
139+
140+
const tmpFolder = folderUtils.getTemporaryFolder(installTarget);
141+
debugUtils.log(`Downloading to ${tmpFolder}`);
142+
143+
const stream = await httpUtils.fetchUrlStream(url);
144+
145+
const parsedUrl = new URL(url);
146+
const ext = path.posix.extname(parsedUrl.pathname);
147+
148+
let outputFile: string | null = null;
149+
let sendTo: any;
150+
151+
if (ext === `.tgz`) {
152+
const {default: tar} = await import(`tar`);
153+
sendTo = tar.x({
154+
strip: 1,
155+
cwd: tmpFolder,
156+
filter: binPath ? path => {
157+
const pos = path.indexOf(`/`);
158+
return pos !== -1 && path.slice(pos + 1) === binPath;
159+
} : undefined,
160+
});
161+
} else if (ext === `.js`) {
162+
outputFile = path.join(tmpFolder, path.posix.basename(parsedUrl.pathname));
163+
sendTo = fs.createWriteStream(outputFile);
164+
}
165+
stream.pipe(sendTo);
166+
167+
let hash = !binPath ? stream.pipe(createHash(algo)) : null;
168+
await once(sendTo, `finish`);
169+
170+
if (binPath) {
171+
const downloadedBin = path.join(tmpFolder, binPath);
172+
outputFile = path.join(tmpFolder, path.basename(downloadedBin));
173+
try {
174+
await renameSafe(downloadedBin, outputFile);
175+
} catch (err) {
176+
if ((err as nodeUtils.NodeError)?.code === `ENOENT`)
177+
throw new Error(`Cannot locate '${binPath}' in downloaded tarball`, {cause: err});
178+
179+
throw err;
180+
}
181+
182+
// Calculate the hash of the bin file
183+
const fileStream = fs.createReadStream(outputFile);
184+
hash = fileStream.pipe(createHash(algo));
185+
await once(fileStream, `close`);
186+
}
187+
188+
return {
189+
tmpFolder,
190+
outputFile,
191+
hash: hash!.digest(`hex`),
192+
};
193+
}
194+
135195
export async function installVersion(installTarget: string, locator: Locator, {spec}: {spec: PackageManagerSpec}): Promise<InstallSpec> {
136196
const locatorIsASupportedPackageManager = isSupportedPackageManagerLocator(locator);
137197
const locatorReference = locatorIsASupportedPackageManager ? semver.parse(locator.reference)! : parseURLReference(locator);
@@ -159,12 +219,16 @@ export async function installVersion(installTarget: string, locator: Locator, {s
159219
}
160220

161221
let url: string;
222+
let binPath: string | null = null;
162223
if (locatorIsASupportedPackageManager) {
163224
url = spec.url.replace(`{}`, version);
164225
if (process.env.COREPACK_NPM_REGISTRY) {
165226
const registry = getRegistryFromPackageManagerSpec(spec);
166227
if (registry.type === `npm`) {
167228
url = await npmRegistryUtils.fetchTarballUrl(registry.package, version);
229+
if (registry.bin) {
230+
binPath = registry.bin;
231+
}
168232
} else {
169233
url = url.replace(
170234
npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL,
@@ -182,33 +246,9 @@ export async function installVersion(installTarget: string, locator: Locator, {s
182246
}
183247
}
184248

185-
// Creating a temporary folder inside the install folder means that we
186-
// are sure it'll be in the same drive as the destination, so we can
187-
// just move it there atomically once we are done
188-
189-
const tmpFolder = folderUtils.getTemporaryFolder(installTarget);
190-
debugUtils.log(`Installing ${locator.name}@${version} from ${url} to ${tmpFolder}`);
191-
const stream = await httpUtils.fetchUrlStream(url);
192-
193-
const parsedUrl = new URL(url);
194-
const ext = path.posix.extname(parsedUrl.pathname);
195-
196-
let outputFile: string | null = null;
197-
198-
let sendTo: any;
199-
if (ext === `.tgz`) {
200-
const {default: tar} = await import(`tar`);
201-
sendTo = tar.x({strip: 1, cwd: tmpFolder});
202-
} else if (ext === `.js`) {
203-
outputFile = path.join(tmpFolder, path.posix.basename(parsedUrl.pathname));
204-
sendTo = fs.createWriteStream(outputFile);
205-
}
206-
207-
stream.pipe(sendTo);
208-
249+
debugUtils.log(`Installing ${locator.name}@${version} from ${url}`);
209250
const algo = build[0] ?? `sha256`;
210-
const hash = stream.pipe(createHash(algo));
211-
await once(sendTo, `finish`);
251+
const {tmpFolder, outputFile, hash: actualHash} = await download(installTarget, url, algo, binPath);
212252

213253
let bin: BinSpec | BinList;
214254
const isSingleFile = outputFile !== null;
@@ -240,7 +280,6 @@ export async function installVersion(installTarget: string, locator: Locator, {s
240280
}
241281
}
242282

243-
const actualHash = hash.digest(`hex`);
244283
if (build[1] && actualHash !== build[1])
245284
throw new Error(`Mismatch hashes. Expected ${build[1]}, got ${actualHash}`);
246285

@@ -305,6 +344,14 @@ export async function installVersion(installTarget: string, locator: Locator, {s
305344
};
306345
}
307346

347+
async function renameSafe(oldPath: fs.PathLike, newPath: fs.PathLike) {
348+
if (process.platform === `win32`) {
349+
await renameUnderWindows(oldPath, newPath);
350+
} else {
351+
await fs.promises.rename(oldPath, newPath);
352+
}
353+
}
354+
308355
async function renameUnderWindows(oldPath: fs.PathLike, newPath: fs.PathLike) {
309356
// Windows malicious file analysis blocks files currently under analysis, so we need to wait for file release
310357
const retries = 5;

sources/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export function isSupportedPackageManager(value: string): value is SupportedPack
2525
export interface NpmRegistrySpec {
2626
type: `npm`;
2727
package: string;
28+
bin?: string;
2829
}
2930

3031
export interface UrlRegistrySpec {
@@ -59,6 +60,12 @@ export interface InstallSpec {
5960
hash: string;
6061
}
6162

63+
export interface DownloadSpec {
64+
tmpFolder: string;
65+
outputFile: string | null;
66+
hash: string;
67+
}
68+
6269
/**
6370
* The data structure found in config.json
6471
*/

tests/main.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -787,19 +787,19 @@ it(`should download yarn berry from custom registry`, async () => {
787787
process.env.COREPACK_ENABLE_DOWNLOAD_PROMPT = `1`;
788788

789789
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
790-
packageManager: `yarn@3.0.0`,
790+
packageManager: `yarn@3.0.0-rc.2+sha224.f83f6d1cbfac10ba6b516a62ccd2a72ccd857aa6c514d1cd7185ec60`,
791791
});
792792

793793
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
794794
exitCode: 0,
795-
stdout: `3.0.0\n`,
796-
stderr: `! Corepack is about to download https://registry.npmmirror.com/@yarnpkg/cli-dist/-/cli-dist-3.0.0.tgz\n`,
795+
stdout: `3.0.0-rc.2\n`,
796+
stderr: `! Corepack is about to download https://registry.npmmirror.com/@yarnpkg/cli-dist/-/cli-dist-3.0.0-rc.2.tgz\n`,
797797
});
798798

799799
// Should keep working with cache
800800
await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
801801
exitCode: 0,
802-
stdout: `3.0.0\n`,
802+
stdout: `3.0.0-rc.2\n`,
803803
stderr: ``,
804804
});
805805
});

tests/nocks.db

1.76 MB
Binary file not shown.

0 commit comments

Comments
 (0)