feat: scaffold v0.1 foundation

This commit is contained in:
hermes 2026-07-28 19:26:47 +00:00
commit 170ca32f58
34 changed files with 6175 additions and 5 deletions

438
Cargo.lock generated Normal file
View file

@ -0,0 +1,438 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anstream"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anstyle-parse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
]
[[package]]
name = "assert_cmd"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6"
dependencies = [
"anstyle",
"bstr",
"libc",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bstr"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530"
dependencies = [
"memchr",
"regex-automata",
"serde_core",
]
[[package]]
name = "clap"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "clap_lex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "dyn-clone"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ghidra-cli"
version = "0.1.0"
dependencies = [
"assert_cmd",
"base64",
"clap",
"schemars",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libc"
version = "0.2.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
[[package]]
name = "memchr"
version = "2.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "predicates"
version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
dependencies = [
"anstyle",
"difflib",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
[[package]]
name = "predicates-tree"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro2"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ref-cast"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "regex-automata"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad"
[[package]]
name = "schemars"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
dependencies = [
"dyn-clone",
"ref-cast",
"schemars_derive",
"serde",
"serde_json",
]
[[package]]
name = "schemars_derive"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.119",
]
[[package]]
name = "serde"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.229"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "serde_derive_internals"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "serde_json"
version = "1.0.151"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.119"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "thiserror"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

42
Cargo.toml Normal file
View file

@ -0,0 +1,42 @@
[package]
name = "ghidra-cli"
version = "0.1.0"
edition = "2024"
rust-version = "1.85"
description = "Reproducible, read-only Ghidra analysis from the command line"
license = "Apache-2.0"
publish = false
[lib]
name = "ghidra_cli"
path = "src/lib.rs"
[[bin]]
name = "ghidr"
path = "src/main.rs"
[dependencies]
base64 = "0.22.1"
clap = { version = "4.5.60", features = ["derive"] }
schemars = { version = "=1.0.4", features = ["derive"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
thiserror = "2.0.18"
[dev-dependencies]
assert_cmd = "2.1.2"
[lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
[lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
todo = "deny"
unimplemented = "deny"
dbg_macro = "deny"
print_stdout = "deny"
print_stderr = "deny"
large_futures = "deny"

View file

@ -3,9 +3,10 @@
A small, dependable command-line interface for read-only Ghidra analysis.
The repository is named `ghidra-cli`; the installed executable is `ghidr`.
The project is currently in its design phase. Its first objective is to make
common reverse-engineering questions easy to ask without exposing Ghidra
project management, JVM lifecycle, or scripting details to the user.
The project now has its version 0.1 Rust contract foundation. The typed CLI,
public envelopes and schemas, bounded Rust/Java protocol, and synchronous
execution seam are present; store, process/sandbox, and Ghidra adapter execution
remain deliberately unconnected.
Proposed usage:
@ -33,8 +34,10 @@ available explicitly through `--format human`.
## Status
Documentation first. There is intentionally no Rust crate yet. Implementation
begins after the initial requirements and interface are reviewed.
Foundation only. Every accepted command currently returns a typed
`internal_not_implemented` runtime error instead of pretending Ghidra analysis
succeeded. Subsequent layers implement the library's synchronous executor
contract while preserving the committed schemas and reviewed fixtures.
## License

View file

@ -0,0 +1,32 @@
{
"schema_version": 1,
"kind": "cleanup",
"provenance": {
"ghidr_version": "0.1.0",
"adapter_protocol_version": 1,
"analysis_store": {
"path": { "encoding": "utf8", "value": "/tmp/ghidr-store" },
"source": "cli"
},
"sandbox": { "backend": "bubblewrap", "verification": "verified" }
},
"data": {
"mode": "dry_run",
"target": {
"kind": "digest",
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"matched": {
"analyses": 1,
"quarantined_analyses": 0,
"artifacts": 1,
"diagnostic_logs": 0,
"usage": { "logical_bytes": 4096, "allocated_bytes": 8192 },
"analysis_profile_sha256": [
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
]
},
"removed": null
},
"warnings": []
}

View file

@ -0,0 +1,47 @@
{
"schema_version": 1,
"kind": "decompilation",
"provenance": {
"ghidr_version": "0.1.0",
"adapter_protocol_version": 1,
"ghidra_version": "12.1.2",
"java_version": "21",
"sample_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"source_path": { "encoding": "utf8", "value": "./sample" },
"analysis_profile_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"target": {
"loader": "ElfLoader",
"format": "ELF",
"processor_language": "x86:LE:64:default",
"compiler_specification": "gcc",
"verification": "verified"
},
"analysis_store": {
"path": { "encoding": "utf8", "value": "/tmp/ghidr-store" },
"source": "cli"
},
"sandbox": { "backend": "bubblewrap", "verification": "verified" },
"limits": {
"max_heap_mib": 2048,
"max_cpu": 2,
"analysis_timeout_seconds": 600,
"decompile_timeout_seconds": 60,
"child_watchdog_seconds": 780,
"max_sample_bytes": 1073741824,
"max_inline_bytes": 65536
}
},
"data": {
"requested_selector": { "kind": "name", "value": "main" },
"function": {
"name": "main",
"qualified_name": "main",
"entry": { "space": "ram", "offset": "0x0000000000401000" }
},
"decompilation": {
"syntax": "ghidra_c",
"text": "int main(void) {\n return 0;\n}\n"
}
},
"warnings": []
}

View file

@ -0,0 +1,21 @@
{
"schema_version": 1,
"kind": "doctor",
"provenance": {
"ghidr_version": "0.1.0",
"adapter_protocol_version": 1,
"analysis_store": {
"path": { "encoding": "utf8", "value": "/tmp/ghidr-store" },
"source": "cli"
},
"sandbox": { "backend": "bubblewrap", "verification": "verified" }
},
"data": {
"ready": true,
"components": {
"ghidra": { "status": "ready", "details": { "version": "12.1.2" } },
"java": { "status": "ready", "details": { "version": "21" } }
}
},
"warnings": []
}

View file

@ -0,0 +1,10 @@
{
"schema_version": 1,
"kind": "error",
"error": {
"code": "function_selector_ambiguous",
"message": "symbol 'parse' resolves to more than one function",
"retryable": false,
"details": { "candidates": [] }
}
}

View file

@ -0,0 +1,58 @@
{
"schema_version": 1,
"kind": "functions",
"provenance": {
"ghidr_version": "0.1.0",
"adapter_protocol_version": 1,
"ghidra_version": "12.1.2",
"java_version": "21",
"sample_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"source_path": { "encoding": "utf8", "value": "./sample" },
"analysis_profile_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"target": {
"loader": "ElfLoader",
"format": "ELF",
"processor_language": "x86:LE:64:default",
"compiler_specification": "gcc",
"verification": "verified"
},
"analysis_store": {
"path": { "encoding": "utf8", "value": "/tmp/ghidr-store" },
"source": "cli"
},
"sandbox": { "backend": "bubblewrap", "verification": "verified" },
"limits": {
"max_heap_mib": 2048,
"max_cpu": 2,
"analysis_timeout_seconds": 600,
"decompile_timeout_seconds": null,
"child_watchdog_seconds": 720,
"max_sample_bytes": 1073741824,
"max_inline_bytes": 65536
}
},
"data": {
"page": {
"order": "location_then_entry_ascending",
"offset": 0,
"limit": 100,
"returned": 1,
"total": 1,
"has_more": false
},
"items": [
{
"name": "main",
"qualified_name": "main",
"entry": { "space": "ram", "offset": "0x0000000000401000" },
"body_address_count": 32,
"location": "memory",
"is_external": false,
"is_thunk": false,
"thunk_target_entry": null,
"decompilable": true
}
]
},
"warnings": []
}

View file

@ -0,0 +1,48 @@
{
"schema_version": 1,
"kind": "inspection",
"provenance": {
"ghidr_version": "0.1.0",
"adapter_protocol_version": 1,
"ghidra_version": "12.1.2",
"java_version": "21",
"sample_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"source_path": { "encoding": "utf8", "value": "./sample" },
"analysis_profile_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"target": {
"loader": "ElfLoader",
"format": "ELF",
"processor_language": "x86:LE:64:default",
"compiler_specification": "gcc",
"verification": "verified"
},
"analysis_store": {
"path": { "encoding": "utf8", "value": "/tmp/ghidr-store" },
"source": "cli"
},
"sandbox": { "backend": "bubblewrap", "verification": "verified" },
"limits": {
"max_heap_mib": 2048,
"max_cpu": 2,
"analysis_timeout_seconds": 600,
"decompile_timeout_seconds": null,
"child_watchdog_seconds": 720,
"max_sample_bytes": 1073741824,
"max_inline_bytes": 65536
}
},
"data": {
"sample": {
"sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"size_bytes": 4096
},
"program": {
"image_base": { "space": "ram", "offset": "0x0000000000400000" },
"minimum_address": { "space": "ram", "offset": "0x0000000000400000" },
"maximum_address": { "space": "ram", "offset": "0x0000000000400fff" },
"function_count": 1
},
"analysis": { "disposition": "created" }
},
"warnings": []
}

View file

@ -0,0 +1,167 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "AnalysisProfile",
"description": "Canonical Analysis Profile document whose exact JSON bytes are hashed.",
"type": "object",
"properties": {
"analysis_adapter_version": {
"description": "Analysis adapter implementation version.",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"analyzer_options": {
"description": "Fully resolved, bytewise-name-sorted analyzer options.",
"type": "array",
"items": {
"$ref": "#/$defs/ProfileOption"
}
},
"ghidra_version": {
"description": "Exact Ghidra version.",
"type": "string"
},
"java_version": {
"description": "Exact Java major version.",
"type": "string"
},
"loader_options": {
"description": "Fully resolved, bytewise-name-sorted loader options.",
"type": "array",
"items": {
"$ref": "#/$defs/ProfileOption"
}
},
"max_cpu": {
"description": "Maximum analysis CPU count.",
"$ref": "#/$defs/PositiveU64"
},
"profile_version": {
"type": "integer",
"const": 1
},
"target": {
"description": "Analysis-affecting Target Specification.",
"$ref": "#/$defs/ProfileTarget"
}
},
"additionalProperties": false,
"required": [
"profile_version",
"ghidra_version",
"java_version",
"analysis_adapter_version",
"target",
"loader_options",
"analyzer_options",
"max_cpu"
],
"$defs": {
"PositiveU64": {
"description": "A validated positive integer accepted by resource-limit flags.",
"type": "integer",
"format": "uint64",
"minimum": 1
},
"ProfileOption": {
"description": "Fully resolved typed Ghidra option.",
"type": "object",
"properties": {
"name": {
"description": "Canonical option name.",
"type": "string"
},
"value": {
"description": "Typed canonical value without floating-point data.",
"$ref": "#/$defs/ProfileOptionValue"
}
},
"additionalProperties": false,
"required": [
"name",
"value"
]
},
"ProfileOptionValue": {
"description": "Values permitted in canonical profile options.",
"oneOf": [
{
"description": "Boolean option.",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "boolean"
},
"value": {
"type": "boolean"
}
},
"required": [
"type",
"value"
]
},
{
"description": "Signed integer option.",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "integer"
},
"value": {
"type": "integer",
"format": "int64"
}
},
"required": [
"type",
"value"
]
},
{
"description": "Exact UTF-8 string option.",
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "string"
},
"value": {
"type": "string"
}
},
"required": [
"type",
"value"
]
}
]
},
"ProfileTarget": {
"description": "Target fields which affect Analysis identity.",
"type": "object",
"properties": {
"compiler_specification": {
"description": "Exact compiler-specification identifier.",
"type": "string"
},
"loader": {
"description": "Exact loader identifier.",
"type": "string"
},
"processor_language": {
"description": "Exact processor-language identifier.",
"type": "string"
}
},
"additionalProperties": false,
"required": [
"loader",
"processor_language",
"compiler_specification"
]
}
}
}

View file

@ -0,0 +1,417 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SuccessEnvelope",
"description": "Versioned successful response.",
"type": "object",
"properties": {
"data": {
"description": "Operation-specific result.",
"$ref": "#/$defs/CleanupData"
},
"kind": {
"type": "string",
"const": "cleanup"
},
"provenance": {
"description": "Reproducibility and execution context.",
"$ref": "#/$defs/ToolProvenance"
},
"schema_version": {
"type": "integer",
"const": 1
},
"warnings": {
"description": "Deduplicated stable warnings.",
"type": "array",
"items": {
"$ref": "#/$defs/Warning"
}
}
},
"required": [
"schema_version",
"kind",
"provenance",
"data",
"warnings"
],
"$defs": {
"AnalysisStoreProvenance": {
"description": "Analysis Store provenance.",
"type": "object",
"properties": {
"path": {
"description": "Tagged absolute store path.",
"$ref": "#/$defs/TaggedPath"
},
"source": {
"description": "Store resolution source.",
"$ref": "#/$defs/StoreSource"
}
},
"required": [
"path",
"source"
]
},
"CleanupData": {
"description": "Cleanup result data; detailed count contracts are carried as typed snapshots.",
"type": "object",
"properties": {
"matched": {
"description": "Preflight match snapshot.",
"$ref": "#/$defs/CleanupSnapshot"
},
"mode": {
"description": "Execution or dry-run mode.",
"$ref": "#/$defs/CleanupMode"
},
"removed": {
"description": "Exact removed snapshot, null for dry runs.",
"anyOf": [
{
"$ref": "#/$defs/CleanupSnapshot"
},
{
"type": "null"
}
]
},
"target": {
"description": "Resolved cleanup target.",
"$ref": "#/$defs/CleanupTarget"
}
},
"required": [
"mode",
"target",
"matched"
]
},
"CleanupMode": {
"description": "Cleanup mode.",
"oneOf": [
{
"description": "Data was removed.",
"type": "string",
"const": "executed"
},
{
"description": "No data was removed.",
"type": "string",
"const": "dry_run"
}
]
},
"CleanupSnapshot": {
"description": "Counts and disk usage associated with cleanup targets.",
"type": "object",
"properties": {
"analyses": {
"description": "Complete Analyses.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"analysis_profile_sha256": {
"description": "Unique bytewise-sorted Analysis Profile digests.",
"type": "array",
"items": {
"$ref": "#/$defs/Digest"
}
},
"artifacts": {
"description": "Query Artifacts.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"diagnostic_logs": {
"description": "Diagnostic Log bundles.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"quarantined_analyses": {
"description": "Quarantined Analyses.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"usage": {
"description": "Logical and allocated usage.",
"$ref": "#/$defs/StorageUsage"
}
},
"required": [
"analyses",
"quarantined_analyses",
"artifacts",
"diagnostic_logs",
"usage",
"analysis_profile_sha256"
]
},
"CleanupTarget": {
"description": "Resolved cleanup target.",
"oneOf": [
{
"description": "Digest resolved from a caller Sample.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "sample"
},
"sha256": {
"description": "Resolved content identity.",
"$ref": "#/$defs/Digest"
},
"source_path": {
"description": "Exact caller path.",
"$ref": "#/$defs/TaggedPath"
}
},
"required": [
"kind",
"source_path",
"sha256"
]
},
{
"description": "Caller-supplied digest.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "digest"
},
"sha256": {
"description": "Selected content identity.",
"$ref": "#/$defs/Digest"
}
},
"required": [
"kind",
"sha256"
]
},
{
"description": "Entire Analysis Store.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "all"
}
},
"required": [
"kind"
]
}
]
},
"Digest": {
"description": "A lowercase SHA-256 digest.",
"type": "string"
},
"SandboxBackend": {
"description": "Sandbox backend.",
"oneOf": [
{
"description": "Bubblewrap namespaces.",
"type": "string",
"const": "bubblewrap"
},
{
"description": "Caller-supplied isolation.",
"type": "string",
"const": "external"
},
{
"description": "Isolation disabled.",
"type": "string",
"const": "off"
}
]
},
"SandboxProvenance": {
"description": "Worker sandbox provenance.",
"type": "object",
"properties": {
"backend": {
"description": "Selected backend.",
"$ref": "#/$defs/SandboxBackend"
},
"verification": {
"description": "Isolation verification state.",
"$ref": "#/$defs/SandboxVerification"
}
},
"required": [
"backend",
"verification"
]
},
"SandboxVerification": {
"description": "Sandbox verification state.",
"oneOf": [
{
"description": "Verified by `ghidr`.",
"type": "string",
"const": "verified"
},
{
"description": "Declared but not verifiable by `ghidr`.",
"type": "string",
"const": "unverified"
},
{
"description": "Explicitly disabled.",
"type": "string",
"const": "disabled"
}
]
},
"StorageUsage": {
"description": "Filesystem usage represented without implying freed space.",
"type": "object",
"properties": {
"allocated_bytes": {
"description": "Sum of Linux allocated blocks including directories.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"logical_bytes": {
"description": "Sum of file lengths.",
"type": "integer",
"format": "uint64",
"minimum": 0
}
},
"required": [
"logical_bytes",
"allocated_bytes"
]
},
"StoreSource": {
"description": "Analysis Store resolution source.",
"oneOf": [
{
"description": "Explicit `--store` flag.",
"type": "string",
"const": "cli"
},
{
"description": "`GHIDR_STORE` environment variable.",
"type": "string",
"const": "environment"
},
{
"description": "Explicit `XDG_CACHE_HOME` fallback.",
"type": "string",
"const": "xdg"
},
{
"description": "Home-directory fallback.",
"type": "string",
"const": "home_fallback"
}
]
},
"TaggedPath": {
"description": "A path preserving either UTF-8 text or exact Unix bytes.",
"oneOf": [
{
"description": "An unmodified UTF-8 path.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "utf8"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
},
{
"description": "Standard padded Base64 over exact Unix path bytes.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "unix_bytes_base64"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
}
]
},
"ToolProvenance": {
"description": "Minimal provenance available to non-Sample operations.",
"type": "object",
"properties": {
"adapter_protocol_version": {
"description": "Rust/Java protocol version.",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"analysis_store": {
"description": "Selected Analysis Store.",
"$ref": "#/$defs/AnalysisStoreProvenance"
},
"ghidr_version": {
"description": "CLI package version.",
"type": "string"
},
"sandbox": {
"description": "Selected sandbox policy.",
"$ref": "#/$defs/SandboxProvenance"
}
},
"required": [
"ghidr_version",
"adapter_protocol_version",
"analysis_store",
"sandbox"
]
},
"Warning": {
"description": "Stable non-fatal warning.",
"type": "object",
"properties": {
"code": {
"description": "Stable machine identifier.",
"type": "string"
},
"details": {
"description": "Warning-specific context, always an object.",
"type": "object",
"additionalProperties": true
},
"message": {
"description": "Concise human context.",
"type": "string"
}
},
"required": [
"code",
"message",
"details"
]
}
}
}

