feat: scaffold v0.1 foundation
This commit is contained in:
parent
c4fb0c6a01
commit
170ca32f58
34 changed files with 6175 additions and 5 deletions
438
Cargo.lock
generated
Normal file
438
Cargo.lock
generated
Normal 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
42
Cargo.toml
Normal 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"
|
||||||
13
README.md
13
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
32
fixtures/golden/v1/cleanup.json
Normal file
32
fixtures/golden/v1/cleanup.json
Normal 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": []
|
||||||
|
}
|
||||||
47
fixtures/golden/v1/decompilation.json
Normal file
47
fixtures/golden/v1/decompilation.json
Normal 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": []
|
||||||
|
}
|
||||||
21
fixtures/golden/v1/doctor.json
Normal file
21
fixtures/golden/v1/doctor.json
Normal 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": []
|
||||||
|
}
|
||||||
10
fixtures/golden/v1/error.json
Normal file
10
fixtures/golden/v1/error.json
Normal 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": [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
58
fixtures/golden/v1/functions.json
Normal file
58
fixtures/golden/v1/functions.json
Normal 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": []
|
||||||
|
}
|
||||||
48
fixtures/golden/v1/inspection.json
Normal file
48
fixtures/golden/v1/inspection.json
Normal 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": []
|
||||||
|
}
|
||||||
167
schemas/v1/analysis-profile.schema.json
Normal file
167
schemas/v1/analysis-profile.schema.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
417
schemas/v1/cleanup.schema.json
Normal file
417
schemas/v1/cleanup.schema.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
507
schemas/v1/decompilation.schema.json
Normal file
507
schemas/v1/decompilation.schema.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
304
schemas/v1/doctor.schema.json
Normal file
304
schemas/v1/doctor.schema.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
241
schemas/v1/error.schema.json
Normal file
241
schemas/v1/error.schema.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
539
schemas/v1/functions.schema.json
Normal file
539
schemas/v1/functions.schema.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
518
schemas/v1/inspection.schema.json
Normal file
518
schemas/v1/inspection.schema.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
341
schemas/v1/protocol-request.schema.json
Normal file
341
schemas/v1/protocol-request.schema.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
schemas/v1/protocol-response.schema.json
Normal file
120
schemas/v1/protocol-response.schema.json
Normal 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
280
src/cli.rs
Normal 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
63
src/domain/address.rs
Normal 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
80
src/domain/identifier.rs
Normal 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
81
src/domain/limits.rs
Normal 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
13
src/domain/mod.rs
Normal 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
75
src/domain/path.rs
Normal 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
14
src/domain/selector.rs
Normal 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
249
src/error.rs
Normal 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
12
src/lib.rs
Normal 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
20
src/main.rs
Normal 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
647
src/operation.rs
Normal 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
214
src/output.rs
Normal 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
305
src/protocol.rs
Normal 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
163
src/schema.rs
Normal 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
43
tests/cli.rs
Normal 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
58
tests/contracts.rs
Normal 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");
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue