fix(ghidra): derive installation admission from Java property semantics
This commit is contained in:
parent
1f7f808c4f
commit
2187e16f14
|
|
@ -196,7 +196,7 @@ describe("Ghidra seed profile", () => {
|
|||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
|
|||
const installationHost = (
|
||||
overrides: Partial<GhidraInstallationHost> = {},
|
||||
): GhidraInstallationHost => ({
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
|
|
@ -111,7 +111,7 @@ describe("Ghidra doctor integration", () => {
|
|||
"wrong version",
|
||||
inspection(
|
||||
{ installDir: INSTALL },
|
||||
{ readText: () => "application.version=11.4\n" },
|
||||
{ readText: () => "application.version=11.4\napplication.java.min=21\n" },
|
||||
),
|
||||
"ghidra-version",
|
||||
],
|
||||
|
|
@ -136,6 +136,14 @@ describe("Ghidra doctor integration", () => {
|
|||
),
|
||||
"ghidra-java",
|
||||
],
|
||||
[
|
||||
"missing Java minimum",
|
||||
inspection(
|
||||
{ installDir: INSTALL },
|
||||
{ readText: () => "application.version=12.1.4\n" },
|
||||
),
|
||||
"ghidra-java",
|
||||
],
|
||||
[
|
||||
"unsupported architecture",
|
||||
inspection({ installDir: INSTALL, architecture: "arm" }),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import {
|
|||
import {
|
||||
SUPPORTED_GHIDRA_JAVA_MAJOR,
|
||||
SUPPORTED_GHIDRA_VERSION,
|
||||
ghidraApplicationProperties,
|
||||
ghidraJavaRequirement,
|
||||
} from "./GhidraInstallationPolicy.js";
|
||||
|
||||
const INSTALL = "/opt/ghidra";
|
||||
|
|
@ -42,7 +44,7 @@ const host = (
|
|||
): GhidraInstallationHost => ({
|
||||
readText: (path) =>
|
||||
path === PROPERTIES
|
||||
? `application.version=${SUPPORTED_GHIDRA_VERSION}\n`
|
||||
? `application.version=${SUPPORTED_GHIDRA_VERSION}\napplication.java.min=${String(SUPPORTED_GHIDRA_JAVA_MAJOR)}\n`
|
||||
: undefined,
|
||||
executable: (path) =>
|
||||
path === HEADLESS || path === MAC_DECOMPILER || path === LINUX_DECOMPILER,
|
||||
|
|
@ -190,7 +192,7 @@ describe("Ghidra installation inspection", () => {
|
|||
architecture: "x64",
|
||||
readText: (path) =>
|
||||
path === WINDOWS_PROPERTIES
|
||||
? `application.version=${SUPPORTED_GHIDRA_VERSION}\n`
|
||||
? `application.version=${SUPPORTED_GHIDRA_VERSION}\napplication.java.min=21\n`
|
||||
: undefined,
|
||||
executable: (path) =>
|
||||
path === WINDOWS_HEADLESS || path === WINDOWS_DECOMPILER,
|
||||
|
|
@ -275,7 +277,9 @@ describe("Ghidra installation rejection diagnostics", () => {
|
|||
platform: "linux" as const,
|
||||
architecture: "x64" as const,
|
||||
},
|
||||
override: { readText: () => "application.version=11.4\n" },
|
||||
override: {
|
||||
readText: () => "application.version=11.4\napplication.java.min=21\n",
|
||||
},
|
||||
failed: "version",
|
||||
code: "unsupported_version",
|
||||
},
|
||||
|
|
@ -350,6 +354,167 @@ describe("Ghidra installation rejection diagnostics", () => {
|
|||
});
|
||||
|
||||
describe("Ghidra release line and JDK bounds", () => {
|
||||
it("uses Java Properties last-key, delimiter, escape, and continuation rules", () => {
|
||||
const properties = ghidraApplicationProperties(
|
||||
[
|
||||
"application.version=11.4",
|
||||
"# comment ending with a slash " + "\\",
|
||||
"application.\\u0076ersion : 12.\\",
|
||||
" 1.\\u0034",
|
||||
"! another comment " + "\\",
|
||||
"application.java.\\u006din\t21",
|
||||
"application.java.max=",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(properties).toEqual({
|
||||
providerVersion: "12.1.4",
|
||||
javaMinimum: { status: "major", major: 21 },
|
||||
javaMaximum: { status: "absent" },
|
||||
parseError: null,
|
||||
});
|
||||
expect(
|
||||
ghidraApplicationProperties("application.version=12.1.4").javaMinimum,
|
||||
).toEqual({
|
||||
status: "absent",
|
||||
});
|
||||
expect(
|
||||
ghidraApplicationProperties(
|
||||
"application.version=12.1.4\napplication.java.min=\n",
|
||||
).javaMinimum,
|
||||
).toEqual({ status: "unresolved", raw: "" });
|
||||
expect(
|
||||
ghidraApplicationProperties("application.version=12.1.4\\")
|
||||
.providerVersion,
|
||||
).toBe("12.1.4");
|
||||
expect(
|
||||
ghidraApplicationProperties("application.version=12.\\\n #.1")
|
||||
.providerVersion,
|
||||
).toBe("12.#.1");
|
||||
expect(
|
||||
ghidraApplicationProperties(
|
||||
"application.version=12.\\\n\napplication.version=12.1.4",
|
||||
).providerVersion,
|
||||
).toBe("12.1.4");
|
||||
});
|
||||
|
||||
it("matches Ghidra integer Java bounds and zero maximum semantics", () => {
|
||||
const properties = ghidraApplicationProperties(
|
||||
"application.java.min=21.0\napplication.java.max=0\n",
|
||||
);
|
||||
expect(properties.javaMinimum).toEqual({
|
||||
status: "unresolved",
|
||||
raw: "21.0",
|
||||
});
|
||||
expect(properties.javaMaximum).toEqual({ status: "absent" });
|
||||
expect(
|
||||
ghidraApplicationProperties("application.java.max=+0").javaMaximum,
|
||||
).toEqual({
|
||||
status: "absent",
|
||||
});
|
||||
expect(
|
||||
ghidraApplicationProperties("application.java.max=+0").javaMaximum,
|
||||
).toEqual({ status: "absent" });
|
||||
expect(
|
||||
ghidraApplicationProperties("application.java.min=+21").javaMinimum,
|
||||
).toEqual({
|
||||
status: "major",
|
||||
major: 21,
|
||||
});
|
||||
expect(
|
||||
ghidraApplicationProperties("application.java.min=٢١").javaMinimum,
|
||||
).toEqual({
|
||||
status: "major",
|
||||
major: 21,
|
||||
});
|
||||
expect(
|
||||
ghidraApplicationProperties("application.java.min=21").javaMinimum,
|
||||
).toEqual({
|
||||
status: "major",
|
||||
major: 21,
|
||||
});
|
||||
expect(
|
||||
ghidraApplicationProperties("application.java.max=+0").javaMaximum,
|
||||
).toEqual({
|
||||
status: "unresolved",
|
||||
raw: "+0",
|
||||
});
|
||||
expect(
|
||||
ghidraApplicationProperties("application.java.min=②").javaMinimum,
|
||||
).toEqual({
|
||||
status: "unresolved",
|
||||
raw: "②",
|
||||
});
|
||||
expect(
|
||||
ghidraApplicationProperties("application.java.min=2147483648")
|
||||
.javaMinimum,
|
||||
).toEqual({ status: "unresolved", raw: "2147483648" });
|
||||
});
|
||||
|
||||
it("keeps an unavailable properties file unresolved for Java admission", () => {
|
||||
expect(ghidraJavaRequirement(undefined).unresolved).toBe(
|
||||
"application.properties is unavailable",
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves property value trailing whitespace instead of normalizing it", () => {
|
||||
const properties = ghidraApplicationProperties(
|
||||
"application.version=12.1.4 \napplication.java.min=21 \n",
|
||||
);
|
||||
expect(properties.providerVersion).toBe("12.1.4 ");
|
||||
expect(properties.javaMinimum).toEqual({
|
||||
status: "unresolved",
|
||||
raw: "21 ",
|
||||
});
|
||||
expect(properties.parseError).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra malformed property admission", () => {
|
||||
it("fails installation and doctor admission closed on malformed Unicode escapes", () => {
|
||||
const result = inspectGhidraInstallation(
|
||||
{
|
||||
environment: {},
|
||||
installDir: INSTALL,
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
},
|
||||
host({
|
||||
readText: () =>
|
||||
"application.version=12.1.4\nbroken=\\u12G4\napplication.java.min=17\n",
|
||||
}),
|
||||
);
|
||||
expect(result.status).toBe("unavailable");
|
||||
expect(result.providerVersion).toBeNull();
|
||||
expect(result.checks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "version",
|
||||
status: "failed",
|
||||
code: "version_unresolved",
|
||||
detail: expect.stringContaining("Malformed Unicode escape"),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: "java",
|
||||
status: "failed",
|
||||
code: "version_unresolved",
|
||||
remediation: expect.stringContaining(
|
||||
"application.properties parse failed",
|
||||
),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(projectGhidraDoctorInspection(result)).toMatchObject({
|
||||
available: false,
|
||||
registrationEnvironment: {},
|
||||
checks: expect.arrayContaining([
|
||||
expect.objectContaining({ name: "version", ok: false }),
|
||||
expect.objectContaining({ name: "java", ok: false }),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Ghidra installation version admission", () => {
|
||||
it.each([
|
||||
{
|
||||
name: "JDK above the installation maximum",
|
||||
|
|
@ -367,16 +532,23 @@ describe("Ghidra release line and JDK bounds", () => {
|
|||
failed: "java",
|
||||
code: "version_unresolved",
|
||||
},
|
||||
{
|
||||
name: "missing Java minimum",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
probeJava: () => JAVA,
|
||||
failed: "java",
|
||||
code: "version_unresolved",
|
||||
},
|
||||
{
|
||||
name: "unparseable Ghidra version",
|
||||
readText: () => "application.version=latest\n",
|
||||
readText: () => "application.version=latest\napplication.java.min=21\n",
|
||||
probeJava: () => JAVA,
|
||||
failed: "version",
|
||||
code: "version_unresolved",
|
||||
},
|
||||
{
|
||||
name: "different Ghidra line",
|
||||
readText: () => "application.version=12.2.0\n",
|
||||
readText: () => "application.version=12.2.0\napplication.java.min=21\n",
|
||||
probeJava: () => JAVA,
|
||||
failed: "version",
|
||||
code: "unsupported_version",
|
||||
|
|
@ -422,7 +594,7 @@ describe("Ghidra release line and JDK bounds", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("honors an installation that declares a lower Java minimum", () => {
|
||||
it("accepts a lower Java minimum declared by the installation", () => {
|
||||
expect(
|
||||
inspectGhidraInstallation(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -641,14 +641,20 @@ const versionCheck = (
|
|||
properties: GhidraApplicationProperties | undefined,
|
||||
): GhidraInstallationCheck => {
|
||||
const raw = properties?.providerVersion;
|
||||
const parseError = properties?.parseError;
|
||||
return installationCheck({
|
||||
name: "version",
|
||||
passed: ghidraReleaseAccepted(raw),
|
||||
passed: parseError == null && ghidraReleaseAccepted(raw),
|
||||
code:
|
||||
ghidraReleaseCompatibility(raw).status === "unsupported"
|
||||
? "unsupported_version"
|
||||
: "version_unresolved",
|
||||
detail: ghidraVersionDetail(raw),
|
||||
parseError != null
|
||||
? "version_unresolved"
|
||||
: ghidraReleaseCompatibility(raw).status === "unsupported"
|
||||
? "unsupported_version"
|
||||
: "version_unresolved",
|
||||
detail:
|
||||
parseError == null
|
||||
? ghidraVersionDetail(raw)
|
||||
: `application.properties could not be parsed: ${parseError}`,
|
||||
remediation: `Install a Ghidra ${ghidraReleaseLine()} release (verified with ${SUPPORTED_GHIDRA_VERSION}), or update REA for another Ghidra line.`,
|
||||
});
|
||||
};
|
||||
|
|
@ -701,7 +707,9 @@ const javaRemediation = (
|
|||
const systemGhidraInstallationHost = (): GhidraInstallationHost => ({
|
||||
readText(path) {
|
||||
try {
|
||||
return readFileSync(path, "utf8");
|
||||
// Ghidra loads this file with Properties.load(InputStream), which maps
|
||||
// bytes as ISO-8859-1 rather than decoding UTF-8.
|
||||
return readFileSync(path).toString("latin1");
|
||||
} catch (cause: unknown) {
|
||||
// best-effort cleanup: optional install probing; unreadable means unknown.
|
||||
void cause;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
/** Ghidra build whose bridge behavior this REA release verifies. */
|
||||
export const SUPPORTED_GHIDRA_VERSION = "12.1.4";
|
||||
/**
|
||||
* JDK major used when an installation omits `application.java.min`.
|
||||
* Admission uses that installation's `application.java.min` and
|
||||
* `application.java.max`. An empty maximum means the release sets no upper bound.
|
||||
*/
|
||||
/** Verified JDK major; admission uses the installation's declared bounds. */
|
||||
export const SUPPORTED_GHIDRA_JAVA_MAJOR = 21;
|
||||
|
||||
/** Numeric Ghidra release read from `application.version`. */
|
||||
|
|
@ -40,30 +38,49 @@ type GhidraJavaBound =
|
|||
| { readonly status: "unresolved"; readonly raw: string };
|
||||
|
||||
/** JDK majors this installation admits, after reading its own properties. */
|
||||
export interface GhidraJavaRequirement {
|
||||
readonly minimumMajor: number;
|
||||
readonly maximumMajor: number | null;
|
||||
readonly unresolved: string | null;
|
||||
}
|
||||
export type GhidraJavaRequirement =
|
||||
| {
|
||||
readonly minimumMajor: number;
|
||||
readonly maximumMajor: number | null;
|
||||
readonly unresolved: null;
|
||||
}
|
||||
| { readonly unresolved: string };
|
||||
|
||||
/** Version and Java bounds read from one `application.properties` file. */
|
||||
export interface GhidraApplicationProperties {
|
||||
readonly providerVersion: string | null;
|
||||
readonly javaMinimum: GhidraJavaBound;
|
||||
readonly javaMaximum: GhidraJavaBound;
|
||||
/** A malformed Java properties escape invalidates the entire file. */
|
||||
readonly parseError: string | null;
|
||||
}
|
||||
|
||||
const GHIDRA_RELEASE_PATTERN = /^(\d+)\.(\d+)(?:\.(\d+))?([-+].*)?$/u;
|
||||
const JAVA_MAJOR_PATTERN = /^(\d+)(?:\.\d+)*$/u;
|
||||
|
||||
/** Read the version and Java bounds Ghidra publishes for one installation. */
|
||||
export const ghidraApplicationProperties = (
|
||||
content: string,
|
||||
): GhidraApplicationProperties => ({
|
||||
providerVersion: propertyValue(content, "application.version"),
|
||||
javaMinimum: parseJavaBound(propertyValue(content, "application.java.min")),
|
||||
javaMaximum: parseJavaBound(propertyValue(content, "application.java.max")),
|
||||
});
|
||||
): GhidraApplicationProperties => {
|
||||
const parsed = parseJavaProperties(content);
|
||||
if (!parsed.ok)
|
||||
return {
|
||||
providerVersion: null,
|
||||
javaMinimum: { status: "unresolved", raw: parsed.error },
|
||||
javaMaximum: { status: "unresolved", raw: parsed.error },
|
||||
parseError: parsed.error,
|
||||
};
|
||||
const providerVersion = parsed.value.get("application.version");
|
||||
const minimum = parsed.value.get("application.java.min");
|
||||
const maximum = parsed.value.get("application.java.max");
|
||||
return {
|
||||
providerVersion: providerVersion ?? null,
|
||||
javaMinimum:
|
||||
minimum === undefined ? { status: "absent" } : parseJavaBound(minimum),
|
||||
// Ghidra uses an explicitly empty maximum to mean no upper bound.
|
||||
javaMaximum: parseJavaMaximum(maximum),
|
||||
parseError: null,
|
||||
};
|
||||
};
|
||||
|
||||
/** Classify `application.version` against the verified release line. */
|
||||
export const ghidraReleaseCompatibility = (
|
||||
|
|
@ -130,14 +147,21 @@ export const ghidraReleaseAccepted = (
|
|||
export const ghidraJavaRequirement = (
|
||||
properties: GhidraApplicationProperties | undefined,
|
||||
): GhidraJavaRequirement => {
|
||||
const minimum = properties?.javaMinimum ?? { status: "absent" };
|
||||
const maximum = properties?.javaMaximum ?? { status: "absent" };
|
||||
if (properties?.parseError !== null && properties?.parseError !== undefined)
|
||||
return unresolvedJavaRequirement(
|
||||
`application.properties parse failed: ${properties.parseError}`,
|
||||
);
|
||||
if (properties === undefined)
|
||||
return unresolvedJavaRequirement("application.properties is unavailable");
|
||||
const minimum = properties.javaMinimum;
|
||||
const maximum = properties.javaMaximum;
|
||||
if (minimum.status === "unresolved")
|
||||
return unresolvedJavaRequirement(`application.java.min=${minimum.raw}`);
|
||||
if (minimum.status === "absent")
|
||||
return unresolvedJavaRequirement("application.java.min is missing");
|
||||
if (maximum.status === "unresolved")
|
||||
return unresolvedJavaRequirement(`application.java.max=${maximum.raw}`);
|
||||
const minimumMajor =
|
||||
minimum.status === "major" ? minimum.major : SUPPORTED_GHIDRA_JAVA_MAJOR;
|
||||
const minimumMajor = minimum.major;
|
||||
const maximumMajor = maximum.status === "major" ? maximum.major : null;
|
||||
if (maximumMajor !== null && maximumMajor < minimumMajor)
|
||||
return unresolvedJavaRequirement(
|
||||
|
|
@ -187,14 +211,56 @@ const parseGhidraRelease = (raw: string): GhidraReleaseVersion | undefined => {
|
|||
return patch === undefined ? undefined : { major, minor, patch };
|
||||
};
|
||||
|
||||
const parseJavaBound = (raw: string | null): GhidraJavaBound => {
|
||||
if (raw === null) return { status: "absent" };
|
||||
const major = integerGroup(JAVA_MAJOR_PATTERN.exec(raw)?.[1]);
|
||||
const parseJavaBound = (raw: string): GhidraJavaBound => {
|
||||
const major = parseJavaInteger(raw);
|
||||
return major === undefined
|
||||
? { status: "unresolved", raw }
|
||||
: { status: "major", major };
|
||||
};
|
||||
|
||||
const parseJavaMaximum = (raw: string | undefined): GhidraJavaBound => {
|
||||
if (raw === undefined || raw === "") return { status: "absent" };
|
||||
const bound = parseJavaBound(raw);
|
||||
return bound.status === "major" && bound.major === 0
|
||||
? { status: "absent" }
|
||||
: bound;
|
||||
};
|
||||
|
||||
const parseJavaInteger = (raw: string): number | undefined => {
|
||||
let index = 0;
|
||||
let sign = 1;
|
||||
if (raw[index] === "+" || raw[index] === "-") {
|
||||
if (raw[index] === "-") sign = -1;
|
||||
index += 1;
|
||||
}
|
||||
if (index === raw.length) return undefined;
|
||||
const limit = sign < 0 ? 2_147_483_648 : 2_147_483_647;
|
||||
let value = 0;
|
||||
for (; index < raw.length; index += 1) {
|
||||
const character = raw[index];
|
||||
if (character === undefined) return undefined;
|
||||
const digit = javaDecimalDigit(character);
|
||||
if (digit === undefined) return undefined;
|
||||
if (value > Math.floor((limit - digit) / 10)) return undefined;
|
||||
value = value * 10 + digit;
|
||||
}
|
||||
return sign * value;
|
||||
};
|
||||
|
||||
/** Match Integer.parseInt(charAt) for BMP Decimal_Number characters. */
|
||||
const javaDecimalDigit = (character: string): number | undefined => {
|
||||
const code = character.charCodeAt(0);
|
||||
if (code >= 48 && code <= 57) return code - 48;
|
||||
if (!/^\p{Decimal_Number}$/u.test(character)) return undefined;
|
||||
let blockStart = code;
|
||||
while (
|
||||
blockStart > 0 &&
|
||||
/^\p{Decimal_Number}$/u.test(String.fromCharCode(blockStart - 1))
|
||||
)
|
||||
blockStart -= 1;
|
||||
return (code - blockStart) % 10;
|
||||
};
|
||||
|
||||
const integerGroup = (value: string | undefined): number | undefined => {
|
||||
if (value === undefined) return undefined;
|
||||
const parsed = Number(value);
|
||||
|
|
@ -204,17 +270,119 @@ const integerGroup = (value: string | undefined): number | undefined => {
|
|||
const unresolvedJavaRequirement = (
|
||||
unresolved: string,
|
||||
): GhidraJavaRequirement => ({
|
||||
minimumMajor: SUPPORTED_GHIDRA_JAVA_MAJOR,
|
||||
maximumMajor: null,
|
||||
unresolved,
|
||||
});
|
||||
|
||||
const propertyValue = (content: string, name: string): string | null => {
|
||||
for (const line of content.split(/\r?\n/u)) {
|
||||
const separator = line.indexOf("=");
|
||||
if (separator < 0 || line.slice(0, separator).trim() !== name) continue;
|
||||
const value = line.slice(separator + 1).trim();
|
||||
return value.length === 0 ? null : value;
|
||||
/** Parse Java Properties.load(InputStream) syntax from its ISO-8859-1 text. */
|
||||
const parseJavaProperties = (
|
||||
content: string,
|
||||
): Result<ReadonlyMap<string, string>, string> => {
|
||||
const values = new Map<string, string>();
|
||||
for (const line of logicalPropertyLines(content)) {
|
||||
let start = 0;
|
||||
while (isPropertyWhitespace(line[start])) start += 1;
|
||||
if (start === line.length || line[start] === "#" || line[start] === "!")
|
||||
continue;
|
||||
let end = start;
|
||||
let escaped = false;
|
||||
while (end < line.length) {
|
||||
const character = line[end];
|
||||
if (
|
||||
!escaped &&
|
||||
(character === "=" ||
|
||||
character === ":" ||
|
||||
isPropertyWhitespace(character))
|
||||
)
|
||||
break;
|
||||
if (character === "\\") escaped = !escaped;
|
||||
else escaped = false;
|
||||
end += 1;
|
||||
}
|
||||
let valueStart = end;
|
||||
while (isPropertyWhitespace(line[valueStart])) valueStart += 1;
|
||||
if (line[valueStart] === "=" || line[valueStart] === ":") valueStart += 1;
|
||||
while (isPropertyWhitespace(line[valueStart])) valueStart += 1;
|
||||
const key = decodePropertyEscapes(line.slice(start, end));
|
||||
if (!key.ok) return err(key.error);
|
||||
const value = decodePropertyEscapes(line.slice(valueStart));
|
||||
if (!value.ok) return err(value.error);
|
||||
values.set(key.value, value.value);
|
||||
}
|
||||
return null;
|
||||
return ok(values);
|
||||
};
|
||||
|
||||
const isPropertyWhitespace = (value: string | undefined): boolean =>
|
||||
value === " " || value === "\t" || value === "\f";
|
||||
|
||||
/** Join odd-backslash continuations while stripping leading continuation space. */
|
||||
function* logicalPropertyLines(content: string): Generator<string> {
|
||||
let physicalStart = 0;
|
||||
let logical = "";
|
||||
let continuing = false;
|
||||
while (physicalStart <= content.length) {
|
||||
let physicalEnd = physicalStart;
|
||||
while (
|
||||
physicalEnd < content.length &&
|
||||
content[physicalEnd] !== "\n" &&
|
||||
content[physicalEnd] !== "\r"
|
||||
)
|
||||
physicalEnd += 1;
|
||||
let line = content.slice(physicalStart, physicalEnd);
|
||||
let leading = 0;
|
||||
while (isPropertyWhitespace(line[leading])) leading += 1;
|
||||
const comment =
|
||||
!continuing && (line[leading] === "#" || line[leading] === "!");
|
||||
if (continuing) line = line.slice(leading);
|
||||
let continued = false;
|
||||
if (!comment) {
|
||||
let slashes = 0;
|
||||
for (
|
||||
let index = line.length - 1;
|
||||
index >= 0 && line[index] === "\\";
|
||||
index -= 1
|
||||
)
|
||||
slashes += 1;
|
||||
continued = slashes % 2 === 1;
|
||||
logical += continued ? line.slice(0, -1) : line;
|
||||
}
|
||||
if (!continued) {
|
||||
yield logical;
|
||||
logical = "";
|
||||
}
|
||||
continuing = continued;
|
||||
if (physicalEnd === content.length) break;
|
||||
physicalStart =
|
||||
content[physicalEnd] === "\r" && content[physicalEnd + 1] === "\n"
|
||||
? physicalEnd + 2
|
||||
: physicalEnd + 1;
|
||||
}
|
||||
// Properties.load still consumes the accumulated line when EOF follows a
|
||||
// continuation marker; the marker itself was already removed above.
|
||||
if (continuing) yield logical;
|
||||
}
|
||||
|
||||
const decodePropertyEscapes = (value: string): Result<string, string> => {
|
||||
let result = "";
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const character = value[index];
|
||||
if (character !== "\\") {
|
||||
result += character;
|
||||
continue;
|
||||
}
|
||||
const escaped = value[++index];
|
||||
if (escaped === undefined)
|
||||
return err("Trailing escape in Java properties value");
|
||||
if (escaped === "t") result += "\t";
|
||||
else if (escaped === "n") result += "\n";
|
||||
else if (escaped === "r") result += "\r";
|
||||
else if (escaped === "f") result += "\f";
|
||||
else if (escaped === "u") {
|
||||
const digits = value.slice(index + 1, index + 5);
|
||||
if (digits.length !== 4 || !/^[0-9a-f]{4}$/iu.test(digits))
|
||||
return err("Malformed Unicode escape in Java properties file");
|
||||
result += String.fromCharCode(Number.parseInt(digits, 16));
|
||||
index += 4;
|
||||
} else result += escaped;
|
||||
}
|
||||
return ok(result);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const X86_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
|
|||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { ghidraProfileLanguageOverride } from "./GhidraAnalysisProfile.js";
|
|||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
|
|||
const installationHost = (): GhidraInstallationHost => ({
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const RISCV_LDEFS = `<?xml version="1.0" encoding="UTF-8"?>
|
|||
const host: GhidraInstallationHost = {
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => "application.version=12.1.4\n",
|
||||
readText: () => "application.version=12.1.4\napplication.java.min=21\n",
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const fixture = (version = "12.1.4") => {
|
|||
{
|
||||
platform: "linux",
|
||||
architecture: "x64",
|
||||
readText: () => `application.version=${version}\n`,
|
||||
readText: () => `application.version=${version}\napplication.java.min=21\n`,
|
||||
executable: () => true,
|
||||
probeJava: () => ({
|
||||
version: "21.0.11",
|
||||
|
|
|
|||
Loading…
Reference in New Issue