From f08346af4cad20f76dc74bff7117dacad485b870 Mon Sep 17 00:00:00 2001 From: "aghiles.benkaoudjt" Date: Wed, 30 Sep 2026 11:09:03 +0200 Subject: [PATCH] Fix read-only files in the content-addressed cache --- packages/zpm/src/linker/helpers.rs | 5 +++- .../features/contentAddressedIndex.test.ts | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/zpm/src/linker/helpers.rs b/packages/zpm/src/linker/helpers.rs index 9a99a723..bdbde0dd 100644 --- a/packages/zpm/src/linker/helpers.rs +++ b/packages/zpm/src/linker/helpers.rs @@ -469,9 +469,12 @@ fn link_into_cas(target_path: &Path, data: &[u8], mode: u32, index_root: &Path) if needs_rewrite { // Write through the existing path: cross-project hardlinks // inherit the repair without losing inode identity. + if index_path.fs_exists() { + index_path.fs_set_permissions(Permissions::from_mode(mode_bits | 0o200))?; + } index_path.fs_write(data)?; - index_path.fs_set_permissions(Permissions::from_mode(mode_bits))?; set_safe_mtime(&index_path)?; + index_path.fs_set_permissions(Permissions::from_mode(mode_bits))?; } ensure_hardlink(target_path, &index_path) diff --git a/tests/acceptance-tests/pkg-tests-specs/sources/features/contentAddressedIndex.test.ts b/tests/acceptance-tests/pkg-tests-specs/sources/features/contentAddressedIndex.test.ts index bbb4b964..ef08bdf7 100644 --- a/tests/acceptance-tests/pkg-tests-specs/sources/features/contentAddressedIndex.test.ts +++ b/tests/acceptance-tests/pkg-tests-specs/sources/features/contentAddressedIndex.test.ts @@ -1,8 +1,35 @@ import {Filename, ppath, xfs} from '@yarnpkg/fslib'; +import {fs as fsUtils} from 'pkg-tests-core'; describe(`Features`, () => { describe(`Content-Addressed Index`, () => { if (process.platform !== `win32`) { + it(`should install and repair read-only files without changing their permissions`, + makeTemporaryEnv({ + dependencies: {readonly: `file:./readonly.tgz`}, + }, {nodeLinker: `pnpm`}, async ({path, run}) => { + const fixture = ppath.join(path, `fixture`); + await xfs.mkdirPromise(fixture); + await xfs.writeJsonPromise(ppath.join(fixture, Filename.manifest), {name: `readonly`, version: `1.0.0`}); + await xfs.writeFilePromise(ppath.join(fixture, `index.js`), `original`); + await xfs.chmodPromise(ppath.join(fixture, `index.js`), 0o555); + await fsUtils.packToFile(ppath.join(path, `readonly.tgz`), fixture, {virtualPath: ppath.resolve(`/package`)}); + + await run(`install`); + const installed = ppath.join(path, `node_modules/readonly/index.js`); + expect((await xfs.statPromise(installed)).mode & 0o777).toEqual(0o555); + + // A failed install or external modification can leave a read-only entry needing repair. + await xfs.chmodPromise(installed, 0o755); + await xfs.writeFilePromise(installed, `modified`); + await xfs.chmodPromise(installed, 0o555); + await run(`install`, `--force`); + + await expect(xfs.readFilePromise(installed, `utf8`)).resolves.toEqual(`original`); + expect((await xfs.statPromise(installed)).mode & 0o777).toEqual(0o555); + }), + ); + test( `it should preserve executable mode when installing`, makeTemporaryEnv({