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. A small, dependable command-line interface for read-only Ghidra analysis.
The repository is named `ghidra-cli`; the installed executable is `ghidr`. 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 The project now has its version 0.1 Rust contract foundation. The typed CLI,
common reverse-engineering questions easy to ask without exposing Ghidra public envelopes and schemas, bounded Rust/Java protocol, and synchronous
project management, JVM lifecycle, or scripting details to the user. execution seam are present; store, process/sandbox, and Ghidra adapter execution
remain deliberately unconnected.
Proposed usage: Proposed usage:
@ -33,8 +34,10 @@ available explicitly through `--format human`.
## Status ## Status
Documentation first. There is intentionally no Rust crate yet. Implementation Foundation only. Every accepted command currently returns a typed
begins after the initial requirements and interface are reviewed. `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 ## 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");
}