View file

@ -0,0 +1,507 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SuccessEnvelope",
"description": "Versioned successful response.",
"type": "object",
"properties": {
"data": {
"description": "Operation-specific result.",
"$ref": "#/$defs/DecompilationData"
},
"kind": {
"type": "string",
"const": "decompilation"
},
"provenance": {
"description": "Reproducibility and execution context.",
"$ref": "#/$defs/QueryProvenance"
},
"schema_version": {
"type": "integer",
"const": 1
},
"warnings": {
"description": "Deduplicated stable warnings.",
"type": "array",
"items": {
"$ref": "#/$defs/Warning"
}
}
},
"required": [
"schema_version",
"kind",
"provenance",
"data",
"warnings"
],
"$defs": {
"Address": {
"description": "A Ghidra address with an explicit, case-sensitive address space.",
"type": "object",
"properties": {
"offset": {
"description": "Canonical lowercase, `0x`-prefixed, even-width hexadecimal offset.",
"type": "string"
},
"space": {
"description": "Exact Ghidra address-space name.",
"type": "string"
}
},
"required": [
"space",
"offset"
]
},
"AnalysisStoreProvenance": {
"description": "Analysis Store provenance.",
"type": "object",
"properties": {
"path": {
"description": "Tagged absolute store path.",
"$ref": "#/$defs/TaggedPath"
},
"source": {
"description": "Store resolution source.",
"$ref": "#/$defs/StoreSource"
}
},
"required": [
"path",
"source"
]
},
"DecompilationData": {
"description": "Successful targeted decompilation data.",
"type": "object",
"properties": {
"decompilation": {
"description": "Complete normalized Ghidra C-like text.",
"$ref": "#/$defs/DecompiledText"
},
"function": {
"description": "Unambiguously resolved Function.",
"$ref": "#/$defs/SelectedFunction"
},
"requested_selector": {
"description": "Exact selector supplied by the caller.",
"$ref": "#/$defs/FunctionSelector"
}
},
"required": [
"requested_selector",
"function",
"decompilation"
]
},
"DecompilationSyntax": {
"description": "Decompiled text representation.",
"oneOf": [
{
"description": "Ghidra's C-like representation.",
"type": "string",
"const": "ghidra_c"
}
]
},
"DecompiledText": {
"description": "Complete decompiler text.",
"type": "object",
"properties": {
"syntax": {
"description": "Always `ghidra_c` in version 0.1.",
"$ref": "#/$defs/DecompilationSyntax"
},
"text": {
"description": "Complete text with only line endings normalized to LF.",
"type": "string"
}
},
"required": [
"syntax",
"text"
]
},
"Digest": {
"description": "A lowercase SHA-256 digest.",
"type": "string"
},
"FunctionSelector": {
"description": "Exactly one explicit Function Selector.",
"oneOf": [
{
"description": "Exact, case-sensitive Ghidra function name.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "name"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
]
},
{
"description": "Exact function entry address.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "address"
},
"value": {
"$ref": "#/$defs/Address"
}
},
"required": [
"kind",
"value"
]
}
]
},
"Limits": {
"description": "Resource policy selected for one invocation.",
"type": "object",
"properties": {
"analysis_timeout_seconds": {
"description": "Native analysis timeout.",
"$ref": "#/$defs/PositiveU64"
},
"child_watchdog_seconds": {
"description": "Derived process watchdog.",
"$ref": "#/$defs/PositiveU64"
},
"decompile_timeout_seconds": {
"description": "Optional native decompilation timeout.",
"anyOf": [
{
"$ref": "#/$defs/PositiveU64"
},
{
"type": "null"
}
]
},
"max_cpu": {
"description": "Maximum Ghidra analysis CPU count.",
"$ref": "#/$defs/PositiveU64"
},
"max_heap_mib": {
"description": "Maximum Java heap, in MiB.",
"$ref": "#/$defs/PositiveU64"
},
"max_inline_bytes": {
"description": "Maximum serialized successful stdout size.",
"$ref": "#/$defs/PositiveU64"
},
"max_sample_bytes": {
"description": "Maximum accepted Sample size.",
"$ref": "#/$defs/PositiveU64"
}
},
"required": [
"max_heap_mib",
"max_cpu",
"analysis_timeout_seconds",
"child_watchdog_seconds",
"max_sample_bytes",
"max_inline_bytes"
]
},
"PositiveU64": {
"description": "A validated positive integer accepted by resource-limit flags.",
"type": "integer",
"format": "uint64",
"minimum": 1
},
"QueryProvenance": {
"description": "Fixed provenance for every Sample-backed Query.",
"type": "object",
"properties": {
"adapter_protocol_version": {
"description": "Rust/Java protocol version.",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"analysis_profile_sha256": {
"description": "Canonical Analysis Profile identity.",
"$ref": "#/$defs/Digest"
},
"analysis_store": {
"description": "Selected Analysis Store.",
"$ref": "#/$defs/AnalysisStoreProvenance"
},
"ghidr_version": {
"description": "CLI package version.",
"type": "string"
},
"ghidra_version": {
"description": "Ghidra version.",
"type": "string"
},
"java_version": {
"description": "Java major version.",
"type": "string"
},
"limits": {
"description": "Selected and derived resource limits.",
"$ref": "#/$defs/Limits"
},
"sample_sha256": {
"description": "Sample content identity.",
"$ref": "#/$defs/Digest"
},
"sandbox": {
"description": "Selected sandbox policy.",
"$ref": "#/$defs/SandboxProvenance"
},
"source_path": {
"description": "Exact caller-supplied path.",
"$ref": "#/$defs/TaggedPath"
},
"target": {
"description": "Resolved Target Specification and verification.",
"$ref": "#/$defs/TargetProvenance"
}
},
"required": [
"ghidr_version",
"adapter_protocol_version",
"ghidra_version",
"java_version",
"sample_sha256",
"source_path",
"analysis_profile_sha256",
"target",
"analysis_store",
"sandbox",
"limits"
]
},
"SandboxBackend": {
"description": "Sandbox backend.",
"oneOf": [
{
"description": "Bubblewrap namespaces.",
"type": "string",
"const": "bubblewrap"
},
{
"description": "Caller-supplied isolation.",
"type": "string",
"const": "external"
},
{
"description": "Isolation disabled.",
"type": "string",
"const": "off"
}
]
},
"SandboxProvenance": {
"description": "Worker sandbox provenance.",
"type": "object",
"properties": {
"backend": {
"description": "Selected backend.",
"$ref": "#/$defs/SandboxBackend"
},
"verification": {
"description": "Isolation verification state.",
"$ref": "#/$defs/SandboxVerification"
}
},
"required": [
"backend",
"verification"
]
},
"SandboxVerification": {
"description": "Sandbox verification state.",
"oneOf": [
{
"description": "Verified by `ghidr`.",
"type": "string",
"const": "verified"
},
{
"description": "Declared but not verifiable by `ghidr`.",
"type": "string",
"const": "unverified"
},
{
"description": "Explicitly disabled.",
"type": "string",
"const": "disabled"
}
]
},
"SelectedFunction": {
"description": "Identity of a selected Function.",
"type": "object",
"properties": {
"entry": {
"description": "Entry Address.",
"$ref": "#/$defs/Address"
},
"name": {
"description": "Exact basename.",
"type": "string"
},
"qualified_name": {
"description": "Exact fully-qualified name.",
"type": "string"
}
},
"required": [
"name",
"qualified_name",
"entry"
]
},
"StoreSource": {
"description": "Analysis Store resolution source.",
"oneOf": [
{
"description": "Explicit `--store` flag.",
"type": "string",
"const": "cli"
},
{
"description": "`GHIDR_STORE` environment variable.",
"type": "string",
"const": "environment"
},
{
"description": "Explicit `XDG_CACHE_HOME` fallback.",
"type": "string",
"const": "xdg"
},
{
"description": "Home-directory fallback.",
"type": "string",
"const": "home_fallback"
}
]
},
"TaggedPath": {
"description": "A path preserving either UTF-8 text or exact Unix bytes.",
"oneOf": [
{
"description": "An unmodified UTF-8 path.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "utf8"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
},
{
"description": "Standard padded Base64 over exact Unix path bytes.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "unix_bytes_base64"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
}
]
},
"TargetProvenance": {
"description": "Target Specification provenance.",
"type": "object",
"properties": {
"compiler_specification": {
"description": "Exact compiler-specification identifier.",
"type": "string"
},
"format": {
"description": "Executable format name.",
"type": "string"
},
"loader": {
"description": "Exact loader identifier.",
"type": "string"
},
"processor_language": {
"description": "Exact processor-language identifier.",
"type": "string"
},
"verification": {
"description": "Project integration-test coverage.",
"$ref": "#/$defs/TargetVerification"
}
},
"required": [
"loader",
"format",
"processor_language",
"compiler_specification",
"verification"
]
},
"TargetVerification": {
"description": "Project integration-test coverage for a recognized target.",
"oneOf": [
{
"description": "Covered by version 0.1 real-Ghidra fixtures.",
"type": "string",
"const": "verified"
},
{
"description": "Recognized by Ghidra but outside the tested matrix.",
"type": "string",
"const": "unverified"
}
]
},
"Warning": {
"description": "Stable non-fatal warning.",
"type": "object",
"properties": {
"code": {
"description": "Stable machine identifier.",
"type": "string"
},
"details": {
"description": "Warning-specific context, always an object.",
"type": "object",
"additionalProperties": true
},
"message": {
"description": "Concise human context.",
"type": "string"
}
},
"required": [
"code",
"message",
"details"
]
}
}
}

View file

@ -0,0 +1,304 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SuccessEnvelope",
"description": "Versioned successful response.",
"type": "object",
"properties": {
"data": {
"description": "Operation-specific result.",
"$ref": "#/$defs/DoctorData"
},
"kind": {
"type": "string",
"const": "doctor"
},
"provenance": {
"description": "Reproducibility and execution context.",
"$ref": "#/$defs/ToolProvenance"
},
"schema_version": {
"type": "integer",
"const": 1
},
"warnings": {
"description": "Deduplicated stable warnings.",
"type": "array",
"items": {
"$ref": "#/$defs/Warning"
}
}
},
"required": [
"schema_version",
"kind",
"provenance",
"data",
"warnings"
],
"$defs": {
"AnalysisStoreProvenance": {
"description": "Analysis Store provenance.",
"type": "object",
"properties": {
"path": {
"description": "Tagged absolute store path.",
"$ref": "#/$defs/TaggedPath"
},
"source": {
"description": "Store resolution source.",
"$ref": "#/$defs/StoreSource"
}
},
"required": [
"path",
"source"
]
},
"ComponentReport": {
"description": "One diagnosed component.",
"type": "object",
"properties": {
"details": {
"description": "Component-specific facts.",
"type": "object",
"additionalProperties": true
},
"status": {
"description": "Stable readiness classification.",
"$ref": "#/$defs/ComponentStatus"
}
},
"required": [
"status",
"details"
]
},
"ComponentStatus": {
"description": "Doctor component readiness classification.",
"oneOf": [
{
"description": "Component is usable.",
"type": "string",
"const": "ready"
},
{
"description": "Component is absent.",
"type": "string",
"const": "missing"
},
{
"description": "Component version is unsupported.",
"type": "string",
"const": "incompatible"
},
{
"description": "Component configuration is invalid.",
"type": "string",
"const": "invalid"
},
{
"description": "Component storage cannot be written.",
"type": "string",
"const": "unwritable"
}
]
},
"DoctorData": {
"description": "Installation diagnosis data.",
"type": "object",
"properties": {
"components": {
"description": "Typed component reports keyed by stable component name.",
"type": "object",
"additionalProperties": {
"$ref": "#/$defs/ComponentReport"
}
},
"ready": {
"description": "True only when every required component is ready.",
"type": "boolean"
}
},
"required": [
"ready",
"components"
]
},
"SandboxBackend": {
"description": "Sandbox backend.",
"oneOf": [
{
"description": "Bubblewrap namespaces.",
"type": "string",
"const": "bubblewrap"
},
{
"description": "Caller-supplied isolation.",
"type": "string",
"const": "external"
},
{
"description": "Isolation disabled.",
"type": "string",
"const": "off"
}
]
},
"SandboxProvenance": {
"description": "Worker sandbox provenance.",
"type": "object",
"properties": {
"backend": {
"description": "Selected backend.",
"$ref": "#/$defs/SandboxBackend"
},
"verification": {
"description": "Isolation verification state.",
"$ref": "#/$defs/SandboxVerification"
}
},
"required": [
"backend",
"verification"
]
},
"SandboxVerification": {
"description": "Sandbox verification state.",
"oneOf": [
{
"description": "Verified by `ghidr`.",
"type": "string",
"const": "verified"
},
{
"description": "Declared but not verifiable by `ghidr`.",
"type": "string",
"const": "unverified"
},
{
"description": "Explicitly disabled.",
"type": "string",
"const": "disabled"
}
]
},
"StoreSource": {
"description": "Analysis Store resolution source.",
"oneOf": [
{
"description": "Explicit `--store` flag.",
"type": "string",
"const": "cli"
},
{
"description": "`GHIDR_STORE` environment variable.",
"type": "string",
"const": "environment"
},
{
"description": "Explicit `XDG_CACHE_HOME` fallback.",
"type": "string",
"const": "xdg"
},
{
"description": "Home-directory fallback.",
"type": "string",
"const": "home_fallback"
}
]
},
"TaggedPath": {
"description": "A path preserving either UTF-8 text or exact Unix bytes.",
"oneOf": [
{
"description": "An unmodified UTF-8 path.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "utf8"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
},
{
"description": "Standard padded Base64 over exact Unix path bytes.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "unix_bytes_base64"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
}
]
},
"ToolProvenance": {
"description": "Minimal provenance available to non-Sample operations.",
"type": "object",
"properties": {
"adapter_protocol_version": {
"description": "Rust/Java protocol version.",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"analysis_store": {
"description": "Selected Analysis Store.",
"$ref": "#/$defs/AnalysisStoreProvenance"
},
"ghidr_version": {
"description": "CLI package version.",
"type": "string"
},
"sandbox": {
"description": "Selected sandbox policy.",
"$ref": "#/$defs/SandboxProvenance"
}
},
"required": [
"ghidr_version",
"adapter_protocol_version",
"analysis_store",
"sandbox"
]
},
"Warning": {
"description": "Stable non-fatal warning.",
"type": "object",
"properties": {
"code": {
"description": "Stable machine identifier.",
"type": "string"
},
"details": {
"description": "Warning-specific context, always an object.",
"type": "object",
"additionalProperties": true
},
"message": {
"description": "Concise human context.",
"type": "string"
}
},
"required": [
"code",
"message",
"details"
]
}
}
}

View file

@ -0,0 +1,241 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "ErrorEnvelope",
"description": "Versioned failure document.",
"type": "object",
"properties": {
"error": {
"description": "Typed failure body.",
"$ref": "#/$defs/ErrorBody"
},
"kind": {
"description": "Always `error`.",
"$ref": "#/$defs/ErrorKind"
},
"schema_version": {
"type": "integer",
"const": 1
}
},
"required": [
"schema_version",
"kind",
"error"
],
"$defs": {
"ErrorBody": {
"description": "Stable public error body.",
"type": "object",
"properties": {
"code": {
"description": "Stable machine identifier.",
"$ref": "#/$defs/ErrorCode"
},
"details": {
"description": "Code-specific structured context, always an object.",
"type": "object",
"additionalProperties": true
},
"message": {
"description": "Concise context that is not a control-flow interface.",
"type": "string"
},
"retryable": {
"description": "Whether the same logical request might later succeed unchanged.",
"type": "boolean"
}
},
"required": [
"code",
"message",
"retryable",
"details"
]
},
"ErrorCode": {
"description": "Stable machine-readable error taxonomy available in the foundation.",
"oneOf": [
{
"description": "Command-line syntax or validation failed.",
"type": "string",
"const": "invalid_arguments"
},
{
"description": "The caller's Sample path does not exist.",
"type": "string",
"const": "sample_not_found"
},
{
"description": "The caller's Sample cannot be read.",
"type": "string",
"const": "sample_unreadable"
},
{
"description": "The opened Sample is not a regular file.",
"type": "string",
"const": "invalid_sample_type"
},
{
"description": "The Sample exceeded the selected byte ceiling.",
"type": "string",
"const": "sample_too_large"
},
{
"description": "Source metadata changed while snapshotting.",
"type": "string",
"const": "sample_changed"
},
{
"description": "The Analysis Store lacks the required free-space reserve.",
"type": "string",
"const": "insufficient_store_space"
},
{
"description": "Ghidra could not recognize a Target Specification.",
"type": "string",
"const": "unsupported_target"
},
{
"description": "Ghidra found multiple Target Specification candidates.",
"type": "string",
"const": "ambiguous_target"
},
{
"description": "Ghidra is not installed.",
"type": "string",
"const": "ghidra_missing"
},
{
"description": "The installed Ghidra version is incompatible.",
"type": "string",
"const": "ghidra_incompatible"
},
{
"description": "Java is not installed.",
"type": "string",
"const": "java_missing"
},
{
"description": "The installed Java version is incompatible.",
"type": "string",
"const": "java_incompatible"
},
{
"description": "A later integration layer has not connected execution yet.",
"type": "string",
"const": "internal_not_implemented"
},
{
"description": "Rust/Java request or response validation failed.",
"type": "string",
"const": "protocol_violation"
},
{
"description": "The child exceeded a native or watchdog timeout.",
"type": "string",
"const": "timeout"
},
{
"description": "Native auto-analysis timed out.",
"type": "string",
"const": "analysis_timeout"
},
{
"description": "Ghidra auto-analysis failed.",
"type": "string",
"const": "analysis_failed"
},
{
"description": "Native Function decompilation timed out.",
"type": "string",
"const": "decompile_timeout"
},
{
"description": "Ghidra Function decompilation failed.",
"type": "string",
"const": "decompilation_failed"
},
{
"description": "The caller interrupted the operation.",
"type": "string",
"const": "interrupted"
},
{
"description": "The adapter could not serialize a bounded response.",
"type": "string",
"const": "result_too_large"
},
{
"description": "The success descriptor cannot fit the requested inline budget.",
"type": "string",
"const": "inline_budget_too_small"
},
{
"description": "Store-wide cleanup lacks explicit confirmation.",
"type": "string",
"const": "confirmation_required"
},
{
"description": "Another process holds the selected Analysis lock.",
"type": "string",
"const": "analysis_busy"
},
{
"description": "Stored Analysis validation failed and recovery did not succeed.",
"type": "string",
"const": "corrupt_analysis"
},
{
"description": "A cleanup transaction requires a recoverable retry.",
"type": "string",
"const": "cleanup_incomplete"
},
{
"description": "The resolved Analysis Store path is invalid.",
"type": "string",
"const": "invalid_store_path"
},
{
"description": "A Function Selector resolved to no Function.",
"type": "string",
"const": "function_not_found"
},
{
"description": "A Function Selector resolved to multiple Functions.",
"type": "string",
"const": "function_selector_ambiguous"
},
{
"description": "An Address is not a Function entry.",
"type": "string",
"const": "function_entry_required"
},
{
"description": "Ghidra does not contain the requested address space.",
"type": "string",
"const": "address_space_not_found"
},
{
"description": "Installation diagnosis found an unusable component.",
"type": "string",
"const": "doctor_failed"
},
{
"description": "A general runtime or integration failure.",
"type": "string",
"const": "internal"
}
]
},
"ErrorKind": {
"description": "Error envelope discriminator.",
"oneOf": [
{
"description": "A failed invocation.",
"type": "string",
"const": "error"
}
]
}
}
}

View file

@ -0,0 +1,539 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SuccessEnvelope",
"description": "Versioned successful response.",
"type": "object",
"properties": {
"data": {
"description": "Operation-specific result.",
"$ref": "#/$defs/FunctionsData"
},
"kind": {
"type": "string",
"const": "functions"
},
"provenance": {
"description": "Reproducibility and execution context.",
"$ref": "#/$defs/QueryProvenance"
},
"schema_version": {
"type": "integer",
"const": 1
},
"warnings": {
"description": "Deduplicated stable warnings.",
"type": "array",
"items": {
"$ref": "#/$defs/Warning"
}
}
},
"required": [
"schema_version",
"kind",
"provenance",
"data",
"warnings"
],
"$defs": {
"Address": {
"description": "A Ghidra address with an explicit, case-sensitive address space.",
"type": "object",
"properties": {
"offset": {
"description": "Canonical lowercase, `0x`-prefixed, even-width hexadecimal offset.",
"type": "string"
},
"space": {
"description": "Exact Ghidra address-space name.",
"type": "string"
}
},
"required": [
"space",
"offset"
]
},
"AnalysisStoreProvenance": {
"description": "Analysis Store provenance.",
"type": "object",
"properties": {
"path": {
"description": "Tagged absolute store path.",
"$ref": "#/$defs/TaggedPath"
},
"source": {
"description": "Store resolution source.",
"$ref": "#/$defs/StoreSource"
}
},
"required": [
"path",
"source"
]
},
"Digest": {
"description": "A lowercase SHA-256 digest.",
"type": "string"
},
"FunctionItem": {
"description": "Minimal version 0.1 Function record.",
"type": "object",
"properties": {
"body_address_count": {
"description": "Number of Addresses in the body.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"decompilable": {
"description": "Whether the Function is a valid decompilation target.",
"type": "boolean"
},
"entry": {
"description": "Function entry Address.",
"$ref": "#/$defs/Address"
},
"is_external": {
"description": "Whether Ghidra marks the Function external.",
"type": "boolean"
},
"is_thunk": {
"description": "Whether Ghidra marks the Function a thunk.",
"type": "boolean"
},
"location": {
"description": "Memory or external location.",
"$ref": "#/$defs/FunctionLocation"
},
"name": {
"description": "Exact basename.",
"type": "string"
},
"qualified_name": {
"description": "Exact fully-qualified name.",
"type": "string"
},
"thunk_target_entry": {
"description": "Resolved thunk target entry when available.",
"anyOf": [
{
"$ref": "#/$defs/Address"
},
{
"type": "null"
}
]
}
},
"required": [
"name",
"qualified_name",
"entry",
"body_address_count",
"location",
"is_external",
"is_thunk",
"decompilable"
]
},
"FunctionLocation": {
"description": "Function location class used by stable ordering.",
"oneOf": [
{
"description": "Function has a memory Address.",
"type": "string",
"const": "memory"
},
{
"description": "External/imported Function.",
"type": "string",
"const": "external"
}
]
},
"FunctionsData": {
"description": "Function collection data.",
"type": "object",
"properties": {
"items": {
"description": "Function records in documented order.",
"type": "array",
"items": {
"$ref": "#/$defs/FunctionItem"
}
},
"page": {
"description": "Stable page facts.",
"$ref": "#/$defs/PageMetadata"
}
},
"required": [
"page",
"items"
]
},
"Limits": {
"description": "Resource policy selected for one invocation.",
"type": "object",
"properties": {
"analysis_timeout_seconds": {
"description": "Native analysis timeout.",
"$ref": "#/$defs/PositiveU64"
},
"child_watchdog_seconds": {
"description": "Derived process watchdog.",
"$ref": "#/$defs/PositiveU64"
},
"decompile_timeout_seconds": {
"description": "Optional native decompilation timeout.",
"anyOf": [
{
"$ref": "#/$defs/PositiveU64"
},
{
"type": "null"
}
]
},
"max_cpu": {
"description": "Maximum Ghidra analysis CPU count.",
"$ref": "#/$defs/PositiveU64"
},
"max_heap_mib": {
"description": "Maximum Java heap, in MiB.",
"$ref": "#/$defs/PositiveU64"
},
"max_inline_bytes": {
"description": "Maximum serialized successful stdout size.",
"$ref": "#/$defs/PositiveU64"
},
"max_sample_bytes": {
"description": "Maximum accepted Sample size.",
"$ref": "#/$defs/PositiveU64"
}
},
"required": [
"max_heap_mib",
"max_cpu",
"analysis_timeout_seconds",
"child_watchdog_seconds",
"max_sample_bytes",
"max_inline_bytes"
]
},
"PageMetadata": {
"description": "Stable page metadata.",
"type": "object",
"properties": {
"has_more": {
"description": "Whether a subsequent page exists.",
"type": "boolean"
},
"limit": {
"description": "Applied item bound, null only for explicit `--all`.",
"type": [
"integer",
"null"
],
"format": "uint64",
"minimum": 0
},
"offset": {
"description": "Number of items skipped.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"order": {
"description": "Stable order identifier.",
"type": "string"
},
"returned": {
"description": "Number of returned items.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"total": {
"description": "Total items in the immutable Analysis.",
"type": "integer",
"format": "uint64",
"minimum": 0
}
},
"required": [
"order",
"offset",
"returned",
"total",
"has_more"
]
},
"PositiveU64": {
"description": "A validated positive integer accepted by resource-limit flags.",
"type": "integer",
"format": "uint64",
"minimum": 1
},
"QueryProvenance": {
"description": "Fixed provenance for every Sample-backed Query.",
"type": "object",
"properties": {
"adapter_protocol_version": {
"description": "Rust/Java protocol version.",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"analysis_profile_sha256": {
"description": "Canonical Analysis Profile identity.",
"$ref": "#/$defs/Digest"
},
"analysis_store": {
"description": "Selected Analysis Store.",
"$ref": "#/$defs/AnalysisStoreProvenance"
},
"ghidr_version": {
"description": "CLI package version.",
"type": "string"
},
"ghidra_version": {
"description": "Ghidra version.",
"type": "string"
},
"java_version": {
"description": "Java major version.",
"type": "string"
},
"limits": {
"description": "Selected and derived resource limits.",
"$ref": "#/$defs/Limits"
},
"sample_sha256": {
"description": "Sample content identity.",
"$ref": "#/$defs/Digest"
},
"sandbox": {
"description": "Selected sandbox policy.",
"$ref": "#/$defs/SandboxProvenance"
},
"source_path": {
"description": "Exact caller-supplied path.",
"$ref": "#/$defs/TaggedPath"
},
"target": {
"description": "Resolved Target Specification and verification.",
"$ref": "#/$defs/TargetProvenance"
}
},
"required": [
"ghidr_version",
"adapter_protocol_version",
"ghidra_version",
"java_version",
"sample_sha256",
"source_path",
"analysis_profile_sha256",
"target",
"analysis_store",
"sandbox",
"limits"
]
},
"SandboxBackend": {
"description": "Sandbox backend.",
"oneOf": [
{
"description": "Bubblewrap namespaces.",
"type": "string",
"const": "bubblewrap"
},
{
"description": "Caller-supplied isolation.",
"type": "string",
"const": "external"
},
{
"description": "Isolation disabled.",
"type": "string",
"const": "off"
}
]
},
"SandboxProvenance": {
"description": "Worker sandbox provenance.",
"type": "object",
"properties": {
"backend": {
"description": "Selected backend.",
"$ref": "#/$defs/SandboxBackend"
},
"verification": {
"description": "Isolation verification state.",
"$ref": "#/$defs/SandboxVerification"
}
},
"required": [
"backend",
"verification"
]
},
"SandboxVerification": {
"description": "Sandbox verification state.",
"oneOf": [
{
"description": "Verified by `ghidr`.",
"type": "string",
"const": "verified"
},
{
"description": "Declared but not verifiable by `ghidr`.",
"type": "string",
"const": "unverified"
},
{
"description": "Explicitly disabled.",
"type": "string",
"const": "disabled"
}
]
},
"StoreSource": {
"description": "Analysis Store resolution source.",
"oneOf": [
{
"description": "Explicit `--store` flag.",
"type": "string",
"const": "cli"
},
{
"description": "`GHIDR_STORE` environment variable.",
"type": "string",
"const": "environment"
},
{
"description": "Explicit `XDG_CACHE_HOME` fallback.",
"type": "string",
"const": "xdg"
},
{
"description": "Home-directory fallback.",
"type": "string",
"const": "home_fallback"
}
]
},
"TaggedPath": {
"description": "A path preserving either UTF-8 text or exact Unix bytes.",
"oneOf": [
{
"description": "An unmodified UTF-8 path.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "utf8"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
},
{
"description": "Standard padded Base64 over exact Unix path bytes.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "unix_bytes_base64"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
}
]
},
"TargetProvenance": {
"description": "Target Specification provenance.",
"type": "object",
"properties": {
"compiler_specification": {
"description": "Exact compiler-specification identifier.",
"type": "string"
},
"format": {
"description": "Executable format name.",
"type": "string"
},
"loader": {
"description": "Exact loader identifier.",
"type": "string"
},
"processor_language": {
"description": "Exact processor-language identifier.",
"type": "string"
},
"verification": {
"description": "Project integration-test coverage.",
"$ref": "#/$defs/TargetVerification"
}
},
"required": [
"loader",
"format",
"processor_language",
"compiler_specification",
"verification"
]
},
"TargetVerification": {
"description": "Project integration-test coverage for a recognized target.",
"oneOf": [
{
"description": "Covered by version 0.1 real-Ghidra fixtures.",
"type": "string",
"const": "verified"
},
{
"description": "Recognized by Ghidra but outside the tested matrix.",
"type": "string",
"const": "unverified"
}
]
},
"Warning": {
"description": "Stable non-fatal warning.",
"type": "object",
"properties": {
"code": {
"description": "Stable machine identifier.",
"type": "string"
},
"details": {
"description": "Warning-specific context, always an object.",
"type": "object",
"additionalProperties": true
},
"message": {
"description": "Concise human context.",
"type": "string"
}
},
"required": [
"code",
"message",
"details"
]
}
}
}

