125 lines
4.3 KiB
TypeScript
125 lines
4.3 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { ConfigurationError } from "./domain/errors.js";
|
|
import { err, ok, type Result } from "./domain/result.js";
|
|
import type { LogLevel } from "./logger.js";
|
|
import type { ProcessExecutionPolicy } from "./domain/processCapture.js";
|
|
|
|
const DEFAULT_HOPPER_LAUNCHER_PATH =
|
|
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
|
|
|
|
export interface AppConfig {
|
|
readonly hopperLauncherPath: string;
|
|
readonly hopperTargetPath: string | undefined;
|
|
readonly hopperTargetKind: "executable" | "database";
|
|
readonly hopperLoaderArgs: readonly string[];
|
|
readonly logLevel: LogLevel;
|
|
readonly processExecutionPolicy: ProcessExecutionPolicy;
|
|
}
|
|
|
|
const environmentSchema = z.object({
|
|
HOPPER_LAUNCHER_PATH: z.string().min(1).optional(),
|
|
HOPPER_TARGET_PATH: z.string().min(1).optional(),
|
|
HOPPER_TARGET_KIND: z.enum(["executable", "database"]).default("executable"),
|
|
HOPPER_LOADER_ARGS_JSON: z.string().optional(),
|
|
REA_LOG_LEVEL: z
|
|
.enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
|
|
.default("info"),
|
|
REA_PROCESS_CAPTURE_ENABLED: z.enum(["true", "false"]).default("false"),
|
|
REA_PROCESS_EXECUTABLE_ROOTS_JSON: z.string().default("[]"),
|
|
REA_PROCESS_WORKING_ROOTS_JSON: z.string().default("[]"),
|
|
REA_PROCESS_ALLOWED_ENV_JSON: z.string().default("[]"),
|
|
});
|
|
|
|
const parseStringArray = (
|
|
encoded: string,
|
|
name: string,
|
|
): Result<readonly string[], ConfigurationError> => {
|
|
try {
|
|
const parsed = z
|
|
.array(z.string().min(1))
|
|
.max(128)
|
|
.safeParse(JSON.parse(encoded));
|
|
return parsed.success
|
|
? ok(parsed.data)
|
|
: err(
|
|
new ConfigurationError(`${name} must encode an array of strings`, {
|
|
cause: parsed.error,
|
|
}),
|
|
);
|
|
} catch (cause: unknown) {
|
|
return err(new ConfigurationError(`${name} must be valid JSON`, { cause }));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Parse Hopper launcher configuration once at the composition root.
|
|
* Explicit loader arguments override REA's header-derived defaults for a
|
|
* supported target, allowing callers to refine Hopper's loader behavior without
|
|
* bypassing REA's path and executable-header checks.
|
|
*/
|
|
export const parseConfig = (
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
): Result<AppConfig, ConfigurationError> => {
|
|
const parsedEnvironment = environmentSchema.safeParse(environment);
|
|
if (!parsedEnvironment.success) {
|
|
return err(
|
|
new ConfigurationError("Invalid Hopper environment configuration", {
|
|
cause: parsedEnvironment.error,
|
|
}),
|
|
);
|
|
}
|
|
let loaderArgs: unknown = [];
|
|
const encoded = parsedEnvironment.data.HOPPER_LOADER_ARGS_JSON;
|
|
if (encoded !== undefined) {
|
|
try {
|
|
loaderArgs = JSON.parse(encoded);
|
|
} catch (cause: unknown) {
|
|
return err(
|
|
new ConfigurationError("HOPPER_LOADER_ARGS_JSON must be valid JSON", {
|
|
cause,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
const parsedArgs = z.array(z.string()).safeParse(loaderArgs);
|
|
if (!parsedArgs.success) {
|
|
return err(
|
|
new ConfigurationError(
|
|
"HOPPER_LOADER_ARGS_JSON must encode an array of strings",
|
|
{ cause: parsedArgs.error },
|
|
),
|
|
);
|
|
}
|
|
const executableRoots = parseStringArray(
|
|
parsedEnvironment.data.REA_PROCESS_EXECUTABLE_ROOTS_JSON,
|
|
"REA_PROCESS_EXECUTABLE_ROOTS_JSON",
|
|
);
|
|
if (!executableRoots.ok) return executableRoots;
|
|
const workingRoots = parseStringArray(
|
|
parsedEnvironment.data.REA_PROCESS_WORKING_ROOTS_JSON,
|
|
"REA_PROCESS_WORKING_ROOTS_JSON",
|
|
);
|
|
if (!workingRoots.ok) return workingRoots;
|
|
const allowedEnvironment = parseStringArray(
|
|
parsedEnvironment.data.REA_PROCESS_ALLOWED_ENV_JSON,
|
|
"REA_PROCESS_ALLOWED_ENV_JSON",
|
|
);
|
|
if (!allowedEnvironment.ok) return allowedEnvironment;
|
|
return ok({
|
|
hopperLauncherPath:
|
|
parsedEnvironment.data.HOPPER_LAUNCHER_PATH ??
|
|
DEFAULT_HOPPER_LAUNCHER_PATH,
|
|
hopperTargetPath: parsedEnvironment.data.HOPPER_TARGET_PATH,
|
|
hopperTargetKind: parsedEnvironment.data.HOPPER_TARGET_KIND,
|
|
hopperLoaderArgs: parsedArgs.data,
|
|
logLevel: parsedEnvironment.data.REA_LOG_LEVEL,
|
|
processExecutionPolicy: {
|
|
enabled: parsedEnvironment.data.REA_PROCESS_CAPTURE_ENABLED === "true",
|
|
executableRoots: executableRoots.value,
|
|
workingRoots: workingRoots.value,
|
|
allowedEnvironment: allowedEnvironment.value,
|
|
},
|
|
});
|
|
};
|