fix(validation): add Windows reserved name validation and version format edge cases

- Fixed version regex to accept -N suffix (e.g., 6.6.80-0)
- Added .trim() to version input for whitespace handling
- Added Windows reserved device names (CON, PRN, AUX, NUL, COM1-9, LPT1-9) validation
- Updated binary-service, variant-service, and validation-service with checks
This commit is contained in:
kaitranntt
2026-01-05 00:00:58 -05:00
parent 3cdf42c2be
commit ae1847d901
5 changed files with 55 additions and 6 deletions
+4 -1
View File
@@ -5,7 +5,7 @@
* Extracted from api-command.ts for reuse and testability.
*/
import { isReservedName } from '../../config/reserved-names';
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
/**
* Validate API profile name
@@ -24,6 +24,9 @@ export function validateApiName(name: string): string | null {
if (isReservedName(name)) {
return `'${name}' is a reserved name`;
}
if (isWindowsReservedName(name)) {
return `'${name}' is a Windows reserved device name and cannot be used`;
}
return null;
}
+2 -2
View File
@@ -84,10 +84,10 @@ export async function checkLatestVersion(): Promise<LatestVersionResult> {
}
/**
* Validate version format
* Validate version format (supports X.Y.Z or X.Y.Z-N suffix)
*/
export function isValidVersionFormat(version: string): boolean {
return /^\d+\.\d+\.\d+$/.test(version);
return /^\d+\.\d+\.\d+(-\d+)?$/.test(version);
}
/**
+4 -1
View File
@@ -9,7 +9,7 @@ import * as os from 'os';
import * as path from 'path';
import { CLIProxyProfileName } from '../../auth/profile-detector';
import { CLIProxyProvider } from '../types';
import { isReservedName } from '../../config/reserved-names';
import { isReservedName, isWindowsReservedName } from '../../config/reserved-names';
import { isUnifiedMode } from '../../config/unified-config-loader';
import { deleteConfigForPort } from '../config-generator';
import { deleteSessionLockForPort } from '../session-tracker';
@@ -58,6 +58,9 @@ export function validateProfileName(name: string): string | null {
if (isReservedName(name)) {
return `'${name}' is a reserved name`;
}
if (isWindowsReservedName(name)) {
return `'${name}' is a Windows reserved device name and cannot be used`;
}
return null;
}
+4 -2
View File
@@ -619,13 +619,15 @@ export async function handleCliproxyCommand(args: string[]): Promise<void> {
const installIdx = args.indexOf('--install');
if (installIdx !== -1) {
const version = args[installIdx + 1];
let version = args[installIdx + 1];
if (!version || version.startsWith('-')) {
console.error(fail('Missing version argument for --install'));
console.error(' Usage: ccs cliproxy --install <version>');
console.error(' Example: ccs cliproxy --install 6.5.53');
console.error(' Example: ccs cliproxy --install 6.6.80-0');
process.exit(1);
}
// Strip leading 'v' prefix and whitespace (user may type " v6.6.80-0 ")
version = version.trim().replace(/^v/, '');
await handleInstallVersion(version, verbose);
return;
}
+41
View File
@@ -19,6 +19,35 @@ export const RESERVED_PROFILE_NAMES = [
export type ReservedProfileName = (typeof RESERVED_PROFILE_NAMES)[number];
/**
* Windows reserved device names - cannot be used as filenames on Windows.
* Case-insensitive on Windows filesystem.
*/
export const WINDOWS_RESERVED_NAMES = [
'CON',
'PRN',
'AUX',
'NUL',
'COM1',
'COM2',
'COM3',
'COM4',
'COM5',
'COM6',
'COM7',
'COM8',
'COM9',
'LPT1',
'LPT2',
'LPT3',
'LPT4',
'LPT5',
'LPT6',
'LPT7',
'LPT8',
'LPT9',
] as const;
/**
* Check if a name is reserved and cannot be used for user profiles.
* @param name - The profile name to check
@@ -28,6 +57,18 @@ export function isReservedName(name: string): boolean {
return RESERVED_PROFILE_NAMES.includes(name.toLowerCase() as ReservedProfileName);
}
/**
* Check if a name is a Windows reserved device name.
* These cause filesystem errors on Windows systems.
* @param name - The name to check
* @returns true if the name is a Windows reserved name
*/
export function isWindowsReservedName(name: string): boolean {
return WINDOWS_RESERVED_NAMES.includes(
name.toUpperCase() as (typeof WINDOWS_RESERVED_NAMES)[number]
);
}
/**
* Validate a profile name and throw if reserved.
* @param name - The profile name to validate