View file

@ -0,0 +1,518 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "SuccessEnvelope",
"description": "Versioned successful response.",
"type": "object",
"properties": {
"data": {
"description": "Operation-specific result.",
"$ref": "#/$defs/InspectionData"
},
"kind": {
"type": "string",
"const": "inspection"
},
"provenance": {
"description": "Reproducibility and execution context.",
"$ref": "#/$defs/QueryProvenance"
},
"schema_version": {
"type": "integer",
"const": 1
},
"warnings": {
"description": "Deduplicated stable warnings.",
"type": "array",
"items": {
"$ref": "#/$defs/Warning"
}
}
},
"required": [
"schema_version",
"kind",
"provenance",
"data",
"warnings"
],
"$defs": {
"Address": {
"description": "A Ghidra address with an explicit, case-sensitive address space.",
"type": "object",
"properties": {
"offset": {
"description": "Canonical lowercase, `0x`-prefixed, even-width hexadecimal offset.",
"type": "string"
},
"space": {
"description": "Exact Ghidra address-space name.",
"type": "string"
}
},
"required": [
"space",
"offset"
]
},
"AnalysisDisposition": {
"description": "Analysis selection disposition.",
"oneOf": [
{
"description": "A new Analysis was created.",
"type": "string",
"const": "created"
},
{
"description": "A compatible Analysis was reused.",
"type": "string",
"const": "reused"
},
{
"description": "Corrupt stored data was quarantined and rebuilt once.",
"type": "string",
"const": "rebuilt"
}
]
},
"AnalysisStoreProvenance": {
"description": "Analysis Store provenance.",
"type": "object",
"properties": {
"path": {
"description": "Tagged absolute store path.",
"$ref": "#/$defs/TaggedPath"
},
"source": {
"description": "Store resolution source.",
"$ref": "#/$defs/StoreSource"
}
},
"required": [
"path",
"source"
]
},
"AnalysisSummary": {
"description": "Analysis selection summary.",
"type": "object",
"properties": {
"disposition": {
"description": "Whether compatible analysis was built or reused.",
"$ref": "#/$defs/AnalysisDisposition"
}
},
"required": [
"disposition"
]
},
"Digest": {
"description": "A lowercase SHA-256 digest.",
"type": "string"
},
"InspectionData": {
"description": "Sample inspection data.",
"type": "object",
"properties": {
"analysis": {
"description": "Reuse disposition.",
"$ref": "#/$defs/AnalysisSummary"
},
"program": {
"description": "Program address and Function summary.",
"$ref": "#/$defs/ProgramSummary"
},
"sample": {
"description": "Sample identity and size.",
"$ref": "#/$defs/SampleSummary"
}
},
"required": [
"sample",
"program",
"analysis"
]
},
"Limits": {
"description": "Resource policy selected for one invocation.",
"type": "object",
"properties": {
"analysis_timeout_seconds": {
"description": "Native analysis timeout.",
"$ref": "#/$defs/PositiveU64"
},
"child_watchdog_seconds": {
"description": "Derived process watchdog.",
"$ref": "#/$defs/PositiveU64"
},
"decompile_timeout_seconds": {
"description": "Optional native decompilation timeout.",
"anyOf": [
{
"$ref": "#/$defs/PositiveU64"
},
{
"type": "null"
}
]
},
"max_cpu": {
"description": "Maximum Ghidra analysis CPU count.",
"$ref": "#/$defs/PositiveU64"
},
"max_heap_mib": {
"description": "Maximum Java heap, in MiB.",
"$ref": "#/$defs/PositiveU64"
},
"max_inline_bytes": {
"description": "Maximum serialized successful stdout size.",
"$ref": "#/$defs/PositiveU64"
},
"max_sample_bytes": {
"description": "Maximum accepted Sample size.",
"$ref": "#/$defs/PositiveU64"
}
},
"required": [
"max_heap_mib",
"max_cpu",
"analysis_timeout_seconds",
"child_watchdog_seconds",
"max_sample_bytes",
"max_inline_bytes"
]
},
"PositiveU64": {
"description": "A validated positive integer accepted by resource-limit flags.",
"type": "integer",
"format": "uint64",
"minimum": 1
},
"ProgramSummary": {
"description": "Program summary.",
"type": "object",
"properties": {
"function_count": {
"description": "Count of memory, external, and thunk Functions.",
"type": "integer",
"format": "uint64",
"minimum": 0
},
"image_base": {
"description": "Image base when applicable.",
"anyOf": [
{
"$ref": "#/$defs/Address"
},
{
"type": "null"
}
]
},
"maximum_address": {
"description": "Maximum Address when applicable.",
"anyOf": [
{
"$ref": "#/$defs/Address"
},
{
"type": "null"
}
]
},
"minimum_address": {
"description": "Minimum Address when applicable.",
"anyOf": [
{
"$ref": "#/$defs/Address"
},
{
"type": "null"
}
]
}
},
"required": [
"function_count"
]
},
"QueryProvenance": {
"description": "Fixed provenance for every Sample-backed Query.",
"type": "object",
"properties": {
"adapter_protocol_version": {
"description": "Rust/Java protocol version.",
"type": "integer",
"format": "uint32",
"minimum": 0
},
"analysis_profile_sha256": {
"description": "Canonical Analysis Profile identity.",
"$ref": "#/$defs/Digest"
},
"analysis_store": {
"description": "Selected Analysis Store.",
"$ref": "#/$defs/AnalysisStoreProvenance"
},
"ghidr_version": {
"description": "CLI package version.",
"type": "string"
},
"ghidra_version": {
"description": "Ghidra version.",
"type": "string"
},
"java_version": {
"description": "Java major version.",
"type": "string"
},
"limits": {
"description": "Selected and derived resource limits.",
"$ref": "#/$defs/Limits"
},
"sample_sha256": {
"description": "Sample content identity.",
"$ref": "#/$defs/Digest"
},
"sandbox": {
"description": "Selected sandbox policy.",
"$ref": "#/$defs/SandboxProvenance"
},
"source_path": {
"description": "Exact caller-supplied path.",
"$ref": "#/$defs/TaggedPath"
},
"target": {
"description": "Resolved Target Specification and verification.",
"$ref": "#/$defs/TargetProvenance"
}
},
"required": [
"ghidr_version",
"adapter_protocol_version",
"ghidra_version",
"java_version",
"sample_sha256",
"source_path",
"analysis_profile_sha256",
"target",
"analysis_store",
"sandbox",
"limits"
]
},
"SampleSummary": {
"description": "Sample identity summary.",
"type": "object",
"properties": {
"sha256": {
"description": "SHA-256 content identity.",
"$ref": "#/$defs/Digest"
},
"size_bytes": {
"description": "Exact byte length.",
"type": "integer",
"format": "uint64",
"minimum": 0
}
},
"required": [
"sha256",
"size_bytes"
]
},
"SandboxBackend": {
"description": "Sandbox backend.",
"oneOf": [
{
"description": "Bubblewrap namespaces.",
"type": "string",
"const": "bubblewrap"
},
{
"description": "Caller-supplied isolation.",
"type": "string",
"const": "external"
},
{
"description": "Isolation disabled.",
"type": "string",
"const": "off"
}
]
},
"SandboxProvenance": {
"description": "Worker sandbox provenance.",
"type": "object",
"properties": {
"backend": {
"description": "Selected backend.",
"$ref": "#/$defs/SandboxBackend"
},
"verification": {
"description": "Isolation verification state.",
"$ref": "#/$defs/SandboxVerification"
}
},
"required": [
"backend",
"verification"
]
},
"SandboxVerification": {
"description": "Sandbox verification state.",
"oneOf": [
{
"description": "Verified by `ghidr`.",
"type": "string",
"const": "verified"
},
{
"description": "Declared but not verifiable by `ghidr`.",
"type": "string",
"const": "unverified"
},
{
"description": "Explicitly disabled.",
"type": "string",
"const": "disabled"
}
]
},
"StoreSource": {
"description": "Analysis Store resolution source.",
"oneOf": [
{
"description": "Explicit `--store` flag.",
"type": "string",
"const": "cli"
},
{
"description": "`GHIDR_STORE` environment variable.",
"type": "string",
"const": "environment"
},
{
"description": "Explicit `XDG_CACHE_HOME` fallback.",
"type": "string",
"const": "xdg"
},
{
"description": "Home-directory fallback.",
"type": "string",
"const": "home_fallback"
}
]
},
"TaggedPath": {
"description": "A path preserving either UTF-8 text or exact Unix bytes.",
"oneOf": [
{
"description": "An unmodified UTF-8 path.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "utf8"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
},
{
"description": "Standard padded Base64 over exact Unix path bytes.",
"type": "object",
"properties": {
"encoding": {
"type": "string",
"const": "unix_bytes_base64"
},
"value": {
"type": "string"
}
},
"required": [
"encoding",
"value"
]
}
]
},
"TargetProvenance": {
"description": "Target Specification provenance.",
"type": "object",
"properties": {
"compiler_specification": {
"description": "Exact compiler-specification identifier.",
"type": "string"
},
"format": {
"description": "Executable format name.",
"type": "string"
},
"loader": {
"description": "Exact loader identifier.",
"type": "string"
},
"processor_language": {
"description": "Exact processor-language identifier.",
"type": "string"
},
"verification": {
"description": "Project integration-test coverage.",
"$ref": "#/$defs/TargetVerification"
}
},
"required": [
"loader",
"format",
"processor_language",
"compiler_specification",
"verification"
]
},
"TargetVerification": {
"description": "Project integration-test coverage for a recognized target.",
"oneOf": [
{
"description": "Covered by version 0.1 real-Ghidra fixtures.",
"type": "string",
"const": "verified"
},
{
"description": "Recognized by Ghidra but outside the tested matrix.",
"type": "string",
"const": "unverified"
}
]
},
"Warning": {
"description": "Stable non-fatal warning.",
"type": "object",
"properties": {
"code": {
"description": "Stable machine identifier.",
"type": "string"
},
"details": {
"description": "Warning-specific context, always an object.",
"type": "object",
"additionalProperties": true
},
"message": {
"description": "Concise human context.",
"type": "string"
}
},
"required": [
"code",
"message",
"details"
]
}
}
}

View file

@ -0,0 +1,341 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "AdapterRequest",
"description": "Strict adapter request.",
"type": "object",
"properties": {
"analysis_path": {
"description": "Private Ghidra project or staging directory when required.",
"anyOf": [
{
"$ref": "#/$defs/WorkerPath"
},
{
"type": "null"
}
]
},
"arguments": {
"description": "Operation-specific validated arguments.",
"$ref": "#/$defs/RequestArguments"
},
"invocation_id": {
"description": "Random 128-bit identifier echoed by the response.",
"$ref": "#/$defs/InvocationId"
},
"limits": {
"description": "Selected limits.",
"$ref": "#/$defs/Limits"
},
"operation": {
"description": "Operation echoed by the response.",
"$ref": "#/$defs/Operation"
},
"protocol_version": {
"type": "integer",
"const": 1
},
"staged_sample": {
"description": "Private staged Sample path, absent for non-Sample operations.",
"anyOf": [
{
"$ref": "#/$defs/WorkerPath"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false,
"required": [
"protocol_version",
"invocation_id",
"operation",
"limits",
"arguments"
],
"$defs": {
"Address": {
"description": "A Ghidra address with an explicit, case-sensitive address space.",
"type": "object",
"properties": {
"offset": {
"description": "Canonical lowercase, `0x`-prefixed, even-width hexadecimal offset.",
"type": "string"
},
"space": {
"description": "Exact Ghidra address-space name.",
"type": "string"
}
},
"additionalProperties": false,
"required": [
"space",
"offset"
]
},
"CollectionLimit": {
"description": "An optional item bound, explicit when unbounded.",
"oneOf": [
{
"description": "At most the enclosed number of items.",
"type": "object",
"properties": {
"bounded": {
"$ref": "#/$defs/PositiveU64"
}
},
"additionalProperties": false,
"required": [
"bounded"
]
},
{
"description": "Every item, still subject to the serialized-byte budget.",
"type": "string",
"const": "all"
}
]
},
"FunctionSelector": {
"description": "Exactly one explicit Function Selector.",
"oneOf": [
{
"description": "Exact, case-sensitive Ghidra function name.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "name"
},
"value": {
"type": "string"
}
},
"required": [
"kind",
"value"
]
},
{
"description": "Exact function entry address.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "address"
},
"value": {
"$ref": "#/$defs/Address"
}
},
"required": [
"kind",
"value"
]
}
]
},
"InvocationId": {
"description": "A random 128-bit invocation identifier encoded as 32 lowercase hex digits.",
"type": "string"
},
"Limits": {
"description": "Resource policy selected for one invocation.",
"type": "object",
"properties": {
"analysis_timeout_seconds": {
"description": "Native analysis timeout.",
"$ref": "#/$defs/PositiveU64"
},
"child_watchdog_seconds": {
"description": "Derived process watchdog.",
"$ref": "#/$defs/PositiveU64"
},
"decompile_timeout_seconds": {
"description": "Optional native decompilation timeout.",
"anyOf": [
{
"$ref": "#/$defs/PositiveU64"
},
{
"type": "null"
}
]
},
"max_cpu": {
"description": "Maximum Ghidra analysis CPU count.",
"$ref": "#/$defs/PositiveU64"
},
"max_heap_mib": {
"description": "Maximum Java heap, in MiB.",
"$ref": "#/$defs/PositiveU64"
},
"max_inline_bytes": {
"description": "Maximum serialized successful stdout size.",
"$ref": "#/$defs/PositiveU64"
},
"max_sample_bytes": {
"description": "Maximum accepted Sample size.",
"$ref": "#/$defs/PositiveU64"
}
},
"additionalProperties": false,
"required": [
"max_heap_mib",
"max_cpu",
"analysis_timeout_seconds",
"child_watchdog_seconds",
"max_sample_bytes",
"max_inline_bytes"
]
},
"Operation": {
"description": "Version 0.1 operation identifier shared with Java.",
"oneOf": [
{
"description": "Installation diagnosis.",
"type": "string",
"const": "doctor"
},
{
"description": "Sample metadata inspection.",
"type": "string",
"const": "inspect"
},
{
"description": "Stable Function collection.",
"type": "string",
"const": "functions"
},
{
"description": "Targeted Function decompilation.",
"type": "string",
"const": "decompile"
},
{
"description": "Explicit Analysis Store cleanup.",
"type": "string",
"const": "clean"
}
]
},
"PageRequest": {
"description": "A normalized collection page request.",
"type": "object",
"properties": {
"limit": {
"description": "Selected item bound.",
"$ref": "#/$defs/CollectionLimit"
},
"offset": {
"description": "Number of items skipped in stable order.",
"type": "integer",
"format": "uint64",
"minimum": 0
}
},
"additionalProperties": false,
"required": [
"offset",
"limit"
]
},
"PositiveU64": {
"description": "A validated positive integer accepted by resource-limit flags.",
"type": "integer",
"format": "uint64",
"minimum": 1
},
"RequestArguments": {
"description": "Operation-specific request arguments.",
"oneOf": [
{
"description": "Installation probe.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "doctor"
}
},
"additionalProperties": false,
"required": [
"kind"
]
},
{
"description": "Program metadata extraction.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "inspect"
}
},
"additionalProperties": false,
"required": [
"kind"
]
},
{
"description": "Stable Function page extraction.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "functions"
},
"page": {
"description": "Explicit page bound.",
"$ref": "#/$defs/PageRequest"
}
},
"additionalProperties": false,
"required": [
"kind",
"page"
]
},
{
"description": "Targeted decompilation.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "decompile"
},
"selector": {
"description": "Explicit Function Selector.",
"$ref": "#/$defs/FunctionSelector"
}
},
"additionalProperties": false,
"required": [
"kind",
"selector"
]
},
{
"description": "Store cleanup does not cross into Ghidra; reserved for parity.",
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "clean"
}
},
"additionalProperties": false,
"required": [
"kind"
]
}
]
},
"WorkerPath": {
"description": "A validated UTF-8 absolute path owned by the trusted harness.",
"type": "string"
}
}
}

View file

@ -0,0 +1,120 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "AdapterResponse",
"description": "Strict adapter response envelope.",
"type": "object",
"properties": {
"invocation_id": {
"description": "Echoed invocation identifier.",
"$ref": "#/$defs/InvocationId"
},
"operation": {
"description": "Echoed operation.",
"$ref": "#/$defs/Operation"
},
"protocol_version": {
"type": "integer",
"const": 1
},
"result": {
"description": "Exactly one typed success or error result.",
"$ref": "#/$defs/AdapterResult"
}
},
"additionalProperties": false,
"required": [
"protocol_version",
"invocation_id",
"operation",
"result"
],
"$defs": {
"AdapterResult": {
"description": "Adapter result, with operation data validated by the harness afterward.",
"oneOf": [
{
"description": "Complete operation-specific result.",
"type": "object",
"properties": {
"data": {
"description": "Strictly schema-validated by the operation dispatcher."
},
"status": {
"type": "string",
"const": "success"
}
},
"additionalProperties": false,
"required": [
"status",
"data"
]
},
{
"description": "Typed adapter failure with no partial Query data.",
"type": "object",
"properties": {
"code": {
"description": "Stable adapter error code.",
"type": "string"
},
"details": {
"description": "Operation-specific error facts.",
"type": "object",
"additionalProperties": true
},
"message": {
"description": "Concise diagnostic context.",
"type": "string"
},
"status": {
"type": "string",
"const": "error"
}
},
"additionalProperties": false,
"required": [
"status",
"code",
"message",
"details"
]
}
]
},
"InvocationId": {
"description": "A random 128-bit invocation identifier encoded as 32 lowercase hex digits.",
"type": "string"
},
"Operation": {
"description": "Version 0.1 operation identifier shared with Java.",
"oneOf": [
{
"description": "Installation diagnosis.",
"type": "string",
"const": "doctor"
},
{
"description": "Sample metadata inspection.",
"type": "string",
"const": "inspect"
},
{
"description": "Stable Function collection.",
"type": "string",
"const": "functions"
},
{
"description": "Targeted Function decompilation.",
"type": "string",
"const": "decompile"
},
{
"description": "Explicit Analysis Store cleanup.",
"type": "string",
"const": "clean"
}
]
}
}
}

280
src/cli.rs Normal file
View file

@ -0,0 +1,280 @@
//! Typed command-line surface for every version 0.1 operation.
use std::path::PathBuf;
use clap::{ArgGroup, Args, Parser, Subcommand, ValueEnum};
use crate::domain::{Address, CollectionLimit, Digest, FunctionSelector, PageRequest, PositiveU64};
/// Parsed `ghidr` command line.
#[derive(Clone, Debug, Parser)]
#[command(name = "ghidr", version, about, disable_help_subcommand = true)]
pub struct Cli {
/// Structured JSON or explicit human-readable presentation.
#[arg(long, global = true, value_enum, default_value_t = OutputFormat::Json)]
pub format: OutputFormat,
/// Absolute Analysis Store path.
#[arg(long, global = true, value_name = "ABSOLUTE_PATH", value_parser = parse_store_path)]
pub store: Option<PathBuf>,
/// Worker sandbox policy; never auto-detected.
#[arg(long, global = true, value_enum, default_value_t = SandboxMode::Bubblewrap)]
pub sandbox: SandboxMode,
/// Maximum native analysis duration.
#[arg(long, global = true, default_value = "600")]
pub analysis_timeout_seconds: PositiveU64,
/// Maximum native decompilation duration.
#[arg(long, global = true, default_value = "60")]
pub decompile_timeout_seconds: PositiveU64,
/// Java maximum heap in MiB, not total process memory.
#[arg(long, global = true, default_value = "2048")]
pub max_heap_mib: PositiveU64,
/// Maximum Ghidra analysis CPU count.
#[arg(long, global = true, default_value = "2")]
pub max_cpu: PositiveU64,
/// Maximum accepted Sample bytes.
#[arg(long, global = true, default_value = "1073741824")]
pub max_sample_bytes: PositiveU64,
/// Maximum serialized successful stdout bytes.
#[arg(long, global = true, default_value = "65536")]
pub max_inline_bytes: PositiveU64,
/// The requested operation.
#[command(subcommand)]
pub command: Command,
}
/// Public output presentation.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
#[value(rename_all = "snake_case")]
pub enum OutputFormat {
/// One versioned JSON document on the result stream.
#[default]
Json,
/// Concise terminal-oriented presentation.
Human,
}
/// Explicit worker isolation selection.
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
#[value(rename_all = "snake_case")]
pub enum SandboxMode {
/// Linux namespace isolation through Bubblewrap.
#[default]
Bubblewrap,
/// Isolation supplied by the caller and not verified by `ghidr`.
External,
/// Explicitly disable worker isolation for debugging.
Off,
}
/// Version 0.1 operations.
#[derive(Clone, Debug, Subcommand)]
pub enum Command {
/// Diagnose the Ghidra, JDK, store, and sandbox installation.
Doctor,
/// Inspect Sample and Analysis metadata.
Inspect(SampleArgs),
/// List every discovered Function in stable order.
Functions(FunctionsArgs),
/// Decompile one explicitly selected Function.
Decompile(DecompileArgs),
/// Explicitly remove tool-owned stored data.
Clean(CleanArgs),
}
/// A command operating on one caller-supplied Sample path.
#[derive(Clone, Debug, Args)]
pub struct SampleArgs {
/// Sample to snapshot and analyze read-only.
#[arg(value_name = "SAMPLE")]
pub sample: PathBuf,
}
/// Function collection arguments.
#[derive(Clone, Debug, Args)]
pub struct FunctionsArgs {
/// Sample to snapshot and analyze read-only.
#[arg(value_name = "SAMPLE")]
pub sample: PathBuf,
/// Maximum returned Functions; defaults to 100.
#[arg(long, conflicts_with = "all")]
pub limit: Option<PositiveU64>,
/// Number of Functions skipped in stable order.
#[arg(long, default_value_t = 0, conflicts_with = "all")]
pub offset: u64,
/// Request every Function, retaining the serialized-byte bound.
#[arg(long)]
pub all: bool,
}
impl FunctionsArgs {
/// Converts clap flags to the protocol's explicit page form.
#[must_use]
pub fn page(&self) -> PageRequest {
PageRequest {
offset: self.offset,
limit: if self.all {
CollectionLimit::All
} else {
CollectionLimit::Bounded(self.limit.unwrap_or_else(default_collection_limit))
},
}
}
}
fn default_collection_limit() -> PositiveU64 {
"100".parse().unwrap_or(PositiveU64::ONE)
}
fn parse_store_path(value: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(value);
if !path.is_absolute() || path.to_str().is_none() {
return Err("store path must be absolute UTF-8".to_owned());
}
Ok(path)
}
/// Targeted decompilation arguments.
#[derive(Clone, Debug, Args)]
#[command(group(ArgGroup::new("selector").required(true).multiple(false).args(["name", "address"])))]
pub struct DecompileArgs {
/// Sample to snapshot and analyze read-only.
#[arg(value_name = "SAMPLE")]
pub sample: PathBuf,
/// Exact, case-sensitive Ghidra Function name.
#[arg(long, value_parser = parse_function_name)]
pub name: Option<String>,
/// Exact entry Address in canonical `SPACE:0x...` form.
#[arg(long)]
pub address: Option<Address>,
}
fn parse_function_name(value: &str) -> Result<String, String> {
if value.is_empty() {
return Err("Function name must not be empty".to_owned());
}
Ok(value.to_owned())
}
impl DecompileArgs {
/// Returns the selector guaranteed by clap's required group.
pub fn selector(&self) -> Result<FunctionSelector, &'static str> {
match (&self.name, &self.address) {
(Some(name), None) => Ok(FunctionSelector::Name(name.clone())),
(None, Some(address)) => Ok(FunctionSelector::Address(address.clone())),
_ => Err("exactly one Function Selector is required"),
}
}
}
/// Explicit cleanup arguments.
#[derive(Clone, Debug, Args)]
#[command(group(ArgGroup::new("target").required(true).multiple(false).args(["sample", "digest", "all"])))]
pub struct CleanArgs {
/// Sample whose digest selects stored data.
#[arg(value_name = "SAMPLE")]
pub sample: Option<PathBuf>,
/// Sample digest used when the original Sample is unavailable.
#[arg(long)]
pub digest: Option<Digest>,
/// Select all tool-owned stored data.
#[arg(long)]
pub all: bool,
/// Report exact matches without removing them.
#[arg(long)]
pub dry_run: bool,
/// Confirm store-wide cleanup without an interactive prompt.
#[arg(long, requires = "all")]
pub yes: bool,
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use clap::Parser as _;
use super::{Cli, Command, OutputFormat};
use crate::domain::{CollectionLimit, FunctionSelector};
#[test]
fn json_and_defaults_are_unconditional() {
let cli = Cli::try_parse_from(["ghidr", "functions", "sample"]).expect("valid CLI");
assert_eq!(cli.format, OutputFormat::Json);
let Command::Functions(args) = cli.command else {
panic!("expected functions");
};
assert_eq!(args.page().offset, 0);
assert!(matches!(args.page().limit, CollectionLimit::Bounded(value) if value.get() == 100));
}
#[test]
fn global_flags_work_after_subcommand() {
let cli = Cli::try_parse_from([
"ghidr",
"inspect",
"sample",
"--format",
"human",
"--max-cpu",
"4",
])
.expect("valid CLI");
assert_eq!(cli.format, OutputFormat::Human);
assert_eq!(cli.max_cpu.get(), 4);
}
#[test]
fn rejects_zero_and_conflicting_collection_bounds() {
assert!(Cli::try_parse_from(["ghidr", "functions", "sample", "--limit", "0"]).is_err());
assert!(
Cli::try_parse_from(["ghidr", "functions", "sample", "--all", "--offset", "1"])
.is_err()
);
}
#[test]
fn selector_is_explicit_and_canonical() {
let cli = Cli::try_parse_from([
"ghidr",
"decompile",
"sample",
"--address",
"ram:0x0000000000401120",
])
.expect("valid CLI");
let Command::Decompile(args) = cli.command else {
panic!("expected decompile");
};
assert!(matches!(args.selector(), Ok(FunctionSelector::Address(_))));
assert!(
Cli::try_parse_from(["ghidr", "decompile", "sample", "--address", "ram:0xABC"])
.is_err()
);
}
#[test]
fn clean_requires_exactly_one_target() {
assert!(Cli::try_parse_from(["ghidr", "clean"]).is_err());
assert!(
Cli::try_parse_from(["ghidr", "clean", "sample", "--digest", &"a".repeat(64),])
.is_err()
);
}
}

63
src/domain/address.rs Normal file
View file

@ -0,0 +1,63 @@
use std::{fmt, str::FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// A Ghidra address with an explicit, case-sensitive address space.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Address {
/// Exact Ghidra address-space name.
pub space: String,
/// Canonical lowercase, `0x`-prefixed, even-width hexadecimal offset.
pub offset: String,
}
impl FromStr for Address {
type Err = AddressParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (space, offset) = value.split_once(':').ok_or(AddressParseError)?;
let digits = offset.strip_prefix("0x").ok_or(AddressParseError)?;
if space.is_empty()
|| digits.is_empty()
|| digits.len() % 2 != 0
|| !digits
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(AddressParseError);
}
Ok(Self {
space: space.to_owned(),
offset: offset.to_owned(),
})
}
}
impl fmt::Display for Address {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}:{}", self.space, self.offset)
}
}
/// The address selector was not in canonical `SPACE:0x...` form.
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
#[error("address must be SPACE:0x followed by an even number of lowercase hexadecimal digits")]
pub struct AddressParseError;
#[cfg(test)]
mod tests {
use std::str::FromStr as _;
use super::Address;
#[test]
fn only_canonical_explicit_addresses_parse() {
assert!(Address::from_str("ram:0x0000000000401120").is_ok());
assert!(Address::from_str("0x00401120").is_err());
assert!(Address::from_str("ram:0xABC0").is_err());
assert!(Address::from_str("ram:0xabc").is_err());
}
}

80
src/domain/identifier.rs Normal file
View file

@ -0,0 +1,80 @@
use std::{fmt, str::FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// A lowercase SHA-256 digest.
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct Digest(String);
impl Digest {
/// Returns the canonical hexadecimal spelling.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for Digest {
type Err = IdentifierError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(IdentifierError::Digest);
}
Ok(Self(value.to_ascii_lowercase()))
}
}
impl fmt::Display for Digest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
/// A random 128-bit invocation identifier encoded as 32 lowercase hex digits.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct InvocationId(String);
impl InvocationId {
/// Parses a canonical invocation identifier.
pub fn parse(value: &str) -> Result<Self, IdentifierError> {
if value.len() != 32
|| !value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(IdentifierError::Invocation);
}
Ok(Self(value.to_owned()))
}
}
/// Identifier validation failure.
#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
pub enum IdentifierError {
/// SHA-256 spelling is invalid.
#[error("digest must contain exactly 64 hexadecimal characters")]
Digest,
/// Invocation identifier spelling is invalid.
#[error("invocation ID must contain exactly 32 lowercase hexadecimal characters")]
Invocation,
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use std::str::FromStr as _;
use super::Digest;
#[test]
fn digest_accepts_hex_and_canonicalizes_case() {
let digest = Digest::from_str(&"A".repeat(64)).expect("valid digest");
assert_eq!(digest.as_str(), "a".repeat(64));
assert!(Digest::from_str(&"a".repeat(63)).is_err());
}
}

81
src/domain/limits.rs Normal file
View file

@ -0,0 +1,81 @@
use std::{num::NonZeroU64, str::FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// A validated positive integer accepted by resource-limit flags.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct PositiveU64(NonZeroU64);
impl PositiveU64 {
/// The smallest valid positive value.
pub const ONE: Self = Self(NonZeroU64::MIN);
/// Returns the positive integer.
#[must_use]
pub const fn get(self) -> u64 {
self.0.get()
}
}
impl FromStr for PositiveU64 {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
value
.parse::<NonZeroU64>()
.map(Self)
.map_err(|_| "value must be a positive integer without a unit suffix")
}
}
impl TryFrom<u64> for PositiveU64 {
type Error = &'static str;
fn try_from(value: u64) -> Result<Self, Self::Error> {
NonZeroU64::new(value)
.map(Self)
.ok_or("value must be a positive integer")
}
}
/// An optional item bound, explicit when unbounded.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CollectionLimit {
/// At most the enclosed number of items.
Bounded(PositiveU64),
/// Every item, still subject to the serialized-byte budget.
All,
}
/// A normalized collection page request.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PageRequest {
/// Number of items skipped in stable order.
pub offset: u64,
/// Selected item bound.
pub limit: CollectionLimit,
}
/// Resource policy selected for one invocation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Limits {
/// Maximum Java heap, in MiB.
pub max_heap_mib: PositiveU64,
/// Maximum Ghidra analysis CPU count.
pub max_cpu: PositiveU64,
/// Native analysis timeout.
pub analysis_timeout_seconds: PositiveU64,
/// Optional native decompilation timeout.
pub decompile_timeout_seconds: Option<PositiveU64>,
/// Derived process watchdog.
pub child_watchdog_seconds: PositiveU64,
/// Maximum accepted Sample size.
pub max_sample_bytes: PositiveU64,
/// Maximum serialized successful stdout size.
pub max_inline_bytes: PositiveU64,
}

13
src/domain/mod.rs Normal file
View file

@ -0,0 +1,13 @@
//! Validated domain values shared by the CLI, store, and adapter protocol.
mod address;
mod identifier;
mod limits;
mod path;
mod selector;
pub use address::{Address, AddressParseError};
pub use identifier::{Digest, IdentifierError, InvocationId};
pub use limits::{CollectionLimit, Limits, PageRequest, PositiveU64};
pub use path::TaggedPath;
pub use selector::FunctionSelector;

75
src/domain/path.rs Normal file
View file

@ -0,0 +1,75 @@
use std::path::{Path, PathBuf};
use base64::{Engine as _, engine::general_purpose::STANDARD};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// A path preserving either UTF-8 text or exact Unix bytes.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "encoding", content = "value", rename_all = "snake_case")]
pub enum TaggedPath {
/// An unmodified UTF-8 path.
Utf8(String),
/// Standard padded Base64 over exact Unix path bytes.
UnixBytesBase64(String),
}
impl TaggedPath {
/// Captures a path without lossy conversion.
#[must_use]
pub fn from_path(path: &Path) -> Self {
path.to_str().map_or_else(
|| Self::UnixBytesBase64(STANDARD.encode(unix_path_bytes(path))),
|value| Self::Utf8(value.to_owned()),
)
}
/// Recovers the represented path when the encoding is valid.
pub fn to_path_buf(&self) -> Result<PathBuf, base64::DecodeError> {
match self {
Self::Utf8(value) => Ok(PathBuf::from(value)),
Self::UnixBytesBase64(value) => {
let bytes = STANDARD.decode(value)?;
Ok(path_from_unix_bytes(bytes))
}
}
}
}
#[cfg(unix)]
fn unix_path_bytes(path: &Path) -> &[u8] {
use std::os::unix::ffi::OsStrExt as _;
path.as_os_str().as_bytes()
}
#[cfg(not(unix))]
fn unix_path_bytes(path: &Path) -> &[u8] {
path.as_os_str().as_encoded_bytes()
}
#[cfg(unix)]
fn path_from_unix_bytes(bytes: Vec<u8>) -> PathBuf {
use std::os::unix::ffi::OsStringExt as _;
PathBuf::from(std::ffi::OsString::from_vec(bytes))
}
#[cfg(all(test, unix))]
#[allow(clippy::expect_used)]
mod tests {
use std::{ffi::OsString, os::unix::ffi::OsStringExt as _, path::PathBuf};
use super::TaggedPath;
#[test]
fn non_utf8_path_round_trips_exact_unix_bytes() {
let original = PathBuf::from(OsString::from_vec(vec![b'/', b't', b'm', b'p', b'/', 0xff]));
let tagged = TaggedPath::from_path(&original);
assert!(matches!(&tagged, TaggedPath::UnixBytesBase64(value) if !value.is_empty()));
assert_eq!(tagged.to_path_buf().expect("valid Base64"), original);
}
}
#[cfg(not(unix))]
fn path_from_unix_bytes(bytes: Vec<u8>) -> PathBuf {
PathBuf::from(String::from_utf8_lossy(&bytes).into_owned())
}

14
src/domain/selector.rs Normal file
View file

@ -0,0 +1,14 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::Address;
/// Exactly one explicit Function Selector.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum FunctionSelector {
/// Exact, case-sensitive Ghidra function name.
Name(String),
/// Exact function entry address.
Address(Address),
}

249
src/error.rs Normal file
View file

@ -0,0 +1,249 @@
//! Typed failures and coarse process status allocation.
use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
/// Stable machine-readable error taxonomy available in the foundation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ErrorCode {
/// Command-line syntax or validation failed.
InvalidArguments,
/// The caller's Sample path does not exist.
SampleNotFound,
/// The caller's Sample cannot be read.
SampleUnreadable,
/// The opened Sample is not a regular file.
InvalidSampleType,
/// The Sample exceeded the selected byte ceiling.
SampleTooLarge,
/// Source metadata changed while snapshotting.
SampleChanged,
/// The Analysis Store lacks the required free-space reserve.
InsufficientStoreSpace,
/// Ghidra could not recognize a Target Specification.
UnsupportedTarget,
/// Ghidra found multiple Target Specification candidates.
AmbiguousTarget,
/// Ghidra is not installed.
GhidraMissing,
/// The installed Ghidra version is incompatible.
GhidraIncompatible,
/// Java is not installed.
JavaMissing,
/// The installed Java version is incompatible.
JavaIncompatible,
/// A later integration layer has not connected execution yet.
InternalNotImplemented,
/// Rust/Java request or response validation failed.
ProtocolViolation,
/// The child exceeded a native or watchdog timeout.
Timeout,
/// Native auto-analysis timed out.
AnalysisTimeout,
/// Ghidra auto-analysis failed.
AnalysisFailed,
/// Native Function decompilation timed out.
DecompileTimeout,
/// Ghidra Function decompilation failed.
DecompilationFailed,
/// The caller interrupted the operation.
Interrupted,
/// The adapter could not serialize a bounded response.
ResultTooLarge,
/// The success descriptor cannot fit the requested inline budget.
InlineBudgetTooSmall,
/// Store-wide cleanup lacks explicit confirmation.
ConfirmationRequired,
/// Another process holds the selected Analysis lock.
AnalysisBusy,
/// Stored Analysis validation failed and recovery did not succeed.
CorruptAnalysis,
/// A cleanup transaction requires a recoverable retry.
CleanupIncomplete,
/// The resolved Analysis Store path is invalid.
InvalidStorePath,
/// A Function Selector resolved to no Function.
FunctionNotFound,
/// A Function Selector resolved to multiple Functions.
FunctionSelectorAmbiguous,
/// An Address is not a Function entry.
FunctionEntryRequired,
/// Ghidra does not contain the requested address space.
AddressSpaceNotFound,
/// Installation diagnosis found an unusable component.
DoctorFailed,
/// A general runtime or integration failure.
Internal,
}
impl ErrorCode {
/// Authoritative coarse status mapping from ADR 0002.
#[must_use]
pub const fn exit_status(self) -> ExitStatus {
match self {
Self::InvalidArguments => ExitStatus::InvalidInvocation,
Self::Timeout | Self::AnalysisTimeout | Self::DecompileTimeout => ExitStatus::Timeout,
Self::Interrupted => ExitStatus::Interrupted,
_ => ExitStatus::RuntimeFailure,
}
}
}
/// Stable public error body.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ErrorBody {
/// Stable machine identifier.
pub code: ErrorCode,
/// Concise context that is not a control-flow interface.
pub message: String,
/// Whether the same logical request might later succeed unchanged.
pub retryable: bool,
/// Code-specific structured context, always an object.
pub details: BTreeMap<String, Value>,
}
/// Versioned failure document.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ErrorEnvelope {
/// Public schema major version.
pub schema_version: u32,
/// Always `error`.
pub kind: ErrorKind,
/// Typed failure body.
pub error: ErrorBody,
}
/// Error envelope discriminator.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ErrorKind {
/// A failed invocation.
Error,
}
/// Internal error paired with its public document and status class.
#[derive(Clone, Debug, Error)]
#[error("{body_message}")]
pub struct AppError {
body_message: String,
envelope: ErrorEnvelope,
status: ExitStatus,
}
impl AppError {
/// Constructs a typed failure with empty structured details.
#[must_use]
pub fn new(code: ErrorCode, message: impl Into<String>, retryable: bool) -> Self {
let message = message.into();
let status = code.exit_status();
Self {
body_message: message.clone(),
envelope: ErrorEnvelope {
schema_version: 1,
kind: ErrorKind::Error,
error: ErrorBody {
code,
message,
retryable,
details: BTreeMap::new(),
},
},
status,
}
}
/// Clear failure used until the store/process/Java execution layer is attached.
#[must_use]
pub fn execution_not_implemented(operation: &str) -> Self {
let mut error = Self::new(
ErrorCode::InternalNotImplemented,
format!("operation '{operation}' is not connected to the Ghidra execution layer"),
false,
);
error
.envelope
.error
.details
.insert("operation".to_owned(), Value::String(operation.to_owned()));
error
}
/// Constructs an invalid-invocation failure from clap context.
#[must_use]
pub fn invalid_arguments(message: impl Into<String>) -> Self {
Self::new(ErrorCode::InvalidArguments, message, false)
}
/// Adds one typed detail field.
#[must_use]
pub fn with_detail(mut self, key: impl Into<String>, value: Value) -> Self {
self.envelope.error.details.insert(key.into(), value);
self
}
/// Returns the public error document.
#[must_use]
pub const fn envelope(&self) -> &ErrorEnvelope {
&self.envelope
}
/// Returns the coarse process status.
#[must_use]
pub const fn status(&self) -> ExitStatus {
self.status
}
}
/// Accepted coarse process statuses from ADR 0002.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(i32)]
pub enum ExitStatus {
/// Successful operation.
Success = 0,
/// Runtime failure.
RuntimeFailure = 1,
/// Invalid invocation or arguments.
InvalidInvocation = 2,
/// Native phase or child watchdog timeout.
Timeout = 124,
/// User interruption.
Interrupted = 130,
}
impl ExitStatus {
/// Integer process exit code.
#[must_use]
pub const fn code(self) -> i32 {
self as i32
}
}
#[cfg(test)]
mod tests {
use super::{ErrorCode, ExitStatus};
#[test]
fn detailed_errors_map_to_only_accepted_coarse_statuses() {
assert_eq!(
ErrorCode::InvalidArguments.exit_status(),
ExitStatus::InvalidInvocation
);
assert_eq!(
ErrorCode::AnalysisTimeout.exit_status(),
ExitStatus::Timeout
);
assert_eq!(
ErrorCode::Interrupted.exit_status(),
ExitStatus::Interrupted
);
assert_eq!(
ErrorCode::ProtocolViolation.exit_status(),
ExitStatus::RuntimeFailure
);
}
}

12
src/lib.rs Normal file
View file

@ -0,0 +1,12 @@
#![forbid(unsafe_code)]
#![doc = "Public contracts and synchronous orchestration seams for `ghidr`."]
pub mod cli;
pub mod domain;
pub mod error;
pub mod operation;
pub mod output;
pub mod protocol;
pub mod schema;
pub use error::{AppError, ErrorCode, ExitStatus};

20
src/main.rs Normal file
View file

@ -0,0 +1,20 @@
#![forbid(unsafe_code)]
#![doc = "`ghidr` synchronous command-line entry point."]
use std::{io, process::ExitCode};
use ghidra_cli::output::{UnimplementedExecutor, run_from};
fn main() -> ExitCode {
let mut stdout = io::stdout().lock();
let mut stderr = io::stderr().lock();
match run_from(
std::env::args_os(),
&UnimplementedExecutor,
&mut stdout,
&mut stderr,
) {
Ok(status) => ExitCode::from(u8::try_from(status.code()).unwrap_or(1)),
Err(_) => ExitCode::FAILURE,
}
}

647
src/operation.rs Normal file
View file

@ -0,0 +1,647 @@
//! Public result models and the authoritative compile-time operation registry.
use std::collections::BTreeMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::domain::{Address, Digest, FunctionSelector, Limits, PositiveU64, TaggedPath};
/// Public schema version emitted by version 0.1.
pub const SCHEMA_VERSION: u32 = 1;
/// Rust/Java adapter protocol version shipped with version 0.1.
pub const ADAPTER_PROTOCOL_VERSION: u32 = 1;
/// Version 0.1 operation identifier shared with Java.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Operation {
/// Installation diagnosis.
Doctor,
/// Sample metadata inspection.
Inspect,
/// Stable Function collection.
Functions,
/// Targeted Function decompilation.
Decompile,
/// Explicit Analysis Store cleanup.
Clean,
}
impl Operation {
/// Stable snake-case spelling.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Doctor => "doctor",
Self::Inspect => "inspect",
Self::Functions => "functions",
Self::Decompile => "decompile",
Self::Clean => "clean",
}
}
}
/// Static operation metadata used by conformance tests and future adapters.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OperationDescriptor {
/// Protocol operation enum member.
pub operation: Operation,
/// Public response `kind` value.
pub public_kind: &'static str,
/// Published success schema filename.
pub schema: &'static str,
/// Reviewed representative fixture filename.
pub golden: &'static str,
/// Stable capability category.
pub category: &'static str,
/// One-sentence agent-facing description.
pub description: &'static str,
}
/// The single authoritative version 0.1 operation set.
pub const OPERATIONS: &[OperationDescriptor] = &[
OperationDescriptor {
operation: Operation::Doctor,
public_kind: "doctor",
schema: "doctor.schema.json",
golden: "doctor.json",
category: "diagnostics",
description: "Diagnose the local Ghidra, JDK, store, and sandbox installation.",
},
OperationDescriptor {
operation: Operation::Inspect,
public_kind: "inspection",
schema: "inspection.schema.json",
golden: "inspection.json",
category: "analysis",
description: "Inspect reproducible Sample, program, and Analysis metadata.",
},
OperationDescriptor {
operation: Operation::Functions,
public_kind: "functions",
schema: "functions.schema.json",
golden: "functions.json",
category: "query",
description: "List Ghidra-discovered Functions in stable bounded order.",
},
OperationDescriptor {
operation: Operation::Decompile,
public_kind: "decompilation",
schema: "decompilation.schema.json",
golden: "decompilation.json",
category: "query",
description: "Decompile exactly one unambiguously selected Function.",
},
OperationDescriptor {
operation: Operation::Clean,
public_kind: "cleanup",
schema: "cleanup.schema.json",
golden: "cleanup.json",
category: "store",
description: "Explicitly inspect or remove tool-owned stored data.",
},
];
/// Versioned successful response.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SuccessEnvelope<P, D> {
/// Public schema major version.
pub schema_version: u32,
/// Operation-specific public discriminator.
pub kind: String,
/// Reproducibility and execution context.
pub provenance: P,
/// Operation-specific result.
pub data: D,
/// Deduplicated stable warnings.
pub warnings: Vec<Warning>,
}
impl<P, D> SuccessEnvelope<P, D> {
/// Builds a version 1 success while enforcing stable warning ordering.
#[must_use]
pub fn new(kind: impl Into<String>, provenance: P, data: D, warnings: Vec<Warning>) -> Self {
Self {
schema_version: SCHEMA_VERSION,
kind: kind.into(),
provenance,
data,
warnings: normalize_warnings(warnings),
}
}
}
/// Stable non-fatal warning.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct Warning {
/// Stable machine identifier.
pub code: String,
/// Concise human context.
pub message: String,
/// Warning-specific context, always an object.
pub details: BTreeMap<String, Value>,
}
/// Deterministically sorts and deduplicates warnings by code and canonical details.
#[must_use]
pub fn normalize_warnings(mut warnings: Vec<Warning>) -> Vec<Warning> {
warnings.sort_by_key(warning_key);
warnings.dedup_by(|left, right| {
left.code == right.code
&& canonical_details(&left.details) == canonical_details(&right.details)
});
warnings
}
fn warning_key(warning: &Warning) -> (String, String, String) {
(
warning.code.clone(),
canonical_details(&warning.details),
warning.message.clone(),
)
}
fn canonical_details(details: &BTreeMap<String, Value>) -> String {
serde_json::to_string(details).unwrap_or_default()
}
/// Minimal provenance available to non-Sample operations.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ToolProvenance {
/// CLI package version.
pub ghidr_version: String,
/// Rust/Java protocol version.
pub adapter_protocol_version: u32,
/// Selected Analysis Store.
pub analysis_store: AnalysisStoreProvenance,
/// Selected sandbox policy.
pub sandbox: SandboxProvenance,
}
/// Fixed provenance for every Sample-backed Query.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct QueryProvenance {
/// CLI package version.
pub ghidr_version: String,
/// Rust/Java protocol version.
pub adapter_protocol_version: u32,
/// Ghidra version.
pub ghidra_version: String,
/// Java major version.
pub java_version: String,
/// Sample content identity.
pub sample_sha256: Digest,
/// Exact caller-supplied path.
pub source_path: TaggedPath,
/// Canonical Analysis Profile identity.
pub analysis_profile_sha256: Digest,
/// Resolved Target Specification and verification.
pub target: TargetProvenance,
/// Selected Analysis Store.
pub analysis_store: AnalysisStoreProvenance,
/// Selected sandbox policy.
pub sandbox: SandboxProvenance,
/// Selected and derived resource limits.
pub limits: Limits,
}
/// Target Specification provenance.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct TargetProvenance {
/// Exact loader identifier.
pub loader: String,
/// Executable format name.
pub format: String,
/// Exact processor-language identifier.
pub processor_language: String,
/// Exact compiler-specification identifier.
pub compiler_specification: String,
/// Project integration-test coverage.
pub verification: TargetVerification,
}
/// Project integration-test coverage for a recognized target.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TargetVerification {
/// Covered by version 0.1 real-Ghidra fixtures.
Verified,
/// Recognized by Ghidra but outside the tested matrix.
Unverified,
}
/// Analysis Store provenance.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AnalysisStoreProvenance {
/// Tagged absolute store path.
pub path: TaggedPath,
/// Store resolution source.
pub source: StoreSource,
}
/// Analysis Store resolution source.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum StoreSource {
/// Explicit `--store` flag.
Cli,
/// `GHIDR_STORE` environment variable.
Environment,
/// Explicit `XDG_CACHE_HOME` fallback.
Xdg,
/// Home-directory fallback.
HomeFallback,
}
/// Worker sandbox provenance.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SandboxProvenance {
/// Selected backend.
pub backend: SandboxBackend,
/// Isolation verification state.
pub verification: SandboxVerification,
}
/// Sandbox backend.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SandboxBackend {
/// Bubblewrap namespaces.
Bubblewrap,
/// Caller-supplied isolation.
External,
/// Isolation disabled.
Off,
}
/// Sandbox verification state.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SandboxVerification {
/// Verified by `ghidr`.
Verified,
/// Declared but not verifiable by `ghidr`.
Unverified,
/// Explicitly disabled.
Disabled,
}
/// Installation diagnosis data.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct DoctorData {
/// True only when every required component is ready.
pub ready: bool,
/// Typed component reports keyed by stable component name.
pub components: BTreeMap<String, ComponentReport>,
}
/// One diagnosed component.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ComponentReport {
/// Stable readiness classification.
pub status: ComponentStatus,
/// Component-specific facts.
pub details: BTreeMap<String, Value>,
}
/// Doctor component readiness classification.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ComponentStatus {
/// Component is usable.
Ready,
/// Component is absent.
Missing,
/// Component version is unsupported.
Incompatible,
/// Component configuration is invalid.
Invalid,
/// Component storage cannot be written.
Unwritable,
}
/// Sample inspection data.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct InspectionData {
/// Sample identity and size.
pub sample: SampleSummary,
/// Program address and Function summary.
pub program: ProgramSummary,
/// Reuse disposition.
pub analysis: AnalysisSummary,
}
/// Sample identity summary.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SampleSummary {
/// SHA-256 content identity.
pub sha256: Digest,
/// Exact byte length.
pub size_bytes: u64,
}
/// Program summary.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ProgramSummary {
/// Image base when applicable.
pub image_base: Option<Address>,
/// Minimum Address when applicable.
pub minimum_address: Option<Address>,
/// Maximum Address when applicable.
pub maximum_address: Option<Address>,
/// Count of memory, external, and thunk Functions.
pub function_count: u64,
}
/// Analysis selection summary.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AnalysisSummary {
/// Whether compatible analysis was built or reused.
pub disposition: AnalysisDisposition,
}
/// Analysis selection disposition.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AnalysisDisposition {
/// A new Analysis was created.
Created,
/// A compatible Analysis was reused.
Reused,
/// Corrupt stored data was quarantined and rebuilt once.
Rebuilt,
}
/// Stable page metadata.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct PageMetadata {
/// Stable order identifier.
pub order: String,
/// Number of items skipped.
pub offset: u64,
/// Applied item bound, null only for explicit `--all`.
pub limit: Option<u64>,
/// Number of returned items.
pub returned: u64,
/// Total items in the immutable Analysis.
pub total: u64,
/// Whether a subsequent page exists.
pub has_more: bool,
}
/// Function collection data.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct FunctionsData {
/// Stable page facts.
pub page: PageMetadata,
/// Function records in documented order.
pub items: Vec<FunctionItem>,
}
/// Minimal version 0.1 Function record.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct FunctionItem {
/// Exact basename.
pub name: String,
/// Exact fully-qualified name.
pub qualified_name: String,
/// Function entry Address.
pub entry: Address,
/// Number of Addresses in the body.
pub body_address_count: u64,
/// Memory or external location.
pub location: FunctionLocation,
/// Whether Ghidra marks the Function external.
pub is_external: bool,
/// Whether Ghidra marks the Function a thunk.
pub is_thunk: bool,
/// Resolved thunk target entry when available.
pub thunk_target_entry: Option<Address>,
/// Whether the Function is a valid decompilation target.
pub decompilable: bool,
}
/// Function location class used by stable ordering.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FunctionLocation {
/// Function has a memory Address.
Memory,
/// External/imported Function.
External,
}
/// Successful targeted decompilation data.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct DecompilationData {
/// Exact selector supplied by the caller.
pub requested_selector: FunctionSelector,
/// Unambiguously resolved Function.
pub function: SelectedFunction,
/// Complete normalized Ghidra C-like text.
pub decompilation: DecompiledText,
}
/// Identity of a selected Function.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct SelectedFunction {
/// Exact basename.
pub name: String,
/// Exact fully-qualified name.
pub qualified_name: String,
/// Entry Address.
pub entry: Address,
}
/// Complete decompiler text.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct DecompiledText {
/// Always `ghidra_c` in version 0.1.
pub syntax: DecompilationSyntax,
/// Complete text with only line endings normalized to LF.
pub text: String,
}
/// Decompiled text representation.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DecompilationSyntax {
/// Ghidra's C-like representation.
GhidraC,
}
/// Cleanup result data; detailed count contracts are carried as typed snapshots.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct CleanupData {
/// Execution or dry-run mode.
pub mode: CleanupMode,
/// Resolved cleanup target.
pub target: CleanupTarget,
/// Preflight match snapshot.
pub matched: CleanupSnapshot,
/// Exact removed snapshot, null for dry runs.
pub removed: Option<CleanupSnapshot>,
}
/// Cleanup mode.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum CleanupMode {
/// Data was removed.
Executed,
/// No data was removed.
DryRun,
}
/// Resolved cleanup target.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CleanupTarget {
/// Digest resolved from a caller Sample.
Sample {
/// Exact caller path.
source_path: TaggedPath,
/// Resolved content identity.
sha256: Digest,
},
/// Caller-supplied digest.
Digest {
/// Selected content identity.
sha256: Digest,
},
/// Entire Analysis Store.
All,
}
/// Counts and disk usage associated with cleanup targets.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct CleanupSnapshot {
/// Complete Analyses.
pub analyses: u64,
/// Quarantined Analyses.
pub quarantined_analyses: u64,
/// Query Artifacts.
pub artifacts: u64,
/// Diagnostic Log bundles.
pub diagnostic_logs: u64,
/// Logical and allocated usage.
pub usage: StorageUsage,
/// Unique bytewise-sorted Analysis Profile digests.
pub analysis_profile_sha256: Vec<Digest>,
}
/// Filesystem usage represented without implying freed space.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct StorageUsage {
/// Sum of file lengths.
pub logical_bytes: u64,
/// Sum of Linux allocated blocks including directories.
pub allocated_bytes: u64,
}
/// Artifact descriptor returned instead of oversized inline data.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ArtifactDescriptor {
/// Tagged absolute Artifact path.
pub path: TaggedPath,
/// Exact file bytes including trailing LF.
pub bytes: u64,
/// SHA-256 of exact file bytes.
pub sha256: Digest,
/// Always `application/json`.
pub media_type: String,
/// Always `complete_success_response`.
pub contains: String,
}
/// Canonical Analysis Profile document whose exact JSON bytes are hashed.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AnalysisProfile {
/// Canonical profile format version.
pub profile_version: u32,
/// Exact Ghidra version.
pub ghidra_version: String,
/// Exact Java major version.
pub java_version: String,
/// Analysis adapter implementation version.
pub analysis_adapter_version: u32,
/// Analysis-affecting Target Specification.
pub target: ProfileTarget,
/// Fully resolved, bytewise-name-sorted loader options.
pub loader_options: Vec<ProfileOption>,
/// Fully resolved, bytewise-name-sorted analyzer options.
pub analyzer_options: Vec<ProfileOption>,
/// Maximum analysis CPU count.
pub max_cpu: PositiveU64,
}
/// Target fields which affect Analysis identity.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ProfileTarget {
/// Exact loader identifier.
pub loader: String,
/// Exact processor-language identifier.
pub processor_language: String,
/// Exact compiler-specification identifier.
pub compiler_specification: String,
}
/// Fully resolved typed Ghidra option.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ProfileOption {
/// Canonical option name.
pub name: String,
/// Typed canonical value without floating-point data.
pub value: ProfileOptionValue,
}
/// Values permitted in canonical profile options.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum ProfileOptionValue {
/// Boolean option.
Boolean(bool),
/// Signed integer option.
Integer(i64),
/// Exact UTF-8 string option.
String(String),
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use serde_json::json;
use super::{Warning, normalize_warnings};
#[test]
fn warnings_sort_and_deduplicate_by_code_and_details() {
let details = BTreeMap::from([("target".to_owned(), json!("elf"))]);
let warnings = normalize_warnings(vec![
Warning {
code: "zeta".to_owned(),
message: "z".to_owned(),
details: BTreeMap::new(),
},
Warning {
code: "alpha".to_owned(),
message: "second".to_owned(),
details: details.clone(),
},
Warning {
code: "alpha".to_owned(),
message: "first".to_owned(),
details,
},
]);
assert_eq!(warnings.len(), 2);
assert_eq!(warnings[0].code, "alpha");
assert_eq!(warnings[0].message, "first");
}
}

214
src/output.rs Normal file
View file

@ -0,0 +1,214 @@
//! Strict stdout/stderr framing and replaceable synchronous execution seam.
use std::{
ffi::OsString,
io::{self, Write},
};
use clap::Parser as _;
use serde_json::Value;
use crate::{
AppError, ErrorCode, ExitStatus,
cli::{Cli, Command, OutputFormat},
protocol::requests_human_format,
};
/// Fixed serialized safety cap for an error document.
pub const MAX_ERROR_BYTES: usize = 65_536;
/// A fully computed success ready for strict framing.
#[derive(Clone, Debug)]
pub struct RenderedSuccess {
/// Complete public JSON document.
pub json: Value,
/// Semantically equivalent concise human presentation.
pub human: String,
}
/// Synchronous boundary implemented by later store/process/Java layers.
pub trait Executor {
/// Executes one already parsed and validated invocation.
fn execute(&self, cli: &Cli) -> Result<RenderedSuccess, AppError>;
}
/// Foundation executor which truthfully rejects every unconnected path.
#[derive(Clone, Copy, Debug, Default)]
pub struct UnimplementedExecutor;
impl Executor for UnimplementedExecutor {
fn execute(&self, cli: &Cli) -> Result<RenderedSuccess, AppError> {
Err(AppError::execution_not_implemented(operation_name(
&cli.command,
)))
}
}
/// Parses, executes, and writes exactly the stream framing selected by the caller.
pub fn run_from<I, T>(
arguments: I,
executor: &impl Executor,
stdout: &mut impl Write,
stderr: &mut impl Write,
) -> io::Result<ExitStatus>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + Clone,
{
let arguments: Vec<OsString> = arguments.into_iter().map(Into::into).collect();
match Cli::try_parse_from(arguments.clone()) {
Ok(cli) => run_parsed(&cli, executor, stdout, stderr),
Err(error) => {
if requests_human_format(&arguments) {
stderr.write_all(error.to_string().as_bytes())?;
} else {
write_error_json(&AppError::invalid_arguments(error.to_string()), stderr)?;
}
Ok(ExitStatus::InvalidInvocation)
}
}
}
/// Executes an already parsed command; useful for embedding and integration tests.
pub fn run_parsed(
cli: &Cli,
executor: &impl Executor,
stdout: &mut impl Write,
stderr: &mut impl Write,
) -> io::Result<ExitStatus> {
match executor.execute(cli) {
Ok(success) => {
let bytes = success_bytes(cli.format, &success)?;
let limit = usize::try_from(cli.max_inline_bytes.get()).unwrap_or(usize::MAX);
if bytes.len() > limit {
let error = AppError::new(
ErrorCode::ProtocolViolation,
"executor returned a success larger than the selected inline budget",
false,
)
.with_detail("configured_bytes", Value::from(cli.max_inline_bytes.get()))
.with_detail("observed_bytes", Value::from(bytes.len()));
write_error_json(&error, stderr)?;
return Ok(error.status());
}
stdout.write_all(&bytes)?;
Ok(ExitStatus::Success)
}
Err(error) => {
match cli.format {
OutputFormat::Json => write_error_json(&error, stderr)?,
OutputFormat::Human => {
stderr.write_all(error.to_string().as_bytes())?;
stderr.write_all(b"\n")?;
}
}
Ok(error.status())
}
}
}
fn success_bytes(format: OutputFormat, success: &RenderedSuccess) -> io::Result<Vec<u8>> {
let mut bytes = match format {
OutputFormat::Json => serde_json::to_vec(&success.json).map_err(io::Error::other)?,
OutputFormat::Human => success.human.as_bytes().to_vec(),
};
if !bytes.ends_with(b"\n") {
bytes.push(b'\n');
}
Ok(bytes)
}
fn write_error_json(error: &AppError, writer: &mut impl Write) -> io::Result<()> {
let mut bytes = serde_json::to_vec(error.envelope()).map_err(io::Error::other)?;
bytes.push(b'\n');
if bytes.len() > MAX_ERROR_BYTES {
let fallback = AppError::new(
ErrorCode::Internal,
"error document exceeded the fixed safety bound",
false,
);
bytes = serde_json::to_vec(fallback.envelope()).map_err(io::Error::other)?;
bytes.push(b'\n');
}
writer.write_all(&bytes)
}
fn operation_name(command: &Command) -> &'static str {
match command {
Command::Doctor => "doctor",
Command::Inspect(_) => "inspect",
Command::Functions(_) => "functions",
Command::Decompile(_) => "decompile",
Command::Clean(_) => "clean",
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use serde_json::json;
use super::{Executor, RenderedSuccess, run_from};
use crate::{AppError, ExitStatus, cli::Cli};
struct SuccessExecutor;
impl Executor for SuccessExecutor {
fn execute(&self, _cli: &Cli) -> Result<RenderedSuccess, AppError> {
Ok(RenderedSuccess {
json: json!({"schema_version": 1, "kind": "doctor"}),
human: "ready".to_owned(),
})
}
}
#[test]
fn json_success_uses_only_stdout() {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let status = run_from(
["ghidr", "doctor"],
&SuccessExecutor,
&mut stdout,
&mut stderr,
)
.expect("write succeeds");
assert_eq!(status, ExitStatus::Success);
assert_eq!(stdout, b"{\"kind\":\"doctor\",\"schema_version\":1}\n");
assert!(stderr.is_empty());
}
#[test]
fn invalid_json_invocation_uses_only_stderr() {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let status = run_from(
["ghidr", "decompile", "sample"],
&SuccessExecutor,
&mut stdout,
&mut stderr,
)
.expect("write succeeds");
assert_eq!(status, ExitStatus::InvalidInvocation);
assert!(stdout.is_empty());
let document: serde_json::Value = serde_json::from_slice(&stderr).expect("valid JSON");
assert_eq!(document["error"]["code"], "invalid_arguments");
}
#[test]
fn oversized_executor_success_cannot_escape_inline_bound() {
let mut stdout = Vec::new();
let mut stderr = Vec::new();
let status = run_from(
["ghidr", "doctor", "--max-inline-bytes", "1"],
&SuccessExecutor,
&mut stdout,
&mut stderr,
)
.expect("write succeeds");
assert_eq!(status, ExitStatus::RuntimeFailure);
assert!(stdout.is_empty());
let document: serde_json::Value = serde_json::from_slice(&stderr).expect("valid JSON");
assert_eq!(document["error"]["code"], "protocol_violation");
}
}

305
src/protocol.rs Normal file
View file

