feat(bytecode): add JVM and Python bytecode providers
Add execution-free providers for JVM bytecode/JAR/WAR and Python bytecode/wheels/zipapps (#367). - bytecodeProvider.ts: domain schemas for bytecode families (JVM, Python), artifact formats (class, jar, war, ear, pyc, pyo, whl, zipapp), symbol types, and provenance classification. - detectBytecodeFamily: classifies artifacts from file extensions - detectArtifactFormat: identifies artifact format from path - classifyProvenance: distinguishes application, generated, vendored, bundled, and standard_library code - analyzeBytecodeArtifacts: summarizes total and classified symbols Closes #367
This commit is contained in:
parent
645668b70d
commit
4e0127d620
|
|
@ -0,0 +1,206 @@
|
|||
import { z } from "zod";
|
||||
|
||||
/** Supported managed-runtime bytecode families. */
|
||||
export const bytecodeFamilySchema = z.enum(["jvm", "python"]);
|
||||
export type BytecodeFamily = z.infer<typeof bytecodeFamilySchema>;
|
||||
|
||||
/** Classification of bytecode provenance. */
|
||||
export const bytecodeProvenanceSchema = z.enum([
|
||||
"application",
|
||||
"generated",
|
||||
"vendored",
|
||||
"bundled",
|
||||
"standard_library",
|
||||
]);
|
||||
export type BytecodeProvenance = z.infer<typeof bytecodeProvenanceSchema>;
|
||||
|
||||
/** A bytecode symbol discovered through static analysis. */
|
||||
export const bytecodeSymbolSchema = z.strictObject({
|
||||
/** Fully qualified name. */
|
||||
name: z.string().min(1),
|
||||
/** Raw bytecode location or offset. */
|
||||
raw_location: z.string().min(1),
|
||||
/** Normalized symbol type. */
|
||||
kind: z.enum([
|
||||
"class",
|
||||
"interface",
|
||||
"method",
|
||||
"field",
|
||||
"function",
|
||||
"module",
|
||||
"variable",
|
||||
]),
|
||||
/** Bytecode version if known. */
|
||||
bytecode_version: z.number().int().nullable(),
|
||||
/** Provenance classification. */
|
||||
provenance: bytecodeProvenanceSchema,
|
||||
/** Whether the symbol is accessible (public). */
|
||||
is_public: z.boolean().default(false),
|
||||
});
|
||||
export type BytecodeSymbol = z.infer<typeof bytecodeSymbolSchema>;
|
||||
|
||||
/** A discovered bytecode artifact (class file, wheel, etc). */
|
||||
export const bytecodeArtifactSchema = z.strictObject({
|
||||
/** Artifact path relative to the analysis root. */
|
||||
path: z.string().min(1),
|
||||
/** Bytecode family. */
|
||||
family: bytecodeFamilySchema,
|
||||
/** Artifact format. */
|
||||
format: z.enum(["class", "jar", "war", "ear", "pyc", "pyo", "whl", "zipapp"]),
|
||||
/** Size in bytes. */
|
||||
size: z.number().int().nonnegative(),
|
||||
/** SHA-256 digest. */
|
||||
digest: z.string().regex(/^[a-f0-9]{64}$/u),
|
||||
/** Discovered symbols. */
|
||||
symbols: z.array(bytecodeSymbolSchema).default([]),
|
||||
/** Whether this is a standard library artifact. */
|
||||
is_standard_library: z.boolean().default(false),
|
||||
/** Whether this is a generated artifact. */
|
||||
is_generated: z.boolean().default(false),
|
||||
/** Whether this is a vendored artifact. */
|
||||
is_vendored: z.boolean().default(false),
|
||||
});
|
||||
export type BytecodeArtifact = z.infer<typeof bytecodeArtifactSchema>;
|
||||
|
||||
/** Static bytecode analysis result. */
|
||||
export const bytecodeAnalysisSchema = z.strictObject({
|
||||
/** Bytecode family. */
|
||||
family: bytecodeFamilySchema,
|
||||
/** All discovered artifacts. */
|
||||
artifacts: z.array(bytecodeArtifactSchema).min(0).max(10_000),
|
||||
/** Total symbols across all artifacts. */
|
||||
total_symbols: z.number().int().nonnegative(),
|
||||
/** Symbols classified as application code. */
|
||||
application_symbols: z.number().int().nonnegative(),
|
||||
/** Symbols classified as generated code. */
|
||||
generated_symbols: z.number().int().nonnegative(),
|
||||
/** Symbols classified as vendored code. */
|
||||
vendored_symbols: z.number().int().nonnegative(),
|
||||
/** Symbols classified as standard library. */
|
||||
standard_library_symbols: z.number().int().nonnegative(),
|
||||
});
|
||||
export type BytecodeAnalysis = z.infer<typeof bytecodeAnalysisSchema>;
|
||||
|
||||
/** Detect bytecode family from file extension. */
|
||||
export function detectBytecodeFamily(path: string): BytecodeFamily | null {
|
||||
const lower = path.toLowerCase();
|
||||
if (
|
||||
lower.endsWith(".class") ||
|
||||
lower.endsWith(".jar") ||
|
||||
lower.endsWith(".war") ||
|
||||
lower.endsWith(".ear")
|
||||
) {
|
||||
return "jvm";
|
||||
}
|
||||
if (
|
||||
lower.endsWith(".pyc") ||
|
||||
lower.endsWith(".pyo") ||
|
||||
lower.endsWith(".whl") ||
|
||||
lower.endsWith(".pyz") ||
|
||||
lower.endsWith(".zipapp")
|
||||
) {
|
||||
return "python";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Detect artifact format from file path. */
|
||||
export function detectArtifactFormat(
|
||||
path: string,
|
||||
): `class` | `jar` | `war` | `ear` | `pyc` | `pyo` | `whl` | `zipapp` | null {
|
||||
const lower = path.toLowerCase();
|
||||
if (lower.endsWith(".class")) return "class";
|
||||
if (lower.endsWith(".jar")) return "jar";
|
||||
if (lower.endsWith(".war")) return "war";
|
||||
if (lower.endsWith(".ear")) return "ear";
|
||||
if (lower.endsWith(".pyc")) return "pyc";
|
||||
if (lower.endsWith(".pyo")) return "pyo";
|
||||
if (lower.endsWith(".whl")) return "whl";
|
||||
if (lower.endsWith(".pyz") || lower.endsWith(".zipapp")) return "zipapp";
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Classify bytecode symbol provenance. */
|
||||
export function classifyProvenance(
|
||||
path: string,
|
||||
symbolName: string,
|
||||
): BytecodeProvenance {
|
||||
const lower = path.toLowerCase();
|
||||
// Standard library paths
|
||||
if (
|
||||
lower.includes("/stdlib/") ||
|
||||
lower.includes("/site-packages/") ||
|
||||
lower.includes("java/") ||
|
||||
lower.includes("javax/") ||
|
||||
lower.includes("sun/") ||
|
||||
symbolName.startsWith("java.") ||
|
||||
symbolName.startsWith("javax.") ||
|
||||
symbolName.startsWith("sun.") ||
|
||||
symbolName.startsWith("__")
|
||||
) {
|
||||
return "standard_library";
|
||||
}
|
||||
// Generated code
|
||||
if (
|
||||
lower.includes("/generated/") ||
|
||||
lower.includes("_generated") ||
|
||||
lower.includes("/build/") ||
|
||||
lower.includes("/target/") ||
|
||||
lower.includes("/dist-packages/") ||
|
||||
lower.includes("/__pycache__/") ||
|
||||
symbolName.includes("Generated") ||
|
||||
symbolName.includes("_generated")
|
||||
) {
|
||||
return "generated";
|
||||
}
|
||||
// Vendored code
|
||||
if (
|
||||
lower.includes("/vendor/") ||
|
||||
lower.includes("/vendored/") ||
|
||||
lower.includes("/third_party/") ||
|
||||
lower.includes("/3rdparty/") ||
|
||||
lower.includes("/node_modules/") ||
|
||||
lower.includes("/vendor/")
|
||||
) {
|
||||
return "vendored";
|
||||
}
|
||||
// Bundled code
|
||||
if (
|
||||
lower.includes("/bundle/") ||
|
||||
lower.includes("/bundled/") ||
|
||||
lower.includes(".jar!") ||
|
||||
lower.includes(".war!")
|
||||
) {
|
||||
return "bundled";
|
||||
}
|
||||
return "application";
|
||||
}
|
||||
|
||||
/** Analyze a set of bytecode artifacts and produce a summary. */
|
||||
export function analyzeBytecodeArtifacts(
|
||||
artifacts: readonly BytecodeArtifact[],
|
||||
family: BytecodeFamily,
|
||||
): BytecodeAnalysis {
|
||||
const applicationSymbols = artifacts
|
||||
.flatMap((a) => a.symbols)
|
||||
.filter((s) => s.provenance === "application").length;
|
||||
const generatedSymbols = artifacts
|
||||
.flatMap((a) => a.symbols)
|
||||
.filter((s) => s.provenance === "generated").length;
|
||||
const vendoredSymbols = artifacts
|
||||
.flatMap((a) => a.symbols)
|
||||
.filter((s) => s.provenance === "vendored").length;
|
||||
const standardLibrarySymbols = artifacts
|
||||
.flatMap((a) => a.symbols)
|
||||
.filter((s) => s.provenance === "standard_library").length;
|
||||
|
||||
return {
|
||||
family,
|
||||
artifacts: artifacts.slice(0, 10_000),
|
||||
total_symbols: artifacts.reduce((sum, a) => sum + a.symbols.length, 0),
|
||||
application_symbols: applicationSymbols,
|
||||
generated_symbols: generatedSymbols,
|
||||
vendored_symbols: vendoredSymbols,
|
||||
standard_library_symbols: standardLibrarySymbols,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
analyzeBytecodeArtifacts,
|
||||
bytecodeAnalysisSchema,
|
||||
classifyProvenance,
|
||||
detectArtifactFormat,
|
||||
detectBytecodeFamily,
|
||||
type BytecodeArtifact,
|
||||
type BytecodeSymbol,
|
||||
} from "../src/domain/bytecodeProvider.js";
|
||||
|
||||
describe("bytecode provider", () => {
|
||||
it("detects JVM family from extensions", () => {
|
||||
expect(detectBytecodeFamily("App.class")).toBe("jvm");
|
||||
expect(detectBytecodeFamily("app.jar")).toBe("jvm");
|
||||
expect(detectBytecodeFamily("webapp.war")).toBe("jvm");
|
||||
expect(detectBytecodeFamily("app.ear")).toBe("jvm");
|
||||
});
|
||||
|
||||
it("detects Python family from extensions", () => {
|
||||
expect(detectBytecodeFamily("module.pyc")).toBe("python");
|
||||
expect(detectBytecodeFamily("module.pyo")).toBe("python");
|
||||
expect(detectBytecodeFamily("package.whl")).toBe("python");
|
||||
expect(detectBytecodeFamily("app.pyz")).toBe("python");
|
||||
expect(detectBytecodeFamily("app.zipapp")).toBe("python");
|
||||
});
|
||||
|
||||
it("returns null for unknown extensions", () => {
|
||||
expect(detectBytecodeFamily("readme.txt")).toBeNull();
|
||||
expect(detectBytecodeFamily("script.js")).toBeNull();
|
||||
});
|
||||
|
||||
it("detects artifact format from path", () => {
|
||||
expect(detectArtifactFormat("App.class")).toBe("class");
|
||||
expect(detectArtifactFormat("lib.jar")).toBe("jar");
|
||||
expect(detectArtifactFormat("web.war")).toBe("war");
|
||||
expect(detectArtifactFormat("mod.pyc")).toBe("pyc");
|
||||
expect(detectArtifactFormat("pkg.whl")).toBe("whl");
|
||||
expect(detectArtifactFormat("unknown.xyz")).toBeNull();
|
||||
});
|
||||
|
||||
it("classifies standard library provenance", () => {
|
||||
expect(classifyProvenance("/usr/lib/stdlib", "java.lang.String")).toBe(
|
||||
"standard_library",
|
||||
);
|
||||
expect(
|
||||
classifyProvenance("/lib/python3.10/site-packages/numpy", "__init__"),
|
||||
).toBe("standard_library");
|
||||
});
|
||||
|
||||
it("classifies generated provenance", () => {
|
||||
expect(classifyProvenance("/build/generated/src", "GeneratedClass")).toBe(
|
||||
"generated",
|
||||
);
|
||||
expect(
|
||||
classifyProvenance("/__pycache__/module.cpython-310.pyc", "module"),
|
||||
).toBe("generated");
|
||||
});
|
||||
|
||||
it("classifies vendored provenance", () => {
|
||||
expect(classifyProvenance("/vendor/libs/lib.jar", "VendorLib")).toBe(
|
||||
"vendored",
|
||||
);
|
||||
expect(classifyProvenance("/third_party/pkg/module.pyc", "module")).toBe(
|
||||
"vendored",
|
||||
);
|
||||
});
|
||||
|
||||
it("classifies application provenance", () => {
|
||||
expect(classifyProvenance("/src/main/App.java", "com.example.App")).toBe(
|
||||
"application",
|
||||
);
|
||||
expect(classifyProvenance("/src/app.py", "main")).toBe("application");
|
||||
});
|
||||
|
||||
it("analyzes bytecode artifacts and computes summary", () => {
|
||||
const artifacts: BytecodeArtifact[] = [
|
||||
{
|
||||
path: "src/App.class",
|
||||
family: "jvm",
|
||||
format: "class",
|
||||
size: 1000,
|
||||
digest: "a".repeat(64),
|
||||
symbols: [
|
||||
{
|
||||
name: "com.example.App",
|
||||
raw_location: "0x1000",
|
||||
kind: "class",
|
||||
bytecode_version: 52,
|
||||
provenance: "application",
|
||||
is_public: true,
|
||||
},
|
||||
{
|
||||
name: "java.lang.String",
|
||||
raw_location: "0x2000",
|
||||
kind: "class",
|
||||
bytecode_version: 52,
|
||||
provenance: "standard_library",
|
||||
is_public: true,
|
||||
},
|
||||
],
|
||||
is_standard_library: false,
|
||||
is_generated: false,
|
||||
is_vendored: false,
|
||||
},
|
||||
{
|
||||
path: "lib/generated/Generated.class",
|
||||
family: "jvm",
|
||||
format: "class",
|
||||
size: 500,
|
||||
digest: "b".repeat(64),
|
||||
symbols: [
|
||||
{
|
||||
name: "GeneratedClass",
|
||||
raw_location: "0x3000",
|
||||
kind: "class",
|
||||
bytecode_version: 52,
|
||||
provenance: "generated",
|
||||
is_public: false,
|
||||
},
|
||||
],
|
||||
is_standard_library: false,
|
||||
is_generated: true,
|
||||
is_vendored: false,
|
||||
},
|
||||
];
|
||||
const result = analyzeBytecodeArtifacts(artifacts, "jvm");
|
||||
expect(result.family).toBe("jvm");
|
||||
expect(result.total_symbols).toBe(3);
|
||||
expect(result.application_symbols).toBe(1);
|
||||
expect(result.standard_library_symbols).toBe(1);
|
||||
expect(result.generated_symbols).toBe(1);
|
||||
expect(result.vendored_symbols).toBe(0);
|
||||
|
||||
// Validate schema
|
||||
const parsed = bytecodeAnalysisSchema.safeParse(result);
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
|
||||
it("uses BytecodeSymbol type", () => {
|
||||
const sym: BytecodeSymbol = {
|
||||
name: "com.example.Test",
|
||||
raw_location: "0x100",
|
||||
kind: "class",
|
||||
bytecode_version: 52,
|
||||
provenance: "application",
|
||||
is_public: true,
|
||||
};
|
||||
expect(sym.provenance).toBe("application");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue