From 4e0127d620377e5dc7e09444f9db0977e3d47ed1 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 28 Jul 2026 05:10:25 +0000 Subject: [PATCH] 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 --- src/domain/bytecodeProvider.ts | 206 +++++++++++++++++++++++++++++++++ tests/bytecodeProvider.test.ts | 152 ++++++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 src/domain/bytecodeProvider.ts create mode 100644 tests/bytecodeProvider.test.ts diff --git a/src/domain/bytecodeProvider.ts b/src/domain/bytecodeProvider.ts new file mode 100644 index 000000000..556a4a696 --- /dev/null +++ b/src/domain/bytecodeProvider.ts @@ -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; + +/** Classification of bytecode provenance. */ +export const bytecodeProvenanceSchema = z.enum([ + "application", + "generated", + "vendored", + "bundled", + "standard_library", +]); +export type BytecodeProvenance = z.infer; + +/** 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; + +/** 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; + +/** 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; + +/** 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, + }; +} diff --git a/tests/bytecodeProvider.test.ts b/tests/bytecodeProvider.test.ts new file mode 100644 index 000000000..a44a6dcff --- /dev/null +++ b/tests/bytecodeProvider.test.ts @@ -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"); + }); +});