diff --git a/.changeset/funny-wolves-enjoy.md b/.changeset/funny-wolves-enjoy.md new file mode 100644 index 0000000000..806fbc4afd --- /dev/null +++ b/.changeset/funny-wolves-enjoy.md @@ -0,0 +1,6 @@ +--- +"pnpm": major +"@pnpm/core": major +--- + +Changed the hash stored in the `packageExtensionsChecksum` field of `pnpm-lock.yaml` to SHA256. diff --git a/.changeset/happy-cheetahs-grab.md b/.changeset/happy-cheetahs-grab.md new file mode 100644 index 0000000000..c11d7f820f --- /dev/null +++ b/.changeset/happy-cheetahs-grab.md @@ -0,0 +1,5 @@ +--- +"pnpm": major +--- + +Use an SHA256 hash for the side effects cache keys. diff --git a/.changeset/soft-wasps-drum.md b/.changeset/soft-wasps-drum.md new file mode 100644 index 0000000000..a29d0ae2f6 --- /dev/null +++ b/.changeset/soft-wasps-drum.md @@ -0,0 +1,5 @@ +--- +"@pnpm/npm-resolver": major +--- + +Use SHA256 to encode the package name of a package that has upper case letters in its name. diff --git a/.changeset/strange-students-whisper.md b/.changeset/strange-students-whisper.md new file mode 100644 index 0000000000..a1c66c7eeb --- /dev/null +++ b/.changeset/strange-students-whisper.md @@ -0,0 +1,5 @@ +--- +"@pnpm/crypto.object-hasher": major +--- + +Use SHA256 encoded in base64 to hash objects. diff --git a/crypto/object-hasher/src/index.ts b/crypto/object-hasher/src/index.ts index d2f9945e68..fd0dbfb399 100644 --- a/crypto/object-hasher/src/index.ts +++ b/crypto/object-hasher/src/index.ts @@ -3,19 +3,20 @@ // avoiding "Invalid string length" errors. import hash from 'object-hash' -const defaultOptions: hash.BaseOptions = { +const defaultOptions: hash.NormalOption = { respectType: false, - algorithm: 'sha1', + algorithm: 'sha256', + encoding: 'base64', } -const withoutSortingOptions: hash.BaseOptions = { +const withoutSortingOptions: hash.NormalOption = { ...defaultOptions, unorderedArrays: false, unorderedObjects: false, unorderedSets: false, } -const withSortingOptions: hash.BaseOptions = { +const withSortingOptions: hash.NormalOption = { ...defaultOptions, unorderedArrays: true, unorderedObjects: true, @@ -24,8 +25,8 @@ const withSortingOptions: hash.BaseOptions = { function hashUnknown (object: unknown, options: hash.BaseOptions): string { if (object === undefined) { - // '0'.repeat(40) to match the length of other returned sha1 hashes. - return '0000000000000000000000000000000000000000' + // '0'.repeat(44) to match the length of other returned sha1 hashes. + return '00000000000000000000000000000000000000000000' } return hash(object, options) } diff --git a/crypto/object-hasher/test/index.ts b/crypto/object-hasher/test/index.ts index 40dd99bb41..9103b710c5 100644 --- a/crypto/object-hasher/test/index.ts +++ b/crypto/object-hasher/test/index.ts @@ -3,8 +3,8 @@ import { hashObject, hashObjectWithoutSorting } from '@pnpm/crypto.object-hasher describe('hashObject', () => { const hash = hashObject it('creates a hash', () => { - expect(hash({ b: 1, a: 2 })).toEqual('e3d3f89836fac144779e57d0e831efd06336036b') - expect(hash(undefined)).toEqual('0000000000000000000000000000000000000000') + expect(hash({ b: 1, a: 2 })).toEqual('48AVoXIXcTKcnHt8qVKp5vNw4gyOB5VfztHwtYBRcAQ=') + expect(hash(undefined)).toEqual('00000000000000000000000000000000000000000000') }) it('sorts', () => { expect(hash({ b: 1, a: 2 })).toEqual(hash({ a: 2, b: 1 })) @@ -15,8 +15,8 @@ describe('hashObject', () => { describe('hashObjectWithoutSorting', () => { const hash = hashObjectWithoutSorting it('creates a hash', () => { - expect(hash({ b: 1, a: 2 })).toEqual('dd34c1644a1d52da41808e5c1e6849829ef77999') - expect(hash(undefined)).toEqual('0000000000000000000000000000000000000000') + expect(hash({ b: 1, a: 2 })).toEqual('mh+rYklpd1DBj/dg6dnG+yd8BQhU2UiUoRMSXjPV1JA=') + expect(hash(undefined)).toEqual('00000000000000000000000000000000000000000000') }) it('does not sort', () => { expect(hash({ b: 1, a: 2 })).not.toEqual(hash({ a: 2, b: 1 })) diff --git a/pkg-manager/core/package.json b/pkg-manager/core/package.json index dc3030903b..74a198bd31 100644 --- a/pkg-manager/core/package.json +++ b/pkg-manager/core/package.json @@ -24,6 +24,7 @@ "@pnpm/constants": "workspace:*", "@pnpm/core-loggers": "workspace:*", "@pnpm/crypto.hash": "workspace:*", + "@pnpm/crypto.object-hasher": "workspace:*", "@pnpm/dependency-path": "workspace:*", "@pnpm/deps.graph-sequencer": "workspace:*", "@pnpm/error": "workspace:*", @@ -72,15 +73,13 @@ "path-exists": "catalog:", "ramda": "catalog:", "run-groups": "catalog:", - "semver": "catalog:", - "sort-keys": "catalog:" + "semver": "catalog:" }, "devDependencies": { "@pnpm/assert-project": "workspace:*", "@pnpm/assert-store": "workspace:*", "@pnpm/client": "workspace:*", "@pnpm/core": "workspace:*", - "@pnpm/crypto.object-hasher": "workspace:*", "@pnpm/git-utils": "workspace:*", "@pnpm/lockfile.types": "workspace:*", "@pnpm/logger": "workspace:*", diff --git a/pkg-manager/core/src/install/index.test.ts b/pkg-manager/core/src/install/index.test.ts deleted file mode 100644 index 16b7f7afd5..0000000000 --- a/pkg-manager/core/src/install/index.test.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { createObjectChecksum } from './index' - -function assets () { - const sorted = { - abc: { - a: 0, - b: [0, 1, 2], - c: null, - }, - def: { - foo: 'bar', - hello: 'world', - }, - } as const - - const unsorted1 = { - abc: { - b: [0, 1, 2], - a: 0, - c: null, - }, - def: { - hello: 'world', - foo: 'bar', - }, - } as const - - const unsorted2 = { - def: { - foo: 'bar', - hello: 'world', - }, - abc: { - a: 0, - b: [0, 1, 2], - c: null, - }, - } as const - - const unsorted3 = { - def: { - hello: 'world', - foo: 'bar', - }, - abc: { - b: [0, 1, 2], - a: 0, - c: null, - }, - } as const - - return { sorted, unsorted1, unsorted2, unsorted3 } as const -} - -test('createObjectChecksum', () => { - const { sorted, unsorted1, unsorted2, unsorted3 } = assets() - expect(createObjectChecksum(unsorted1)).toBe(createObjectChecksum(sorted)) - expect(createObjectChecksum(unsorted2)).toBe(createObjectChecksum(sorted)) - expect(createObjectChecksum(unsorted3)).toBe(createObjectChecksum(sorted)) -}) diff --git a/pkg-manager/core/src/install/index.ts b/pkg-manager/core/src/install/index.ts index d3c69289e4..3d5725fd97 100644 --- a/pkg-manager/core/src/install/index.ts +++ b/pkg-manager/core/src/install/index.ts @@ -1,4 +1,3 @@ -import crypto from 'crypto' import path from 'path' import { buildModules, type DepsStateCache, linkBinsOfDependencies } from '@pnpm/build-modules' import { createAllowBuildFunction } from '@pnpm/builder.policy' @@ -15,6 +14,7 @@ import { summaryLogger, } from '@pnpm/core-loggers' import { createHexHashFromFile } from '@pnpm/crypto.hash' +import { hashObject } from '@pnpm/crypto.object-hasher' import { PnpmError } from '@pnpm/error' import { getContext, type PnpmContext } from '@pnpm/get-context' import { headlessInstall, type InstallationResultStats } from '@pnpm/headless' @@ -78,7 +78,6 @@ import equals from 'ramda/src/equals' import isEmpty from 'ramda/src/isEmpty' import pipeWith from 'ramda/src/pipeWith' import props from 'ramda/src/props' -import sortKeys from 'sort-keys' import { parseWantedDependencies } from '../parseWantedDependencies' import { removeDeps } from '../uninstall/removeDeps' import { @@ -328,7 +327,7 @@ export async function mutateModules ( } ) } - const packageExtensionsChecksum = isEmpty(opts.packageExtensions ?? {}) ? undefined : createObjectChecksum(opts.packageExtensions!) + const packageExtensionsChecksum = isEmpty(opts.packageExtensions ?? {}) ? undefined : `sha256-${hashObject(opts.packageExtensions!)}` const pnpmfileChecksum = await opts.hooks.calculatePnpmfileChecksum?.() const patchedDependencies = opts.ignorePackageManifest ? ctx.wantedLockfile.patchedDependencies @@ -818,11 +817,6 @@ function getOutdatedLockfileSetting ( return null } -export function createObjectChecksum (obj: Record): string { - const s = JSON.stringify(sortKeys(obj, { deep: true })) - return crypto.createHash('md5').update(s).digest('hex') -} - function cacheExpired (prunedAt: string, maxAgeInMinutes: number): boolean { return ((Date.now() - new Date(prunedAt).valueOf()) / (1000 * 60)) > maxAgeInMinutes } diff --git a/pkg-manager/core/test/install/packageExtensions.ts b/pkg-manager/core/test/install/packageExtensions.ts index a21b61a215..df635cda87 100644 --- a/pkg-manager/core/test/install/packageExtensions.ts +++ b/pkg-manager/core/test/install/packageExtensions.ts @@ -1,12 +1,16 @@ import { PnpmError } from '@pnpm/error' import { prepareEmpty } from '@pnpm/prepare' import { addDependenciesToPackage, mutateModulesInSingleProject, install } from '@pnpm/core' +import { hashObject as _hashObject } from '@pnpm/crypto.object-hasher' import { type ProjectRootDir, type PackageExtension, type ProjectManifest } from '@pnpm/types' -import { createObjectChecksum } from '../../lib/install/index' import { testDefaults, } from '../utils' +function hashObject (obj: Record): string { + return `sha256-${_hashObject(obj)}` +} + test('manifests are extended with fields specified by packageExtensions', async () => { const project = prepareEmpty() @@ -26,7 +30,7 @@ test('manifests are extended with fields specified by packageExtensions', async { const lockfile = project.readLockfile() expect(lockfile.snapshots['is-positive@1.0.0'].dependencies?.['@pnpm.e2e/bar']).toBe('100.1.0') - expect(lockfile.packageExtensionsChecksum).toStrictEqual(createObjectChecksum({ + expect(lockfile.packageExtensionsChecksum).toStrictEqual(hashObject({ 'is-positive': { dependencies: { '@pnpm.e2e/bar': '100.1.0', @@ -48,7 +52,7 @@ test('manifests are extended with fields specified by packageExtensions', async { const lockfile = project.readLockfile() expect(lockfile.snapshots['is-positive@1.0.0'].dependencies?.['@pnpm.e2e/foobar']).toBe('100.0.0') - expect(lockfile.packageExtensionsChecksum).toStrictEqual(createObjectChecksum({ + expect(lockfile.packageExtensionsChecksum).toStrictEqual(hashObject({ 'is-positive': { dependencies: { '@pnpm.e2e/bar': '100.1.0', @@ -68,7 +72,7 @@ test('manifests are extended with fields specified by packageExtensions', async { const lockfile = project.readLockfile() - expect(lockfile.packageExtensionsChecksum).toStrictEqual(createObjectChecksum({ + expect(lockfile.packageExtensionsChecksum).toStrictEqual(hashObject({ 'is-positive': { dependencies: { '@pnpm.e2e/bar': '100.1.0', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 656be44594..6e75fb9e72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3947,6 +3947,9 @@ importers: '@pnpm/crypto.hash': specifier: workspace:* version: link:../../crypto/hash + '@pnpm/crypto.object-hasher': + specifier: workspace:* + version: link:../../crypto/object-hasher '@pnpm/dependency-path': specifier: workspace:* version: link:../../packages/dependency-path @@ -4097,9 +4100,6 @@ importers: semver: specifier: 'catalog:' version: 7.6.2 - sort-keys: - specifier: 'catalog:' - version: 4.2.0 devDependencies: '@pnpm/assert-project': specifier: workspace:* @@ -4113,9 +4113,6 @@ importers: '@pnpm/core': specifier: workspace:* version: 'link:' - '@pnpm/crypto.object-hasher': - specifier: workspace:* - version: link:../../crypto/object-hasher '@pnpm/git-utils': specifier: workspace:* version: link:../../packages/git-utils @@ -6126,6 +6123,9 @@ importers: '@pnpm/core-loggers': specifier: workspace:* version: link:../../packages/core-loggers + '@pnpm/crypto.hash': + specifier: workspace:* + version: link:../../crypto/hash '@pnpm/error': specifier: workspace:* version: link:../../packages/error diff --git a/resolving/npm-resolver/package.json b/resolving/npm-resolver/package.json index c92ba927da..71efd2e0aa 100644 --- a/resolving/npm-resolver/package.json +++ b/resolving/npm-resolver/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@pnpm/core-loggers": "workspace:*", + "@pnpm/crypto.hash": "workspace:*", "@pnpm/error": "workspace:*", "@pnpm/fetching-types": "workspace:*", "@pnpm/graceful-fs": "workspace:*", diff --git a/resolving/npm-resolver/src/pickPackage.ts b/resolving/npm-resolver/src/pickPackage.ts index 7a647d8c1f..54d7696254 100644 --- a/resolving/npm-resolver/src/pickPackage.ts +++ b/resolving/npm-resolver/src/pickPackage.ts @@ -1,6 +1,6 @@ -import crypto from 'crypto' import { promises as fs } from 'fs' import path from 'path' +import { createHexHash } from '@pnpm/crypto.hash' import { PnpmError } from '@pnpm/error' import { logger } from '@pnpm/logger' import gfs from '@pnpm/graceful-fs' @@ -268,7 +268,7 @@ function clearMeta (pkg: PackageMeta): PackageMeta { function encodePkgName (pkgName: string): string { if (pkgName !== pkgName.toLowerCase()) { - return `${pkgName}_${crypto.createHash('md5').update(pkgName).digest('hex')}` + return `${pkgName}_${createHexHash(pkgName)}` } return pkgName } diff --git a/resolving/npm-resolver/test/index.ts b/resolving/npm-resolver/test/index.ts index 544a875622..1a601f9095 100644 --- a/resolving/npm-resolver/test/index.ts +++ b/resolving/npm-resolver/test/index.ts @@ -1,6 +1,7 @@ /// import fs from 'fs' import path from 'path' +import { createHexHash } from '@pnpm/crypto.hash' import { PnpmError } from '@pnpm/error' import { createFetchFromRegistry } from '@pnpm/fetch' import { @@ -110,7 +111,7 @@ test('resolveFromNpm() should save metadata to a unique file when the package na // The resolve function does not wait for the package meta cache file to be saved // so we must delay for a bit in order to read it - const meta = await retryLoadJsonFile(path.join(cacheDir, 'metadata/registry.npmjs.org/JSON_0ecd11c1d7a287401d148a23bbd7a2f8.json')) // eslint-disable-line @typescript-eslint/no-explicit-any + const meta = await retryLoadJsonFile(path.join(cacheDir, `metadata/registry.npmjs.org/JSON_${createHexHash('JSON')}.json`)) // eslint-disable-line @typescript-eslint/no-explicit-any expect(meta.name).toBeTruthy() expect(meta.versions).toBeTruthy() expect(meta['dist-tags']).toBeTruthy() diff --git a/resolving/npm-resolver/tsconfig.json b/resolving/npm-resolver/tsconfig.json index 588439ffb8..c7703969b3 100644 --- a/resolving/npm-resolver/tsconfig.json +++ b/resolving/npm-resolver/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../../__utils__/test-fixtures" }, + { + "path": "../../crypto/hash" + }, { "path": "../../fs/graceful-fs" },