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
20 changes: 17 additions & 3 deletions scripts/generate-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ interface PackageJson {
macOSTemplateVersion: string;
}

const VERSION_REGEX = /^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Version regex rejects valid semver pre-release hyphens

Medium Severity

VERSION_REGEX uses [a-zA-Z0-9.] for pre-release and build-metadata character classes, which doesn't include -. The semver spec explicitly allows hyphens in these identifiers (e.g., 2.4.0-beta-1, 1.0.0-rc-1, 1.0.0+build-42). The validateVersion function will throw on valid semver versions containing hyphens in pre-release or build metadata, blocking releases that use common naming conventions like beta-1 or rc-2.

Fix in Cursor Fix in Web


function validateVersion(name: string, value: string): void {
if (!VERSION_REGEX.test(value)) {
throw new Error(
`Invalid ${name} in package.json: ${JSON.stringify(value)}. Expected a version string.`,
);
}
}

async function main(): Promise<void> {
const repoRoot = process.cwd();
const packagePath = path.join(repoRoot, 'package.json');
Expand All @@ -15,10 +25,14 @@ async function main(): Promise<void> {
const raw = await readFile(packagePath, 'utf8');
const pkg = JSON.parse(raw) as PackageJson;

validateVersion('version', pkg.version);
validateVersion('iOSTemplateVersion', pkg.iOSTemplateVersion);
validateVersion('macOSTemplateVersion', pkg.macOSTemplateVersion);

const content =
`export const version = '${pkg.version}';\n` +
`export const iOSTemplateVersion = '${pkg.iOSTemplateVersion}';\n` +
`export const macOSTemplateVersion = '${pkg.macOSTemplateVersion}';\n`;
`export const version = ${JSON.stringify(pkg.version)};\n` +
`export const iOSTemplateVersion = ${JSON.stringify(pkg.iOSTemplateVersion)};\n` +
`export const macOSTemplateVersion = ${JSON.stringify(pkg.macOSTemplateVersion)};\n`;

await writeFile(versionPath, content, 'utf8');
}
Expand Down
13 changes: 3 additions & 10 deletions src/utils/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { spawn } from 'child_process';
import { createWriteStream, existsSync } from 'fs';
import { tmpdir as osTmpdir } from 'os';
import { log } from './logger.ts';
import { shellEscapeArg } from './shell-escape.ts';
import type { FileSystemExecutor } from './FileSystemExecutor.ts';
import type { CommandExecutor, CommandResponse, CommandExecOptions } from './CommandExecutor.ts';

Expand Down Expand Up @@ -41,16 +42,8 @@ async function defaultExecutor(
let escapedCommand = command;
if (useShell) {
// For shell execution, we need to format as ['/bin/sh', '-c', 'full command string']
const commandString = command
.map((arg) => {
// Shell metacharacters that require quoting: space, quotes, equals, dollar, backticks, semicolons, pipes, etc.
if (/[\s,"'=$`;&|<>(){}[\]\\*?~]/.test(arg) && !/^".*"$/.test(arg)) {
// Escape all quotes and backslashes, then wrap in double quotes
return `"${arg.replace(/(["\\])/g, '\\$1')}"`;
}
return arg;
})
.join(' ');
// Use POSIX single-quote escaping for each argument to prevent injection
const commandString = command.map((arg) => shellEscapeArg(arg)).join(' ');

escapedCommand = ['/bin/sh', '-c', commandString];
}
Expand Down
19 changes: 16 additions & 3 deletions src/utils/log_capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export interface LogSession {
*/
export type SubsystemFilter = 'app' | 'all' | 'swiftui' | string[];

/**
* Escape a string for safe use inside a double-quoted NSPredicate string literal.
* Backslash-escapes any backslashes and double quotes so the value cannot
* break out of the `"..."` context in predicates like `subsystem == "VALUE"`.
*/
function escapePredicateString(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

/**
* Build the predicate string for log filtering based on subsystem filter option.
*/
Expand All @@ -45,18 +54,22 @@ function buildLogPredicate(bundleId: string, subsystemFilter: SubsystemFilter):
return null;
}

const safeBundleId = escapePredicateString(bundleId);

if (subsystemFilter === 'app') {
return `subsystem == "${bundleId}"`;
return `subsystem == "${safeBundleId}"`;
}

if (subsystemFilter === 'swiftui') {
// Include both app logs and SwiftUI logs (for Self._printChanges())
return `subsystem == "${bundleId}" OR subsystem == "com.apple.SwiftUI"`;
return `subsystem == "${safeBundleId}" OR subsystem == "com.apple.SwiftUI"`;
}

// Custom array of subsystems - always include the app's bundle ID
const subsystems = new Set([bundleId, ...subsystemFilter]);
const predicates = Array.from(subsystems).map((s) => `subsystem == "${s}"`);
const predicates = Array.from(subsystems).map(
(s) => `subsystem == "${escapePredicateString(s)}"`,
);
return predicates.join(' OR ');
}

Expand Down
15 changes: 15 additions & 0 deletions src/utils/shell-escape.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* POSIX-safe shell argument escaping.
*
* Wraps a string in single quotes and escapes any embedded single quotes
* using the standard `'\''` technique. This is the safest way to pass
* arbitrary strings as arguments to `/bin/sh -c` commands.
*
* @param arg The argument to escape for safe shell interpolation
* @returns A single-quoted, safely escaped string
*/
export function shellEscapeArg(arg: string): string {
// Replace each single quote with: end current quote, escaped single quote, start new quote
// Then wrap the whole thing in single quotes
return "'" + arg.replace(/'/g, "'\\''") + "'";
}