mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-03-25 09:25:47 +00:00
Addresses critical edge case where malformed app targets with multiple leading colons (e.g., "::::::::::::::::Finder") created empty app names that would match ALL system processes. This could potentially expose sensitive information or cause unintended system-wide captures. Key improvements: - Enhanced app target parsing to validate non-empty app names - Added fallback logic to extract valid app names from malformed inputs - Default to screen mode when all parts are empty (security-first approach) - Comprehensive test coverage for edge cases - Improved backward compatibility with hidden path parameters 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
167 lines
No EOL
5.9 KiB
TypeScript
167 lines
No EOL
5.9 KiB
TypeScript
import { ImageInput } from "../types/index.js";
|
|
import { Logger } from "pino";
|
|
import * as fs from "fs/promises";
|
|
import * as path from "path";
|
|
import * as os from "os";
|
|
|
|
export interface ResolvedImagePath {
|
|
effectivePath: string | undefined;
|
|
tempDirUsed: string | undefined;
|
|
}
|
|
|
|
export async function resolveImagePath(
|
|
input: ImageInput,
|
|
logger: Logger,
|
|
): Promise<ResolvedImagePath> {
|
|
// If input.path is provided, use it directly
|
|
if (input.path) {
|
|
return { effectivePath: input.path, tempDirUsed: undefined };
|
|
}
|
|
|
|
// Check if a temporary directory is required
|
|
// A temp dir is needed if:
|
|
// 1. A question is present
|
|
// 2. Format is explicitly set to 'data'
|
|
const needsTempDir = input.question || input.format === "data";
|
|
|
|
if (needsTempDir) {
|
|
// Create a temporary directory
|
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "peekaboo-img-"));
|
|
logger.debug({ tempPath: tempDir }, "Created temporary directory for capture");
|
|
return { effectivePath: tempDir, tempDirUsed: tempDir };
|
|
}
|
|
|
|
// Check for PEEKABOO_DEFAULT_SAVE_PATH environment variable
|
|
const defaultSavePath = process.env.PEEKABOO_DEFAULT_SAVE_PATH;
|
|
if (defaultSavePath) {
|
|
return { effectivePath: defaultSavePath, tempDirUsed: undefined };
|
|
}
|
|
|
|
// Final fallback: create a temporary directory
|
|
// This happens when: no path, no question, no explicit 'data' format, no env var
|
|
const fallbackTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "peekaboo-img-"));
|
|
logger.debug({ tempPath: fallbackTempDir }, "Created fallback temporary directory for capture");
|
|
return { effectivePath: fallbackTempDir, tempDirUsed: fallbackTempDir };
|
|
}
|
|
|
|
export function buildSwiftCliArgs(
|
|
input: ImageInput,
|
|
effectivePath: string | undefined,
|
|
swiftFormat?: string,
|
|
logger?: Logger,
|
|
): string[] {
|
|
const args = ["image"];
|
|
|
|
// Use provided format or derive from input
|
|
// Format validation is already handled by the schema preprocessor
|
|
const inputFormat = input.format || "png";
|
|
const actualFormat = swiftFormat || (inputFormat === "data" ? "png" : inputFormat);
|
|
|
|
// Create a logger if not provided (for backward compatibility)
|
|
const log = logger || {
|
|
warn: (_msg: unknown) => {},
|
|
error: (_msg: unknown) => {},
|
|
debug: (_msg: unknown) => {},
|
|
};
|
|
|
|
// Parse app_target to determine Swift CLI arguments
|
|
if (!input.app_target || input.app_target === "") {
|
|
// Omitted/empty: All screens
|
|
args.push("--mode", "screen");
|
|
} else if (input.app_target.startsWith("screen:")) {
|
|
// 'screen:INDEX': Specific display
|
|
const screenIndexStr = input.app_target.substring(7);
|
|
const screenIndex = parseInt(screenIndexStr, 10);
|
|
if (isNaN(screenIndex) || screenIndex < 0) {
|
|
log.warn(
|
|
{ screenIndex: screenIndexStr },
|
|
`Invalid screen index '${screenIndexStr}' in app_target, capturing all screens.`,
|
|
);
|
|
args.push("--mode", "screen");
|
|
} else {
|
|
args.push("--mode", "screen", "--screen-index", screenIndex.toString());
|
|
}
|
|
} else if (input.app_target.toLowerCase() === "frontmost") {
|
|
// 'frontmost': All windows of the frontmost app
|
|
log.warn(
|
|
"'frontmost' target requires determining current frontmost app, defaulting to screen mode",
|
|
);
|
|
args.push("--mode", "screen");
|
|
} else if (input.app_target.includes(":")) {
|
|
// 'AppName:WINDOW_TITLE:Title' or 'AppName:WINDOW_INDEX:Index'
|
|
const parts = input.app_target.split(":");
|
|
if (parts.length >= 3) {
|
|
const appName = parts[0].trim();
|
|
const specifierType = parts[1].trim();
|
|
const specifierValue = parts.slice(2).join(":"); // Handle colons in window titles
|
|
|
|
// Validate that we have a non-empty app name
|
|
if (!appName) {
|
|
log.warn(
|
|
{ app_target: input.app_target },
|
|
"Empty app name detected in app_target, treating as malformed",
|
|
);
|
|
// Try to find the first non-empty part as the app name
|
|
const nonEmptyParts = parts.filter(part => part.trim());
|
|
if (nonEmptyParts.length > 0) {
|
|
args.push("--app", nonEmptyParts[0].trim());
|
|
args.push("--mode", "multi");
|
|
} else {
|
|
// All parts are empty, default to screen mode
|
|
log.warn("All parts of app_target are empty, defaulting to screen mode");
|
|
args.push("--mode", "screen");
|
|
}
|
|
} else {
|
|
args.push("--app", appName);
|
|
args.push("--mode", "window");
|
|
|
|
if (specifierType.toUpperCase() === "WINDOW_TITLE") {
|
|
args.push("--window-title", specifierValue);
|
|
} else if (specifierType.toUpperCase() === "WINDOW_INDEX") {
|
|
args.push("--window-index", specifierValue);
|
|
} else {
|
|
log.warn(
|
|
{ specifierType },
|
|
"Unknown window specifier type, defaulting to main window",
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Malformed: treat as app name, but validate it's not empty
|
|
const cleanAppTarget = input.app_target.trim();
|
|
if (!cleanAppTarget || cleanAppTarget === ":".repeat(cleanAppTarget.length)) {
|
|
log.warn(
|
|
{ app_target: input.app_target },
|
|
"Malformed app_target with only colons or empty, defaulting to screen mode",
|
|
);
|
|
args.push("--mode", "screen");
|
|
} else {
|
|
log.warn(
|
|
{ app_target: input.app_target },
|
|
"Malformed window specifier, treating as app name",
|
|
);
|
|
// Remove trailing colons from app name
|
|
const appName = cleanAppTarget.replace(/:+$/, "");
|
|
args.push("--app", appName);
|
|
args.push("--mode", "multi");
|
|
}
|
|
}
|
|
} else {
|
|
// 'AppName': All windows of that app
|
|
args.push("--app", input.app_target.trim());
|
|
args.push("--mode", "multi");
|
|
}
|
|
|
|
// Add path if it was provided
|
|
if (effectivePath) {
|
|
args.push("--path", effectivePath);
|
|
}
|
|
|
|
// Add format
|
|
args.push("--format", actualFormat);
|
|
|
|
// Add capture focus
|
|
args.push("--capture-focus", input.capture_focus || "background");
|
|
|
|
return args;
|
|
} |