Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Skip untracked files whose basename is a Windows reserved device name (e.g. `nul`, `con`, `aux`, `com1`-`com9`, `lpt1`-`lpt9`) when computing repo state on Windows. `git hash-object` cannot open such paths and otherwise aborts the entire repo-state calculation.",
"type": "patch",
"packageName": "@rushstack/package-deps-hash"
}
],
"packageName": "@rushstack/package-deps-hash",
"email": "sshaurya914@gmail.com"
}
30 changes: 30 additions & 0 deletions libraries/package-deps-hash/src/getRepoState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ const STANDARD_GIT_OPTIONS: readonly string[] = [
'maintenance.auto=false'
];

// Windows reserved device names (case-insensitive). A file whose final path segment matches one
// of these (with or without an extension) cannot be opened by name on Windows, so passing it to
// `git hash-object` aborts the process. Such files are typically untracked artifacts left behind
// by tooling (e.g. stray `nul` from a shell redirect).
const WINDOWS_RESERVED_BASENAMES: ReadonlySet<string> = new Set([
'CON', 'PRN', 'AUX', 'NUL',
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
]);

/**
* Returns `true` if `filePath`'s final path segment is a Windows reserved device name
* (with or without an extension), case-insensitively. Exported for tests.
* @internal
*/
export function isWindowsReservedPath(filePath: string): boolean {
const lastSlash: number = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
const basename: string = lastSlash >= 0 ? filePath.slice(lastSlash + 1) : filePath;
const dot: number = basename.indexOf('.');
const stem: string = (dot >= 0 ? basename.slice(0, dot) : basename).toUpperCase();
return WINDOWS_RESERVED_BASENAMES.has(stem);
}

const OBJECTMODE_SUBMODULE: '160000' = '160000';
const OBJECTMODE_SYMLINK: '120000' = '120000';
const OBJECTMODE_FILE_NONEXECUTABLE: '100644' = '100644';
Expand Down Expand Up @@ -480,8 +503,15 @@ export async function getDetailedRepoStateAsync(

const [{ files, symlinks }, locallyModified] = await Promise.all([statePromise, locallyModifiedPromise]);

const isWindows: boolean = process.platform === 'win32';
for (const [filePath, exists] of locallyModified) {
if (exists && !symlinks.has(filePath)) {
// Skip Windows reserved device names. `git hash-object` cannot open them and would abort
// the entire repo-state computation. These are almost always stray artifacts (e.g. a `nul`
// file produced by a misdirected shell redirect) rather than meaningful inputs.
if (isWindows && isWindowsReservedPath(filePath)) {
continue;
}
yield filePath;
} else {
files.delete(filePath);
Expand Down
31 changes: 30 additions & 1 deletion libraries/package-deps-hash/src/test/getRepoState.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { parseGitStatus, parseGitVersion } from '../getRepoState';
import { isWindowsReservedPath, parseGitStatus, parseGitVersion } from '../getRepoState';

describe(parseGitVersion.name, () => {
it('Can parse valid git version responses', () => {
Expand Down Expand Up @@ -87,3 +87,32 @@ describe(parseGitStatus.name, () => {
expect(result.get(files[2])).toEqual(true);
});
});

describe(isWindowsReservedPath.name, () => {
it('detects bare reserved basenames', () => {
expect(isWindowsReservedPath('nul')).toBe(true);
expect(isWindowsReservedPath('NUL')).toBe(true);
expect(isWindowsReservedPath('Con')).toBe(true);
expect(isWindowsReservedPath('com1')).toBe(true);
expect(isWindowsReservedPath('LPT9')).toBe(true);
});

it('detects reserved basenames with an extension', () => {
expect(isWindowsReservedPath('nul.txt')).toBe(true);
expect(isWindowsReservedPath('aux.log.bak')).toBe(true);
});

it('matches the final segment of nested paths with either slash style', () => {
expect(isWindowsReservedPath('apps/admin/nul')).toBe(true);
expect(isWindowsReservedPath('apps\\admin\\nul')).toBe(true);
expect(isWindowsReservedPath('apps/admin/sub/CON.tmp')).toBe(true);
});

it('does not match non-reserved names', () => {
expect(isWindowsReservedPath('null')).toBe(false);
expect(isWindowsReservedPath('console.ts')).toBe(false);
expect(isWindowsReservedPath('com.ts')).toBe(false);
expect(isWindowsReservedPath('lpt10')).toBe(false);
expect(isWindowsReservedPath('packages/nul-suffix/index.ts')).toBe(false);
});
});
Loading