[GH-ISSUE #2619] fix(artifacts): inventory rejects supported literal backslash names #934

Closed
opened 2026-10-15 18:29:42 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 13, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2619

Summary

On a POSIX host, inventory of a directory or packed ASAR fails when a regular member contains a literal backslash in its name. The owning readers preserve that supported filename, but inventory rejects it before returning its graph.

Steps to reproduce

Run on a POSIX host with installed lockfile dependencies. Save the following test as src/artifacts/literal-member.probe.test.ts and run npm run test:focused -- src/artifacts/literal-member.probe.test.ts. It creates a regular file, packs it into an ASAR, checks both readers, and observes both inventory rejections. Its normal-name control inventories successfully.

import { mkdir, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { createPackage } from '@electron/asar';
import { expect, it } from 'vitest';
import { createTestTempDirectory } from '../../tests/fixtures/temporaryDirectory.js';
import { inventoryArtifact } from '../../tests/fixtures/artifactInventory.js';
import { DirectoryArtifactReader } from './DirectoryArtifactReader.js';
import { AsarArtifactReader } from './AsarArtifactReader.js';
it.skipIf(process.platform==='win32')('reader preserves literal backslash while inventory rejects it', async () => {
 const root=await createTestTempDirectory('literal-member-');
 const source=join(root,'source'); await mkdir(source);
 await writeFile(join(source,'literal\\name.js'),'export {};\n');
 const archive=join(root,'input.asar'); await createPackage(source,archive);
 for(const [path,reader] of [[source,new DirectoryArtifactReader(source)],[archive,new AsarArtifactReader(archive)]] as const) {
   try {
     const names=[];
     for await(const entry of reader.entries()) names.push(entry.path);
     expect(names).toContain('literal\\name.js');
   } finally { await reader.close(); }
   await expect(inventoryArtifact(path)).rejects.toMatchObject({reason:'path',message:expect.stringContaining('Artifact path is absolute or unsafe')});
 }
 const control=join(root,'control'); await mkdir(control);
 await writeFile(join(control,'hello.txt:hidden'),'ok');
 expect((await inventoryArtifact(control)).occurrences.some(o=>o.logical_path==='hello.txt:hidden')).toBe(true);
});

Expected behavior

Inventory should preserve the literal filename as the same logical occurrence exposed by the POSIX reader. A destination’s inability to materialize a logical name should be reported at the applicable output boundary while existing path-containment protections remain intact.

Actual behavior

The focused probe observed literal\name.js in both reader listings. Inventory rejected each input with an ArtifactReaderFailure whose reason was path and whose message began Artifact path is absolute or unsafe. Neither operation returned an inventory graph. The hello.txt:hidden control produced its occurrence.

Evidence

  • Expected source: src/artifacts/AsarArtifactReader.ts:708-725 requires preserving literal POSIX backslashes; tests/boundary/filesystem/asarInventoryFreshness.test.ts:90-115 verifies that reader contract; AGENTS.md, Architecture and Cleanup, requires preservation of owner facts.
  • Failure source: src/artifacts/inventory/scanReader.ts:133-144 normalizes each reader path before creating an occurrence; src/artifacts/ArtifactPaths.ts:26-40 rejects all backslashes.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Restoration check

The same directory and ASAR must inventory successfully with the exact logical filename and correct file digest. The ordinary-name control must still inventory. Existing containment, collision and destination compatibility checks must keep their guarantees.

Runtime or environment

Source revision 8b181716aca2cb467be499cba226abfb5ae19141, Node v26.10.0, npm 11.19.1, Vitest 5.0.3, and the installed lockfile dependencies.

Triage assessment

  • Impact level: P1
  • Assessment status: supported
  • Impact basis: A supported POSIX filename prevents the core directory or ASAR inventory workflow from returning any result, and no available workaround preserves the exact selected input.
  • Workaround status: none
  • Workaround basis: The shared inventory path rejects the owner-supplied name. Renaming it changes the input identity instead of preserving the requested inventory.
Originally created by @coygeek on GitHub (Oct 13, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2619 ## Summary On a POSIX host, inventory of a directory or packed ASAR fails when a regular member contains a literal backslash in its name. The owning readers preserve that supported filename, but inventory rejects it before returning its graph. ## Steps to reproduce Run on a POSIX host with installed lockfile dependencies. Save the following test as `src/artifacts/literal-member.probe.test.ts` and run `npm run test:focused -- src/artifacts/literal-member.probe.test.ts`. It creates a regular file, packs it into an ASAR, checks both readers, and observes both inventory rejections. Its normal-name control inventories successfully. ```ts import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { createPackage } from '@electron/asar'; import { expect, it } from 'vitest'; import { createTestTempDirectory } from '../../tests/fixtures/temporaryDirectory.js'; import { inventoryArtifact } from '../../tests/fixtures/artifactInventory.js'; import { DirectoryArtifactReader } from './DirectoryArtifactReader.js'; import { AsarArtifactReader } from './AsarArtifactReader.js'; it.skipIf(process.platform==='win32')('reader preserves literal backslash while inventory rejects it', async () => { const root=await createTestTempDirectory('literal-member-'); const source=join(root,'source'); await mkdir(source); await writeFile(join(source,'literal\\name.js'),'export {};\n'); const archive=join(root,'input.asar'); await createPackage(source,archive); for(const [path,reader] of [[source,new DirectoryArtifactReader(source)],[archive,new AsarArtifactReader(archive)]] as const) { try { const names=[]; for await(const entry of reader.entries()) names.push(entry.path); expect(names).toContain('literal\\name.js'); } finally { await reader.close(); } await expect(inventoryArtifact(path)).rejects.toMatchObject({reason:'path',message:expect.stringContaining('Artifact path is absolute or unsafe')}); } const control=join(root,'control'); await mkdir(control); await writeFile(join(control,'hello.txt:hidden'),'ok'); expect((await inventoryArtifact(control)).occurrences.some(o=>o.logical_path==='hello.txt:hidden')).toBe(true); }); ``` ## Expected behavior Inventory should preserve the literal filename as the same logical occurrence exposed by the POSIX reader. A destination’s inability to materialize a logical name should be reported at the applicable output boundary while existing path-containment protections remain intact. ## Actual behavior The focused probe observed `literal\name.js` in both reader listings. Inventory rejected each input with an `ArtifactReaderFailure` whose reason was `path` and whose message began `Artifact path is absolute or unsafe`. Neither operation returned an inventory graph. The `hello.txt:hidden` control produced its occurrence. ## Evidence - Expected source: `src/artifacts/AsarArtifactReader.ts:708-725` requires preserving literal POSIX backslashes; `tests/boundary/filesystem/asarInventoryFreshness.test.ts:90-115` verifies that reader contract; `AGENTS.md`, Architecture and Cleanup, requires preservation of owner facts. - Failure source: `src/artifacts/inventory/scanReader.ts:133-144` normalizes each reader path before creating an occurrence; `src/artifacts/ArtifactPaths.ts:26-40` rejects all backslashes. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ## Restoration check The same directory and ASAR must inventory successfully with the exact logical filename and correct file digest. The ordinary-name control must still inventory. Existing containment, collision and destination compatibility checks must keep their guarantees. ## Runtime or environment Source revision `8b181716aca2cb467be499cba226abfb5ae19141`, Node v26.10.0, npm 11.19.1, Vitest 5.0.3, and the installed lockfile dependencies. ## Triage assessment - Impact level: P1 - Assessment status: supported - Impact basis: A supported POSIX filename prevents the core directory or ASAR inventory workflow from returning any result, and no available workaround preserves the exact selected input. - Workaround status: none - Workaround basis: The shared inventory path rejects the owner-supplied name. Renaming it changes the input identity instead of preserving the requested inventory.
bsakaguchi 2026-10-15 18:29:42 +00:00
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Addressed on main in ec271121e.

Literal POSIX backslash names remain literal through directory and ASAR inventory and JavaScript graph consumers.

Validation: 17 literal-backslash boundary cases passed for inventory and graph projections.

Verification scope: Windows separator semantics remain platform-specific.

<!-- gh-comment-id:6158658838 --> @morluto commented on GitHub (Oct 14, 2026): Addressed on main in [ec271121e](https://github.com/morluto/rea/commit/ec271121e). Literal POSIX backslash names remain literal through directory and ASAR inventory and JavaScript graph consumers. Validation: 17 literal-backslash boundary cases passed for inventory and graph projections. Verification scope: Windows separator semantics remain platform-specific.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#934
No description provided.