@ -0,0 +1,305 @@
//! Strict, bounded Rust/Java file protocol.
use std::{
ffi::OsStr,
io::{self, Read},
path::{Path, PathBuf},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use thiserror::Error;
use crate::{
domain::{FunctionSelector, InvocationId, Limits, PageRequest},
operation::{ADAPTER_PROTOCOL_VERSION, Operation},
};
/// Maximum exact serialized `request.json` size (1 MiB).
pub const MAX_REQUEST_BYTES: usize = 1_048_576;
/// Maximum exact serialized `response.json` size (256 MiB).
pub const MAX_RESPONSE_BYTES: usize = 268_435_456;
/// A validated UTF-8 absolute path owned by the trusted harness.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
pub struct WorkerPath(PathBuf);
impl WorkerPath {
/// Validates a path before it crosses into the worker sandbox.
pub fn new(path: PathBuf) -> Result<Self, ProtocolCodecError> {
if !path.is_absolute() || path.to_str().is_none() {
return Err(ProtocolCodecError::InvalidWorkerPath);
}
Ok(Self(path))
}
/// Returns the validated path.
#[must_use]
pub fn as_path(&self) -> &Path {
&self.0
}
}
/// Strict adapter request.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AdapterRequest {
/// Must equal [`ADAPTER_PROTOCOL_VERSION`].
pub protocol_version: u32,
/// Random 128-bit identifier echoed by the response.
pub invocation_id: InvocationId,
/// Operation echoed by the response.
pub operation: Operation,
/// Private staged Sample path, absent for non-Sample operations.
pub staged_sample: Option<WorkerPath>,
/// Private Ghidra project or staging directory when required.
pub analysis_path: Option<WorkerPath>,
/// Selected limits.
pub limits: Limits,
/// Operation-specific validated arguments.
pub arguments: RequestArguments,
}
/// Operation-specific request arguments.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum RequestArguments {
/// Installation probe.
Doctor,
/// Program metadata extraction.
Inspect,
/// Stable Function page extraction.
Functions {
/// Explicit page bound.
page: PageRequest,
},
/// Targeted decompilation.
Decompile {
/// Explicit Function Selector.
selector: FunctionSelector,
},
/// Store cleanup does not cross into Ghidra; reserved for parity.
Clean,
}
impl RequestArguments {
/// Operation required by this argument variant.
#[must_use]
pub const fn operation(&self) -> Operation {
match self {
Self::Doctor => Operation::Doctor,
Self::Inspect => Operation::Inspect,
Self::Functions { .. } => Operation::Functions,
Self::Decompile { .. } => Operation::Decompile,
Self::Clean => Operation::Clean,
}
}
}
/// Strict adapter response envelope.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct AdapterResponse {
/// Echoed protocol version.
pub protocol_version: u32,
/// Echoed invocation identifier.
pub invocation_id: InvocationId,
/// Echoed operation.
pub operation: Operation,
/// Exactly one typed success or error result.
pub result: AdapterResult,
}
/// Adapter result, with operation data validated by the harness afterward.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "status", rename_all = "snake_case", deny_unknown_fields)]
pub enum AdapterResult {
/// Complete operation-specific result.
Success {
/// Strictly schema-validated by the operation dispatcher.
data: serde_json::Value,
},
/// Typed adapter failure with no partial Query data.
Error {
/// Stable adapter error code.
code: String,
/// Concise diagnostic context.
message: String,
/// Operation-specific error facts.
details: serde_json::Map<String, serde_json::Value>,
},
}
/// Serializes a request only when the exact bytes fit 1 MiB.
pub fn encode_request(request: &AdapterRequest) -> Result<Vec<u8>, ProtocolCodecError> {
if request.protocol_version != ADAPTER_PROTOCOL_VERSION {
return Err(ProtocolCodecError::VersionMismatch);
}
if request.operation != request.arguments.operation() {
return Err(ProtocolCodecError::OperationMismatch);
}
encode_bounded(request, MAX_REQUEST_BYTES, BoundKind::Request)
}
/// Reads and decodes a response without accepting more than 256 MiB.
pub fn decode_response(reader: impl Read) -> Result<AdapterResponse, ProtocolCodecError> {
decode_bounded(reader, MAX_RESPONSE_BYTES, BoundKind::Response)
}
/// Validates all response echo fields before dispatching its data.
pub fn validate_response_echo(
request: &AdapterRequest,
response: &AdapterResponse,
) -> Result<(), ProtocolCodecError> {
if request.protocol_version != response.protocol_version {
return Err(ProtocolCodecError::VersionMismatch);
}
if request.invocation_id != response.invocation_id {
return Err(ProtocolCodecError::InvocationMismatch);
}
if request.operation != response.operation {
return Err(ProtocolCodecError::OperationMismatch);
}
Ok(())
}
fn encode_bounded<T: Serialize>(
value: &T,
limit: usize,
kind: BoundKind,
) -> Result<Vec<u8>, ProtocolCodecError> {
let bytes = serde_json::to_vec(value)?;
if bytes.len() > limit {
return Err(kind.to_error(limit, bytes.len()));
}
Ok(bytes)
}
fn decode_bounded<T: DeserializeOwned>(
reader: impl Read,
limit: usize,
kind: BoundKind,
) -> Result<T, ProtocolCodecError> {
let read_limit = u64::try_from(limit)
.map_err(|_| ProtocolCodecError::PlatformLimit)?
.saturating_add(1);
let mut bytes = Vec::new();
reader.take(read_limit).read_to_end(&mut bytes)?;
if bytes.len() > limit {
return Err(kind.to_error(limit, bytes.len()));
}
Ok(serde_json::from_slice(&bytes)?)
}
#[derive(Clone, Copy)]
enum BoundKind {
Request,
Response,
}
impl BoundKind {
fn to_error(self, limit: usize, observed: usize) -> ProtocolCodecError {
match self {
Self::Request => ProtocolCodecError::RequestTooLarge { limit, observed },
Self::Response => ProtocolCodecError::ResponseTooLarge { limit, observed },
}
}
}
/// Protocol construction, framing, or validation failure.
#[derive(Debug, Error)]
pub enum ProtocolCodecError {
/// A tool-owned path was relative or non-UTF-8.
#[error("worker path must be absolute UTF-8")]
InvalidWorkerPath,
/// Serialized request exceeded the fixed ceiling.
#[error("request exceeds {limit} bytes (observed {observed})")]
RequestTooLarge {
/// Fixed ceiling.
limit: usize,
/// Exact observed bytes.
observed: usize,
},
/// Serialized response exceeded the fixed ceiling.
#[error("response exceeds {limit} bytes (observed at least {observed})")]
ResponseTooLarge {
/// Fixed ceiling.
limit: usize,
/// Bounded observed lower bound.
observed: usize,
},
/// Protocol versions differ.
#[error("adapter protocol version mismatch")]
VersionMismatch,
/// Invocation identifier echo differs.
#[error("adapter invocation ID mismatch")]
InvocationMismatch,
/// Operation echo differs.
#[error("adapter operation mismatch")]
OperationMismatch,
/// Platform cannot represent the protocol bound.
#[error("platform cannot represent protocol size bound")]
PlatformLimit,
/// Filesystem or reader failure.
#[error("protocol I/O failed: {0}")]
Io(#[from] io::Error),
/// JSON was malformed or violated a typed contract.
#[error("protocol JSON failed validation: {0}")]
Json(#[from] serde_json::Error),
}
/// Returns true when an OS argument exactly requests human formatting.
#[must_use]
pub fn requests_human_format(arguments: &[impl AsRef<OsStr>]) -> bool {
arguments.windows(2).any(|pair| {
pair[0].as_ref() == OsStr::new("--format") && pair[1].as_ref() == OsStr::new("human")
}) || arguments
.iter()
.any(|argument| argument.as_ref() == OsStr::new("--format=human"))
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use super::{ProtocolCodecError, decode_bounded, requests_human_format};
#[test]
fn response_reader_stops_at_bound_plus_one() {
let bytes = vec![b' '; 9];
let result =
decode_bounded::<serde_json::Value>(Cursor::new(bytes), 8, super::BoundKind::Response);
assert!(matches!(
result,
Err(ProtocolCodecError::ResponseTooLarge { .. })
));
}
#[test]
fn detects_only_explicit_human_format() {
assert!(requests_human_format(&[
"ghidr", "--format", "human", "doctor"
]));
assert!(requests_human_format(&[
"ghidr",
"doctor",
"--format=human"
]));
assert!(!requests_human_format(&["ghidr", "doctor"]));
}
#[test]
fn request_serialization_rejects_bytes_over_fixed_bound() {
let oversized = "x".repeat(super::MAX_REQUEST_BYTES);
let result = super::encode_bounded(
&oversized,
super::MAX_REQUEST_BYTES,
super::BoundKind::Request,
);
assert!(matches!(
result,
Err(ProtocolCodecError::RequestTooLarge { .. })
));
}
}

163
src/schema.rs Normal file
View file

@ -0,0 +1,163 @@
//! Deterministic JSON Schema generation from the public Rust types.
use std::collections::BTreeMap;
use schemars::{Schema, schema_for};
use serde_json::{Value, json};
use crate::{
error::ErrorEnvelope,
operation::{
AnalysisProfile, CleanupData, DecompilationData, DoctorData, FunctionsData, InspectionData,
QueryProvenance, SuccessEnvelope, ToolProvenance,
},
protocol::{AdapterRequest, AdapterResponse},
};
/// Generates every committed version 1 schema keyed by filename.
#[must_use]
pub fn v1_schemas() -> BTreeMap<&'static str, Schema> {
BTreeMap::from([
(
"analysis-profile.schema.json",
with_constant(schema_for!(AnalysisProfile), "profile_version", json!(1)),
),
(
"cleanup.schema.json",
with_kind(
schema_for!(SuccessEnvelope<ToolProvenance, CleanupData>),
"cleanup",
),
),
(
"decompilation.schema.json",
with_kind(
schema_for!(SuccessEnvelope<QueryProvenance, DecompilationData>),
"decompilation",
),
),
(
"doctor.schema.json",
with_kind(
schema_for!(SuccessEnvelope<ToolProvenance, DoctorData>),
"doctor",
),
),
(
"error.schema.json",
with_constant(
make_extensible(schema_for!(ErrorEnvelope)),
"schema_version",
json!(1),
),
),
(
"functions.schema.json",
with_kind(
schema_for!(SuccessEnvelope<QueryProvenance, FunctionsData>),
"functions",
),
),
(
"inspection.schema.json",
with_kind(
schema_for!(SuccessEnvelope<QueryProvenance, InspectionData>),
"inspection",
),
),
(
"protocol-request.schema.json",
with_constant(schema_for!(AdapterRequest), "protocol_version", json!(1)),
),
(
"protocol-response.schema.json",
with_constant(schema_for!(AdapterResponse), "protocol_version", json!(1)),
),
])
}
fn with_kind(schema: Schema, kind: &str) -> Schema {
let schema = with_constant(make_extensible(schema), "schema_version", json!(1));
with_constant(schema, "kind", json!(kind))
}
fn make_extensible(mut schema: Schema) -> Schema {
fn visit(value: &mut Value) {
match value {
Value::Object(object) => {
if object.get("additionalProperties") == Some(&Value::Bool(false)) {
object.remove("additionalProperties");
}
for child in object.values_mut() {
visit(child);
}
}
Value::Array(array) => {
for child in array {
visit(child);
}
}
_ => {}
}
}
for value in schema.ensure_object().values_mut() {
visit(value);
}
schema
}
fn with_constant(mut schema: Schema, property: &str, value: Value) -> Schema {
if let Some(Value::Object(properties)) = schema.ensure_object().get_mut("properties") {
let property_type = if value.is_string() {
"string"
} else {
"integer"
};
properties.insert(
property.to_owned(),
json!({ "const": value, "type": property_type }),
);
}
schema
}
/// Serializes a schema with stable pretty formatting and one trailing LF.
pub fn serialize(schema: &Schema) -> Result<String, serde_json::Error> {
let mut output = serde_json::to_string_pretty(schema)?;
output.push('\n');
Ok(output)
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use std::{fs, path::PathBuf};
use super::{serialize, v1_schemas};
fn schema_directory() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("schemas/v1")
}
#[test]
#[ignore = "explicit reviewed schema update workflow"]
fn generate_committed_schemas() {
let directory = schema_directory();
fs::create_dir_all(&directory).expect("create schema directory");
for (name, schema) in v1_schemas() {
let content = serialize(&schema).expect("serialize schema");
fs::write(directory.join(name), content).expect("write generated schema");
}
}
#[test]
fn committed_schemas_match_rust_types() {
for (name, schema) in v1_schemas() {
let expected = serialize(&schema).expect("serialize schema");
let actual =
fs::read_to_string(schema_directory().join(name)).expect("read committed schema");
assert_eq!(actual, expected, "schema drift: {name}");
}
}
}

43
tests/cli.rs Normal file
View file

@ -0,0 +1,43 @@
#![allow(clippy::expect_used)]
#![doc = "Black-box CLI framing and exit-status tests."]
use std::process::Command;
#[test]
fn unimplemented_operation_is_a_typed_json_runtime_failure() {
let output = Command::new(env!("CARGO_BIN_EXE_ghidr"))
.arg("doctor")
.output()
.expect("run ghidr");
assert_eq!(output.status.code(), Some(1));
assert!(output.stdout.is_empty());
let error: serde_json::Value =
serde_json::from_slice(&output.stderr).expect("valid JSON error");
assert_eq!(error["error"]["code"], "internal_not_implemented");
assert_eq!(error["error"]["details"]["operation"], "doctor");
}
#[test]
fn invalid_invocation_is_one_json_document_on_stderr() {
let output = Command::new(env!("CARGO_BIN_EXE_ghidr"))
.args(["functions", "sample", "--limit", "0"])
.output()
.expect("run ghidr");
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
let error: serde_json::Value =
serde_json::from_slice(&output.stderr).expect("valid JSON error");
assert_eq!(error["error"]["code"], "invalid_arguments");
assert_eq!(output.stderr.last(), Some(&b'\n'));
}
#[test]
fn explicit_human_parse_failure_is_not_json() {
let output = Command::new(env!("CARGO_BIN_EXE_ghidr"))
.args(["--format", "human", "decompile", "sample"])
.output()
.expect("run ghidr");
assert_eq!(output.status.code(), Some(2));
assert!(output.stdout.is_empty());
assert!(serde_json::from_slice::<serde_json::Value>(&output.stderr).is_err());
}

58
tests/contracts.rs Normal file
View file

@ -0,0 +1,58 @@
#![allow(clippy::expect_used)]
#![doc = "Published schema, registry, and reviewed-fixture parity tests."]
use std::{collections::BTreeSet, fs, path::PathBuf};
use ghidra_cli::{
error::ErrorEnvelope,
operation::{
CleanupData, DecompilationData, DoctorData, FunctionsData, InspectionData, OPERATIONS,
QueryProvenance, SuccessEnvelope, ToolProvenance,
},
};
fn root(relative: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative)
}
fn fixture(name: &str) -> String {
fs::read_to_string(root(&format!("fixtures/golden/v1/{name}"))).expect("read fixture")
}
#[test]
fn registry_has_exact_schema_and_golden_coverage() {
let mut operations = BTreeSet::new();
for descriptor in OPERATIONS {
assert!(operations.insert(descriptor.operation.as_str()));
assert!(root(&format!("schemas/v1/{}", descriptor.schema)).is_file());
assert!(root(&format!("fixtures/golden/v1/{}", descriptor.golden)).is_file());
let value: serde_json::Value =
serde_json::from_str(&fixture(descriptor.golden)).expect("fixture JSON");
assert_eq!(value["kind"], descriptor.public_kind);
}
assert_eq!(
operations,
BTreeSet::from(["clean", "decompile", "doctor", "functions", "inspect"])
);
}
#[test]
fn reviewed_goldens_deserialize_through_public_contracts() {
serde_json::from_str::<SuccessEnvelope<ToolProvenance, DoctorData>>(&fixture("doctor.json"))
.expect("doctor contract");
serde_json::from_str::<SuccessEnvelope<QueryProvenance, InspectionData>>(&fixture(
"inspection.json",
))
.expect("inspection contract");
serde_json::from_str::<SuccessEnvelope<QueryProvenance, FunctionsData>>(&fixture(
"functions.json",
))
.expect("functions contract");
serde_json::from_str::<SuccessEnvelope<QueryProvenance, DecompilationData>>(&fixture(
"decompilation.json",
))
.expect("decompilation contract");
serde_json::from_str::<SuccessEnvelope<ToolProvenance, CleanupData>>(&fixture("cleanup.json"))
.expect("cleanup contract");
serde_json::from_str::<ErrorEnvelope>(&fixture("error.json")).expect("error contract");
}