From d1447197b39462ec3584925de91779d8e3b06bb0 Mon Sep 17 00:00:00 2001 From: MaddoScientisto Date: Sat, 21 Mar 2026 09:50:09 +0100 Subject: [PATCH] Add JSON output schema and enhance CLI command aliases for improved usability --- .github/skills/pyghidra-ghidra-ops/SKILL.md | 23 ++ .../__pycache__/cli.cpython-311.pyc | Bin 50426 -> 55899 bytes .../__pycache__/common.cpython-311.pyc | Bin 31448 -> 31448 bytes tools/pyghidra_crusader/cli.py | 261 +++++++++++++++++- 4 files changed, 269 insertions(+), 15 deletions(-) diff --git a/.github/skills/pyghidra-ghidra-ops/SKILL.md b/.github/skills/pyghidra-ghidra-ops/SKILL.md index f4732da..6514bde 100644 --- a/.github/skills/pyghidra-ghidra-ops/SKILL.md +++ b/.github/skills/pyghidra-ghidra-ops/SKILL.md @@ -62,11 +62,14 @@ MCP-style read/query commands are also available from the same CLI: ```powershell .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-function-by-address --address 000a:48ff +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get_function_by_address --address 000a:48ff .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader get-function-containing --address 000a:4901 .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader decompile-function-by-address --address 000a:48ff .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader disassemble-function --address 000a:48ff .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader read-region --start 000a:48ff --end 000a:4912 .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader search-functions-by-name --query rng_ +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-methods --limit 20 +.\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list_methods --limit 20 .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-strings --limit 20 .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-imports --limit 20 .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader list-exports --limit 20 @@ -84,6 +87,26 @@ All commands also support structured output for scripting: .\.venv-pyghidra311\Scripts\python.exe -m tools.pyghidra_crusader --format json get-function-by-address --address 000a:48ff ``` +JSON output now uses a stable envelope: + +```json +{ + "schema_version": "1.0", + "command": "get-function-by-address", + "ok": true, + "schema": { "type": "object", "properties": { "name": { "type": "string" } } }, + "data": { + "name": "rng_next_modulo", + "signature": "undefined rng_next_modulo()", + "entry": "000a:48ff", + "body_start": "000a:48ff", + "body_end": "000a:4912" + } +} +``` + +The CLI also accepts exact MCP-style underscore command aliases, so local automation can often swap MCP names directly with little or no translation. + For ad hoc investigation, prefer `run-script` over multiline `python -c` or pasted PowerShell here-strings. It avoids leaving the shared shell stuck in an unfinished string/block state: ```powershell diff --git a/tools/pyghidra_crusader/__pycache__/cli.cpython-311.pyc b/tools/pyghidra_crusader/__pycache__/cli.cpython-311.pyc index a363335a5f01547c60adc20ac892ce8cb8fdff14..1bc6291388f50208aff9197abf810df338f2a6da 100644 GIT binary patch delta 17814 zcmc(GYj{)Fm8g#1mMmMcEy=e0kl&WE{NQN}1`L=-5=_A65grl3M>YakGFvjij*Xm@ zWJp62$WB_PAt`A)2{g1Jb*4$%6zFu?p_BRAW5!%`)zXrC+uP3VBWlxrle+CUcddQS zkz{P=w)e*!eaCCR)>?b5wb$8u?R~bVpHThqEmiGnCX+z{zX$eS@sGWxQ2b9U$iHkc z!+rQ=&Dq+oM*C~`)N%@5yT`@r`V_qWa~z++8~pP_{W%S9Jg3@I$M7@f!8ZYi%yVjn zxq#R4S$uY%at_YXkMB%uHJ{61jOtmJmb5Oh8xhBHPQ`HC3@0ZICtVp4KbKK480Axi zoO61{qJgo2ZGG!L|6?_q2ELo}KoRV^UDdX-$?$KFeorcOgMIC{p z%vTSvoWTkPD;ca}u$sXd25WtFfazkejxYAr@nWY7((?7me3av1#6X9JbREDFjiUU( z3XH}#&uU+oY+uCM7boe{6UkZA$RI!Ef3>YJprSxOg5X@itD zO6ej@ef7S2e%U$G-*bBw^C$S$lj=Q7_$a>$(xvm_hBN@_4oLeU-3e*% zq*uLX7i2NFRXoD5QHJC6MlgGz{rJ{we+#r2F}&`Qwm22=zmddiY29XZR7Y z;Q^nQA3dVP36A6{6~2S;cYt0QDAGU5@yafR>kJJjE4XrcM)`x>oZi7;PsASzdcyv` zpf@r|d=n3<#<;9uRdYjATpK!g*w+)eNW*Q7(YQJ?G~kPC!x7>S_PJD~5xR)$iSOv3 zpZNH=VSt1Nd?e!cgYzCIs8ZqREuuJtl8 zTp45;3f_XC9zh*;qVw{6l=#A7k(oN;J01bj9^xN}#MJ`p@=z~P!DqoN%ZHQ#_2mKMhH{c%zvDE|&7S}vP{1IPV=R5A}8N|%NP$=L7zG}u7 z=%T1>f6&(-3i^BEnq%I;pf9ct4h8})Ra_VFA0%Ef6xRg22YmrXBZ(x+pgsbMLk+4U zDxn?l5??SP*4&HYDpUDs6BrXs@9y;neBrppOQOU(1Srht8j!U=JZpPj1%0j9 zTs3VB`y%e1P=7y&K5fGS?}Oq1NJ->+nkXqKn6~#pquiDI;E)@}5QmUHQd+=IX9fJ> zRC9Pbla>2@kwYOqJe?o*c}dTqBzYL=QL^crWOvENxFK1`UzBX<0V~k!6{Qw75>SiI zL0D~er7cRfK~b({c~zSAG|Xx))3gy?L$dF5ChBvX_H2pl+Dyf4Z{Q89vCYVs|fr0(9m)_?~Qof;?Rrs>IrzmVZb(! z!Ju1w9i|Q5fq}q~8(s=}Tir9|eg)vJlyPBj*>6-kqNQgu?8Pp=jbFtiUj3>DWP?BP$0DH7-nbdo)_Y)Wy_dg_HX0W=w(y-m zyH%!@dmPn|=uxI5pJ$9N%SQfwK9})I>)o-9-wqtQW^u?E$(X^RICCBj+1>mO;ILEX zKp)8z<;Rgj8}H(GL;C~C_Mb@Yd!+Wg{61*kpKNb4RpdSxvB|n96ugH&aLG%R0*^kKhfF(a8Zn_3bMh<5n$7Q!$S>Q^9|nF$WPbFCthr^nItPz{ z#G{`N0*_FVM`QNfJbc-+<0DCqTP<^Qe9SUisz)U)Kf#{_9*@a9 zl3M1D9+$NIJN^{#dqU<%U(A_XCN6h2k55WGp5#vhkEfD6j^FUWLB3)x5PXrTg}FW`L>=UKTG<{j2LFW1lTF9O$dv$*Du}# z`&k*6p0%dt@^4sY&gJL%R{-~ON!(Ifh3@kp;TJ&5%{G_r@A+*0)k_wf9UQOm7XbI7 zj4R7N>KJh(W%we{mD*i%zQuc?{_a`fb!iMQ@m~VoUzT~(_w3@#^ak+z!#w<&^XKPx znST@beI?27mHgD`y_f%SJ~5d;+b1L`9TWUp!1W(wu1O1&jFhmk{VG`Q?K#GlwHChv zw*1;9%dC-CKoI`*2}{8_u0RZQhk)_0-tF!Uk$!K4#jf;U3dNc4Hx`l|h#|#?9y!i~fYU=S7jNnq z&H;6#y@ee4v49?8;1kG5lml|Z2Ddxtdx(X1VgS4o+p7UkP07=)#}LP~P7FDnxCLn- zyPz(P(WfUF`VkL(yJUI!aE4nBL&jvS-Q|p9$;+{w9OQKL+Of4 zvIp8o4uIiRbZe=DzFpdxN%kTEi&{N2(|KyLX>wlM9DtHU~!p8ierbj-3L*w zllS#P{LMRMdpKE~=Y-*O;syZrOU{SQK>D$mkHyyV^#&5e zq63T1ly@1(ek@jD@z3R34dg*Any~1p@au>da`9q1{d6(?c}1;(^k8!qHdj?PW|BT^ zX0f-2?yX#EBm^rk#`VxoSN_024k5l3@gr5h-H*jQEdE>7qC9dKa&e5uQ$tIw)oV>8 zj16LV?3wUXmvCGuJyG-L%v_AMr7@d0T06jLN&(YJf9C2leiL#(g1_(=O%uE79^yJI zPQhFjGp$JI6q>@{D%2Y7?-3*z$W-JO-^!`Eky8`XTVs}zGubiI{eYQU3z)fxnX4VU zH({Xq`VNPsTrgL}Ox@6Kt%G(ewp+DhTcEwm-9kT8-z(!Z&V^%jyX5wjbG3K5I~H2p zg1I4P>V$UZ66nK;eK@s(x+q~#Xo_#CT{qOOnBF4f)?DbjrWWqy=~Z`G+vWSN9TRr% z6M7yJ)UHYO@hSCjL4Ev=we*&??uNB4rQAYbY+C?~ZOGWB9qR<)a~lSWEH1%Z7c;Gf z_OhkWUWV;u&~Y&!yxdT;`tn|>3nHlNCe`67by!e`@02aLRkrj-+0vN4EM}<_%nM?s z7T{6740x0yk8-Tm+o+fpcrq z<|4^MY5<{R5g?QxLJ1%g0>aY^=j^&_p00tLDBGfW5bEhCnjI@ImtH$A?1V9?OpVDL z3Zl%j?I|=Tds#XULK_V%%#;;d3rKS*23!^h=K7dv6(E!@2F*)R^HR{<0h<4Mp|8kN zC77#Yre;8xJt~J5Z7i}h3FhXQX$c_A@_hOoM5qzWwJ{UEI)#nUMIm-k2wm8qi}JnHt_G|_(A%dvmJ%BJ<$NZ(fLQ#Tk(0=y>Gx6-wC%E z7e{$GSQPjCfX99i;ix*}oM)PIySTWH_~1CA#|K@051P}vw0nBIK|E3O2B2RSiT}Wkj!_x1V)xRPica?dN7{ zHp20mbDB|g1eY`E7s-}$3dLc)oF7s1T3&fcN3XnInxXGfj3^(mL`?MBtA+XStqCe3 zxiTvB%SB)Xqub8d))3nWw_dT|GO9`MV?^^RX!1Ia7W;9E(>Zf6f%P4k)RXFT;&}w* zQUYpF9ur;DTD%@|u8dCdFm%%`Mme>Rb!p;R;hsajey``4kA&gGk&HrhTp2ooC$ZQ7 zktrNd{W}brf(Bb18#3csDWb)}6w$3)cXZ#squbN9Zd1pWcF)d^?OmIHiH!{hlE$}UHQ ztd)~lE2jYJ+otqwg1(KqRy@yLp;uQpY38yF<#vu5SJzSDh>8AbMRrz11F}iK5s0qV zR?>eR$TH8YKCh;~)#TBp4cYYP&swR_k@c}ioR*=BXxYu&ISmY|vXi)=AD1q!0|R&B zTWp3hc>zG&1XUei@1grcV0QY`=W4nya&f&Jv!=hZmQ-LB33+&bPlS8`ELzdLryw(? zxMR-0Wp>^$J15QMQ|9v9)|w0YNo(_zwV8f%m3`N3Tfunkt-`t+g>{pK^;3oQw+dTt z6t+$luAC}dIcaN~vbEjLvyba<<=5WGubs@Vo64`dmEUqBzhyGNbt=DgGH=CH-U=9k zOo+QgpwW^mFe8mM{Q@=mcLcu%kS=ysN^Cq2Cm%xldL{kb>ZLF?KVCgjzYVGWfK=$h zPH9ri@?b>l2@!ClfyfYyc@|9~2ohG5W03IN14J%Lcz$9#_b z5gXOyPuTcp`iu7BayHdk<;hMu=l8I4e!_uw-lv?XS^JvCIjwAUPOF{nRZj9dWKs~< zA`!fJ=)(=vqfkNajZcDmsu17Cw<^Rf3vAZy9)EG zsblj}$8R7TMt5NEP8uNZ(!k~dEqotW>`+cTx%sDRrJAEhJFU^jkopFK?Eo&S#ZNcM zuQqINJFFK!I`Gb6wbL7Mx-v-#Qq&+|sJQcjhkH9kFqaL zOqLd^?<(4Pmw;t}70=v07>xM)eI0~^NJOe5%rd_)Sm-?__Sh7TcI_!{-{*9yEW?Q?6YP^=~QZhn^KM87)4*b9Rg=7-?Ju zpF(B&I{Jp!%Dqd!2JM5Fz+2X_yyP&v5wf>k(@r^peZ9c`HNl4vm#yH zToIls*BsktNt6N3cUU zVyFh>cm{k}^MR|wx36S3-{Xt4UC{WRlKzq(Kzl}?iqaqXPVT~2$E6h=2~8jlI4(jY z8v#xe7t_6|1d);x1oEZm5n^;Y88IBmUnoen*d#(;Y{b7hhCd_;$-#g8%c zeTX!lq5slfWH<~6KSk^Q7_CcNgRX2;>75|}lR2BGayHX&@LBFdstwgo@Q00DCJsB< zgJ3U5Hyqu(7}X;O5X`CEaIO4;r=9T714CBh2hcC!k&xL*WYqh&GuoFxMtzJCn$Ha8 zlMm*Y@yH=;^8=Xa3V2$gusV8|X-?^GXBP`4+u^yM0bsIZ z`xKPAdndE|rn37^sS`S&KCLFbb$0%vk3LFOnr85#iJ(?&xykf6O$#NHCA_8No*>BhwF#R;*+%AG%{)b9B&0hUW6hd2H^a zmybG)BmzeMJsSBA^Sv`~%LlwYbc&7feYGVg-f} zGhZ-TKIrEDliokr5@l}}x`%WhKzz4g$z#eC18JHT&tNh6g27CeT)#+iCX4GNPU1wS z1rk^n;3w{TW;$2c0_V)ZzAzaELH~@3f1gch3y;M(tvGnpeEgaZmDBBH z_erKTPFPFY*yWjFm6NoQcEE$&Lv3WOm^;J&^ssW6Tcb0o;1n3BhRj91AYR~Ne2oxhPgJ(Z2-_YtaGxPKyBi@G@r&V zj5aB)D*E2&!@bNAa={UBv+QJhR>>EndDf21|7!h7C!({pEZqg@mroRH9|u`2Dmr=M z2KNbi;;91q&FF9C(SB{x52DJ>phYsOY1Q?CwkYvvdb#rVjX- z;jh;LK5vN^S?18A{n0}0lb}bbih3UvX6y0pNCDk@vU?NRgnDpgWHY82D+hxtY+l9o zPHd4L&Rhvrm)7=G^E&5WPP&ziDq8=T2b}WsWBH;}7XFP2v3ov|@N9*E&s`J0jCvNd ztOu9;@H*hgE~ysimD8ED@$redRnI)t)xw7H8T8BFp_I&F@5kajP@LxW&#UVADYtex zz64#g>9Jz^i&L$sX-c}8G;>(-Ebea^R*A`9V+9e_5+N$8+2`L;`j|DeD3}g`9)S=0l4e~eO$UtoE#v*;rRSokjuy~{bfK=ZFJjbZAS7Iv<;?2Q=doCuAGmgi&pk=q5=JI16|{{ z(w}~=AbM&J84Y3(mwLaL&0j!~*-7LahW|JhPg#G)Q3vW&h;n2yyOXy;u&8R{rOzJ| z&yrk5c9(;FXJ=p2#l0vyOJF-yWX(l^`5G|ttQU4!>BNf<1B?2}- zpjFe~$+!`u*#h^#Jp*32_np!KzE}52HQ~K5 z%yIA2A6@)b%frxJT#erxzzkQRIj%hf7dPNeHU!Zy?})*+!GjncIJFFt?*h`7RP^Q- zU(%74fYZa#Z>`CrPrtsi{U#K<$$tX~dkS{kg6zqUv4UgB>F3o(5BmZla=i{*zoMd5 zmm1-qXUC;wnkXYhAqbwO4jbF?+OHEuaBd(t+r`b|DP;1D4{py()iaL%!f6-n&Ejco z8cFIV(hth22(FT;vO0pt z@TsyUf^ToCti7wO!!L*O5BTYm7ZsyW3l$j=jN7Cilns$Wxy0Iyk)j#p%m^A@`T@Qv z;*?7aKP!TRApM{{JAwm{Dw`uXtH2L;0VseaQj0n9M>+cDJI5DCR>>7`Hoqn_ZSj zFo57F0=x(1GKtrl`mrL2U_XKf5qJ^wAm~F7J%rGY;4lJ$APm4&PQD2V{2Ppygnb?_ zf!+4^v2hdu#`L5X9hgIaUnIyv1bFVw&Ya0A%(WxHb4P;bgapsX*ohFq4J&z^Q+N~v zw?k|rPtIZv17?B&Cp*<37`73N0|~}K1fvnQJhO$6EgEc^Bk0U<82@kB{t`-e6~UkA zn_qWW--T>k-S71WNetS52~HgzpznXZJepf~VezGvlesNZxh-SsPu_RiR4`sSX)2vE zm5yyls7rITa5?Cvy)L1G>>a)RWn8Gg`hTVZl<4BBT6f#o9zJ7V@Nm6$>`0V8=qfBBS7DVN;?2 z3x$dd`_26NgaZpjii~^|t{4j?k_e?(a7t)pSSXhY6;Sv?r9xBSzMWrBfA@{@Xd5;w zWB|9i)^LiRu9iE&3wXf(mSp zC*_$4_n_`-_O*a?au9Nel1bILG^gqFJ~C0W6@XxV1}<~?DI`eg))O~)VAaEafb)0= zPU7TK2ym6fYX^}-_#a*@+{r{rE&by+zYt|>QyfvUH|*$K*9})0q)QBO^Y)Gn9osuP z*LBD>rmpVx?j2oe#qFCrH_FAVu8xgcIy&W>5pnbS_U?Ai=I)LyGKOjMmixDEPa>E) zc1P38=Favl9bNagr#j4BcVBy#+#mTNs)I9>I2W<0iA7W&U>*BHFpW7jKe3tWu1Si0 z`@*kd$(ahqfNk|y;JXD|x)97Yec^$tIA_TdnKX~xHHEQ>i%nr4KYOv@!N@`doXALw z9=Tdr$u<{PP>Xi~OzT&|o!P;FZx#6k;J~8@|H?owUR@on;FJl)e;tVam-enqME}3( z4qt%^N(6-!=6{nlX5J*QCwj+XAJ4~omA0bsLxQy?mXkkTD_AOG_Tus5f~_`Yb&T&8 z@~YvE2hOqxbcS588i28En8}gZGJ!owUfI?&$Av6sOy*taJns?;>Ju3nV^r3;>92jCj9b#wV$*ETGvot{1Y) zP;ka8zvO&|V6Q_#3-E$5;JE}#WegP9EaX-vEE&1lC>Q}dG37uf0LHRl)k6te1@_#L zwUwDl!-33N&hHco>jjH@4!fqzYHk$RljM|68H)n{-RF-9MGZnO+<*KSt>GRhK1nv9 zRqgo=LIJxUD%twBKHi_Gz&wOECF7%sD$G}7KANb(e65&wVZKhGv5r40>bKkd~88@fW8>%0d#Q408Zm9 z1+hFgbclVH)m+FIoD1LCaBY*YVvF#ANAUWE-G>F+5qN+YhXa8@QBox|f)Uoh6DzG2 zn!!kG;fYn$2}?SJ!VU1miYqTfM}@pLcrZUl)>$JgTr1eu!Gms)C0JL(6HBiK!Vw{F zB|Nd5Qg{*}Cb$BnM*vQEjDurn@ zA)dTytVzkIRU1u{Oq1GlHNU>LY1$+}v9j)aHc4Nb)%Im2v}x16X_LOa&;0mloAuUP zou&JH_UG)q&pzkub1wh!t(d^^nBwz#eH@3s?JqmcU*fp`VvP_`221{vC5nfNU-a>u zxK7j_Q*JL7*NgE`TE#|jgP3$owY@}a5>ue3RBRU0pez$Nis?|=#1_#EWx2RXv_M$_ zWfqi`P-a6p8_FCgtHjM>9+Y#SoeyO-lm$@Eg>n{@^TaKpuLvsh#al%ylr`d3u@uS$ z;x@4y%7s8!0p%hnE1|4~ayFE8P*y=%59J&v7eiSM!Dl&br+RY`Ft+@ zT=kCseYKCL7vsyR`Bbdp(^UU27sBcm)@CZ)#O1vAL>a4tFAzt6=tCyB<5=h0at{$B&((v>egUgRU($cAnqiqs!X(n zYUN@@a9~rVI6Eu_2gTnJWv`O%PBcX&8my)&an3Y4Rj+Ci-2=%JMg|S5#kpY~`e|~$ zKBVE-W5CQ9T9T3<)f1@aiSr}W1Awam8y0{KFQr&D3!%O!Sl89Ib7I{SyjU+TrrD{b zx+P*mgl}tVl<#P2ln?#2R9qIJ`)C4PdfFs9({gb|m@eoK@JL*59J3U4Vm=0WE5%h| zI{HCcaNO1CbHLpqu8H7Q8}d`vO2)7(4p-v(OO5nSgEe)-r2Zzc8Ty4(t7)Uy66O!{ z9ntsdQHL326&t6^+7y(f+bnJg%cAFuGwU)k1OKfd{;lG+2>(jc%>2Ek>Emw?@$V3K zM)=2Xz+ad?m4BCb8}LKF$}a8>#;UGW+!K*>HhpGIhNeks65B$Wh@vCHe<)*S{!1BC zHE9y}hWOjXeGz^kb7uZ6nbY~5A^!d1fe8PbnfZDbL{rD5+|YVZn`e}G$~;|QI85B4 zCoC`$CWc-E3su|#Iw2e%1O9`P!ine^&JVNlHNBwy5zxe#ZPnZuZMKBO0rUN9G2+!^Ybc70l~1c zrp?pQ(drgASrxKUba*9MOB@HgoWvmp^qs`B-_a)7Xj(zFo@WdJo!HgUX`^ig4s|t? zLfbwSp!dgF z=xc=y`hd>skZh=t!!6NOMGq9zp#}P7!5%^C?sSv~j7TIpfUMK$au6H+yl4S^J1&Fn zE-p<8nA!mw*21=@+eV)#UUW*Aq3EfnPZyi1(%PsAB)FVjFzYt!T0M9Lq>`XV=nK~J z#DJb*y^eNN!%N*Ib)Yxv6I-QLn^SUhcEyO)UxH_QP4r&&ek>%*;>}32&B^oJJRXPWgBR- zE{&ROPI^R_L!Yu08v|*g(~E<5>~T%->CbHynt%=_M6}W5@_Fh&9O-h~=*IGQU{v(a zVdB{9wMiaYTCpK1kPz+KOFSL)SjC*gK*}UP(w|f;)du2PJ3C$7Hh*SiHqU3%1+)L= zUsKi2E3;vi=xcLQ>ChY{`#(Hqovsf$F#x?s96LlFz7b{#!vzn17Zy7zV(1yFc14lC5ir7BkSd^5K|Q3jjB7bn)-|PdL}?w!JBO6kVdbt-<*p&+uJ;T%*9;{ih7zWE6-Y9cf+QnKGOGI;K|fM8B&%EE4Pg*w+$(`y_Z{hEqC5X?z}N=?wGy?`10lh zUmo)10be%o=@$T>^hU*yvUFIveN?%9NV)yJ%z|r~6(gAyjIROs%$2}rMm{s}nSjr` zz-rX52UOuAKougY5Kwu5y0W0!s4oFjRu!PK5S0a}3_xiY*5#RJT{BmXm@CJ$rZIgv zkXdE}nFYx#K$Z?WcuCJqJ+Ph{^_3CZHPY>hhxM8OQXqfNZ*Yd~F$hwl>SC zpK}BBs;Z%R8;6ty!^)OXWy_GVWrEQl74^D_)Oj~SC+B4qUCXK($*LODW{l~V0_jX< zfQ9vd%BcnmbI`&Zu+R(^`s*A<{cJ!@cVB!%A>Ce|Yt+vL^t3pmr|VZ4S#V}nfY3}7 znh8RUAT&@9(^Ust(^2({R~Yp!KxLEzR|axr0G9!{9$VaO(AxlIDg%@WQ6_cY2EfEE z0VQ#jOlLZQs5O9^Nq>hy-wdd!YtnzB;W)qiZdl;t9t3bL<;cAVjw84afR!glq44qm z1mXiI$M5NKxUX`di7j)Zi zagqayg#y8u55x#I!AjlQ3ZGR?5}`+q^LDzNlFR9Kcx4r<%NjV`8sU^ByO2N+trg(O z?j{d7)T)-X;rv04_jW;gM7r&mI?1Y_kk~yQvD=Py@+9z+rvP}>0MN@}KPUU_VJUXW zV;7xm61faC+tBG3q0+})Pc6AnGn`sInp)krZY)0iT73RUeEx9!tkL*ceJjS23|% zB#a~p!%2Bfqe*#VhRidwAKrTUwo|taF29gFY$zKwlwD{ZHB?+rP8-;I%}_dGC>=J~ zMh&*%w3$cMhSjo#h4DeEP;91?jaDOol-Zs0=NS_9bQ++_xt~{ zE}38b0`yo_1oI7&4nS75xjbG6i)Qj|L@LR55cv`!HKF`fRBcK#)^#XYos@4oyjd>^}(XT<1WC4)lAWqgn;s}|Y91HdJjt&p$hM1CC zi6qB3+#=nzNzhTGRqg4PAb-1O(}TuzX9^YynlG}uarJq%{@uWSS~Jv<4+)%M#~D% z0Wu#d7nQOaRF=BDzKehvi;I>SN_^C_BP;G>XrIP4@(6vq&E&tl zV{44!b&hK6<)+^uQw;!X4EYf2Os8_X&TiR)6Es0l0h)6B#xA$y>~O3g#6zMgK>)EB zIf^V1ZDb9B={)2Ry%RNh*^bE&$9;|Wzi;0X6Y}pyc88hZU#8gtl=t5Ndf*C$5y$)} zc?7F=svmQabP&O0N0E=9Z=wRwluhADCl2~zTY~yi==AgcH`^}qT1~4c5{K98CypI_ z_3MD?iEnX80-FuDBoLeo!Rg9A{{RKHA zp7Uy;8H&|lm>TVHMGHoRTe9Eol=j)#B`W}gui)G+1OWSAtYKj91=Ub2{m-^c|F_!x zd@)jx=s>9FMMYGH$aX;=bY`eO2kcLH`i!$s(a+J!OX>6@=kokooDk&k&_R&`;ML_;LZRRq_V_5R)vZRfuDe z9Rt1K7Km%LD^IO~{%brfcP$ETgoFP5E|IsTg!$zdm%|+vG6=o02JY-_`(SdTg8tf3 zsE!9gzu;++yBp5u$K6jge-ns-5w{(-+JalNLXHiue{WD91rUU{T7#V>50f`*FG&K~ zzvbzdJ-2E4QQQX{{j2Aji)AI=3&|VMB`YLPKq2g4Ary$)C3xWS3_DVttPj=fn7AB< zqX8ANDp1C|orZjt{ux8V!u8VuYoyJ=PEjw(fN?+Ksp()R&PDS%AJvj$J29N$5NE-O z?h6J69-{1EXZy>=i4pn>L`Ak1at50iu+t+nhwgsPKtCfowFLzKiT5XX|H|V&qw6YK z$OcSCW)_J!yX@$Wj!MkHbwjD@I1R}Lg3ow*`MBACx$7e)`qA6-GI6P-avc9j4^&Qb z?U2y!syTu=6{J*Yv}r?P!o*~@mxuQsV>s`r6S;GWhZH9lv~U-B@a!Ne_5=V3cid)K zHp6Bg@<&B^<>YCnhQo{2#X%5!(g3u5+)xS${o5&?S$)k?Hex9owv>-r%DB6P`w7$a8ttHTw%j;3{6 zAyVtRO)cTcEI|4^peOkVu3+;lfE!F^A`;zTGUo{&M@V3GK$nXC&MMcpmBC}BKLIg9o z=5M|Qx)e~Elpq26a*d@7tG1gkq#{Fkkl}ZI`L!%i(V4R9V3&h*&*W250rb;DRiJ#I zpd)hunTVcypLy;?f9Z^#^Iz|Oir1sRV51369DU%fRvTL%m^uXy6Epy}>tfXK3Fw^? z4whp!K9jCFR^#gjWKWXYBZS_`guR|Fw+N>Vv&e!1BjEw}9j8ljkZ71d7)1~8$eNcm zAz{HKKQ$ju3!kF!WJfCVUJ3H z-MLOy18kS}^~69*B`fiP4XU@-$*S84T(zKnxK7qM4m;Yqprsa^Zl_cy>#%UPx|}@@ zk+3;l07Fmq4RrKaK|@-7Z=oVjL3iH!BPVlGGCC>r5*hMRXh$@oauF~6L+@zAQl`6y zo;+?=tp*jc=|f!>{~O1D7IDerCr&l7NnQeIS=-@;9Leo$BMs0bOR-ONsMXcwXzbAl ztWrk=MBT*9Qu^TiCQ47HsVgz?yma@yIrOI|x9;l)gL+cIUVLR0*wuBi#rS3I3s|gT zB4#l6zWcLO8$io4G?%_{|F`*D{Cy9^MT{PJ&^2ce#*)>Yt#C?70j&_M+}t=?HegfK zE9j1atuAH@26*Ugc(NgeFJ_1)BxJfNsAL+QJ&>j$o57%k3hMicnQngQg(((2ejxr)g-9x!${f%am4pEz8je`E2 z+F^sYKb#)SBQw6Bb)iS`8(24C*-G{>=`JvK6-3V`n39AON#ziI`FPx^f1AnpcfZ_H zKZttm0M4kL!~jiMc>q$D0Ke--b^=XLG2MM8jox)SO|eNqA343Fy&t5pEiCkaKY4~t zIMzSEIAPCVUje53fbKYxnJ9t=&0sFM4Ju@6ux8N9k2n`H>(HEFa=!;_8xgc1xY?ql zO=m0Bx8YiJ&__?_((`9m!*q_HH8-=}IP;5{636b^?CZr0*6jRBmUG)@@drUV*$=qz!13rh){6s^1K7kog}9(LS?vFORZt&cSr3` zz(L;xz}~8s0rO_yZID>9N4KphDhlfLZREa5M*K7#deY4M=!Z|{>F_}n44-oti<;4UKLp%q`Fm{9vpd9RA=OjaoU=zIq_?eDpC2s4}LjoA& z$g>9Bc0NAt4p7X8iv619B%eJR?>}|^laE}wK zKeDsY;B`<z`8t2aCF2Bx4_WRE+e6?Fyrvp*{c^7Q3~$8&RhPtKrbTrlg4 zE;i=zU8praGS1}oo5gt4_tV}bGU3*jsUgR~in9uT`SXWjSn6lh(ag7Ue9TX5Y!=zM zI5s|qgx?guhaK+%kTv@pF1Yr3+3N}{Uwl}BH?=n6?38+Ra2*zcb-`*RO5{2TBjN*- z*W)_mkTp)P9dcR+dp8Ig|9$w5knxt_x{(ef55+_ui_kk0*O{yfj%e>}mG(uSX5@rW zce~58r`6@1G|_bU#dP&UpxHMR{*Pbm;T5Mj`p|=^H0P9x-gV_d4StT0mH0IQOgx1a zsQ1B5-{qLNj^sEyQrO`U&`llzqBj(@?K_WZWhM5s@%ZhAnqS(|@Bm2LL>@%FLvIx* zpFE5mJUI)T6Q7ZFp@#w{_8cJJQqbpLs(^RkU%oV7HIK1ECd~aZNy%H@F$# z>Lx4c^RLeJ;Tu~h3YZ~1di+lAg!ezD5nM*mF$^VZMEIBhU*pLsdW<2tJj50& ze6C9guXgr%KRA`=LEx~$|LSY+$Hd|36N8Ty@D~(6*hY7~kl;HKm|HuM%xKecDTjp6D46%J4wQ22)}X!ARt*Pz*_J}c?l;l^plFi zHK5%3qr*vJ%DMQj@kf*j4nAKg_UBOL8zsJZKt$#un2(?a!2$#e5#YNmX+W?P!7>EP z5v)M48o@0H)*x7mU>yKRAJ${N5y1uoO$at3XhE|+FfMDQts&k^ttFa%#n3BIVY_b-C)G6XM{1g~4{ibC+( zO)!5akkvME1hYZ*3_&n`BAA*HOic);0qk5PcUOj0sQL7qH%n;~z!v z9eU!&W&=ittn6rYy2)k0t08v1JLq?RoafVLoGBRA=Zxxe`c{rBvs2aZhZ|Q-CF3zr zeNSt;ngM@A0M&6d7n^z2Ts5x2hBnlQ#YSAHp~FTz7dz`}VcobM8wp&j5oIM}BZ-U6 zy_&aVJQ*7)Tx{Oe{H5cmK5V6hS_W(wLk$x)(z)1-tC^MK8Q93=V$)F{Gd3)tQM0g- z&BdCork9Q9U_%Hsa)u5J(!;%@x`M$i7d8#+sz!BHL&_?aOUPQg9eruH!yZ?JUdG6VLy>v>F6>~v zI;{N-QR)dOwoRUl5bfq*a>8=Hg2W-2p#zQOz|SMO9|86TrB6KY*hnuSDmwaZXoe3& zhM%Gx!^gvx3FDI~1T48@LosbxjqUr<;TgMvu*ZF-O6c`9RA&y*L#%;YSpJcWaXGOU z&rfE~-eC7>)K|&&ZG!bl#4M^WMntf|H^E5@N0rn?9CkZ_kmd+%{2cqE0sQPQ?Zc}* z%Qk)t{q(cS3~G4meqSFFoj`CF!DR&Oz4i*$*mC|2)~2lK0QOB;*z7*S(lq=|z#i?V zF7~;IU`sr->^DGrGqW85USxwSo~?7X;%~m#*~y8=WN_JErX}xW7GPo@%yV(62eh>v z9?EJDdXHUxLd2`VUww4 uFanH;yndXU0Tk$vdNQ#1Kl=Fq?>{xx&u0|ybH=$DKqD=aOO^dF;r|P0{1jOL diff --git a/tools/pyghidra_crusader/__pycache__/common.cpython-311.pyc b/tools/pyghidra_crusader/__pycache__/common.cpython-311.pyc index 610f1966b85bb4128732d7917ec36ad51e9f179a..a92f1210b44bdec901f3560b0904b278bab300af 100644 GIT binary patch delta 21 bcmccdmGQ<`My}<&yj%=GP!hF~>s%E8S!M@A delta 21 bcmccdmGQ<`My}<&yj%=G@F{X5*SRVHTvZ3a diff --git a/tools/pyghidra_crusader/cli.py b/tools/pyghidra_crusader/cli.py index ed938fa..dbbeeae 100644 --- a/tools/pyghidra_crusader/cli.py +++ b/tools/pyghidra_crusader/cli.py @@ -15,6 +15,7 @@ from .common import ( decompile_function, disassemble_function, format_function_summary, + function_signature, get_function, get_function_containing, get_functions_by_exact_name, @@ -41,6 +42,181 @@ from .common import ( ) +OUTPUT_SCHEMA_VERSION = "1.0" + +FUNCTION_SCHEMA = { + "type": "object", + "required": ["name", "signature", "entry", "body_start", "body_end"], + "properties": { + "name": {"type": "string"}, + "signature": {"type": "string"}, + "entry": {"type": "string"}, + "body_start": {"type": "string"}, + "body_end": {"type": "string"}, + }, +} + +REFERENCE_SCHEMA = { + "type": "object", + "required": ["from", "to", "type", "operand_index"], + "properties": { + "from": {"type": "string"}, + "to": {"type": "string"}, + "type": {"type": "string"}, + "operand_index": {"type": "integer"}, + }, +} + +STATUS_SCHEMA = { + "type": "object", + "required": ["status", "action"], + "properties": { + "status": {"type": "string"}, + "action": {"type": "string"}, + "entry": {"type": "string"}, + "name": {"type": "string"}, + "address": {"type": "string"}, + "type": {"type": "string"}, + "text": {"type": "string"}, + "script": {"type": "string"}, + "plan": {"type": "string"}, + }, +} + +STRING_SCHEMA = { + "type": "object", + "required": ["address", "length", "text"], + "properties": { + "address": {"type": "string"}, + "length": {"type": "integer"}, + "text": {"type": "string"}, + }, +} + +SEGMENT_SCHEMA = { + "type": "object", + "required": ["name", "start", "end", "length", "initialized", "read", "write", "execute"], + "properties": { + "name": {"type": "string"}, + "start": {"type": "string"}, + "end": {"type": "string"}, + "length": {"type": "integer"}, + "initialized": {"type": "boolean"}, + "read": {"type": "boolean"}, + "write": {"type": "boolean"}, + "execute": {"type": "boolean"}, + }, +} + +DATA_ITEM_SCHEMA = { + "type": "object", + "required": ["address", "length", "mnemonic", "value"], + "properties": { + "address": {"type": "string"}, + "length": {"type": "integer"}, + "mnemonic": {"type": "string"}, + "value": {"type": ["string", "null"]}, + }, +} + +IMPORT_SCHEMA = { + "type": "object", + "required": ["library", "label", "address"], + "properties": { + "library": {"type": "string"}, + "label": {"type": ["string", "null"]}, + "address": {"type": ["string", "null"]}, + }, +} + +EXPORT_SCHEMA = { + "type": "object", + "required": ["address", "name", "kind"], + "properties": { + "address": {"type": "string"}, + "name": {"type": ["string", "null"]}, + "kind": {"type": "string"}, + }, +} + +NAMESPACE_SCHEMA = { + "type": "object", + "required": ["name", "type", "parent"], + "properties": { + "name": {"type": "string"}, + "type": {"type": "string"}, + "parent": {"type": ["string", "null"]}, + }, +} + +CLASS_SCHEMA = { + "type": "object", + "required": ["name", "parent"], + "properties": { + "name": {"type": "string"}, + "parent": {"type": ["string", "null"]}, + }, +} + +JSON_SCHEMAS = { + "project-files": {"type": "array", "items": {"type": "string"}}, + "dump-region": { + "type": "object", + "required": ["start", "end", "preview_bytes", "lines"], + "properties": { + "start": {"type": "string"}, + "end": {"type": "string"}, + "preview_bytes": {"type": "string"}, + "lines": {"type": "array", "items": {"type": "string"}}, + }, + }, + "create-function": STATUS_SCHEMA, + "delete-function": STATUS_SCHEMA, + "rename-function": STATUS_SCHEMA, + "rename-function-by-address": STATUS_SCHEMA, + "set-comment": STATUS_SCHEMA, + "set-decompiler-comment": STATUS_SCHEMA, + "set-disassembly-comment": STATUS_SCHEMA, + "get-function-by-address": FUNCTION_SCHEMA, + "get-function-containing": FUNCTION_SCHEMA, + "list-functions": {"type": "array", "items": FUNCTION_SCHEMA}, + "list-methods": {"type": "array", "items": FUNCTION_SCHEMA}, + "search-functions-by-name": {"type": "array", "items": FUNCTION_SCHEMA}, + "decompile-function": { + "type": "object", + "required": ["name", "decompiled"], + "properties": {"name": {"type": "string"}, "decompiled": {"type": "string"}}, + }, + "decompile-function-by-address": { + "type": "object", + "required": ["address", "decompiled"], + "properties": {"address": {"type": "string"}, "decompiled": {"type": "string"}}, + }, + "disassemble-function": { + "type": "object", + "required": ["address", "lines"], + "properties": {"address": {"type": "string"}, "lines": {"type": "array", "items": {"type": "string"}}}, + }, + "read-region": { + "type": "object", + "required": ["start", "end", "bytes"], + "properties": {"start": {"type": "string"}, "end": {"type": "string"}, "bytes": {"type": "string"}}, + }, + "get-xrefs-to": {"type": "array", "items": REFERENCE_SCHEMA}, + "get-xrefs-from": {"type": "array", "items": REFERENCE_SCHEMA}, + "get-function-xrefs": {"type": "array", "items": REFERENCE_SCHEMA}, + "list-strings": {"type": "array", "items": STRING_SCHEMA}, + "list-imports": {"type": "array", "items": IMPORT_SCHEMA}, + "list-exports": {"type": "array", "items": EXPORT_SCHEMA}, + "list-namespaces": {"type": "array", "items": NAMESPACE_SCHEMA}, + "list-segments": {"type": "array", "items": SEGMENT_SCHEMA}, + "list-data-items": {"type": "array", "items": DATA_ITEM_SCHEMA}, + "list-classes": {"type": "array", "items": CLASS_SCHEMA}, + "run-script": STATUS_SCHEMA, + "apply-plan": STATUS_SCHEMA, +} + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="PyGhidra helpers for the Crusader project." @@ -86,11 +262,13 @@ def build_parser() -> argparse.ArgumentParser: subparsers.add_parser( "project-files", + aliases=["project_files"], help="List root-level files in the Ghidra project.", ) dump_parser = subparsers.add_parser( "dump-region", + aliases=["dump_region"], help="Dump instructions and resolved call targets for an address range.", ) dump_parser.add_argument("--start", required=True, help="Start address.") @@ -98,6 +276,7 @@ def build_parser() -> argparse.ArgumentParser: create_parser = subparsers.add_parser( "create-function", + aliases=["create_function"], help="Create a function at an address with an optional explicit body range.", ) create_parser.add_argument("--entry", required=True, help="Function entry address.") @@ -111,12 +290,14 @@ def build_parser() -> argparse.ArgumentParser: delete_parser = subparsers.add_parser( "delete-function", + aliases=["delete_function"], help="Delete a function at an address.", ) delete_parser.add_argument("--entry", required=True, help="Function entry address.") rename_parser = subparsers.add_parser( "rename-function", + aliases=["rename_function"], help="Rename an existing function by entry address.", ) rename_parser.add_argument("--entry", required=True, help="Function entry address.") @@ -124,15 +305,17 @@ def build_parser() -> argparse.ArgumentParser: rename_by_address_parser = subparsers.add_parser( "rename-function-by-address", + aliases=["rename_function_by_address"], help="Rename an existing function by entry address (MCP-style alias).", ) rename_by_address_parser.add_argument( - "--entry", required=True, help="Function entry address." + "--entry", "--function-address", dest="entry", required=True, help="Function entry address." ) - rename_by_address_parser.add_argument("--name", required=True, help="New function name.") + rename_by_address_parser.add_argument("--name", "--new-name", dest="name", required=True, help="New function name.") comment_parser = subparsers.add_parser( "set-comment", + aliases=["set_comment"], help="Set a code-unit comment by address.", ) comment_parser.add_argument("--address", required=True, help="Comment target address.") @@ -146,26 +329,30 @@ def build_parser() -> argparse.ArgumentParser: decompiler_comment_parser = subparsers.add_parser( "set-decompiler-comment", + aliases=["set_decompiler_comment"], help="Set a decompiler-visible pre-comment by address.", ) decompiler_comment_parser.add_argument("--address", required=True, help="Comment target address.") - decompiler_comment_parser.add_argument("--text", required=True, help="Comment text.") + decompiler_comment_parser.add_argument("--text", "--comment", dest="text", required=True, help="Comment text.") disassembly_comment_parser = subparsers.add_parser( "set-disassembly-comment", + aliases=["set_disassembly_comment"], help="Set a disassembly EOL comment by address.", ) disassembly_comment_parser.add_argument("--address", required=True, help="Comment target address.") - disassembly_comment_parser.add_argument("--text", required=True, help="Comment text.") + disassembly_comment_parser.add_argument("--text", "--comment", dest="text", required=True, help="Comment text.") get_function_parser = subparsers.add_parser( "get-function-by-address", + aliases=["get_function_by_address"], help="Show function metadata for an exact entry address.", ) get_function_parser.add_argument("--address", required=True, help="Function entry address.") get_function_containing_parser = subparsers.add_parser( "get-function-containing", + aliases=["get_function_containing"], help="Show function metadata for the function containing an address.", ) get_function_containing_parser.add_argument( @@ -174,13 +361,23 @@ def build_parser() -> argparse.ArgumentParser: list_functions_parser = subparsers.add_parser( "list-functions", + aliases=["list_functions"], help="List all defined functions.", ) list_functions_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") list_functions_parser.add_argument("--limit", type=int, default=100, help="Maximum functions to print.") + list_methods_parser = subparsers.add_parser( + "list-methods", + aliases=["list_methods"], + help="List defined function names and entries with signatures/body ranges in JSON mode.", + ) + list_methods_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") + list_methods_parser.add_argument("--limit", type=int, default=100, help="Maximum methods to print.") + list_segments_parser = subparsers.add_parser( "list-segments", + aliases=["list_segments"], help="List memory segments or blocks.", ) list_segments_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") @@ -188,6 +385,7 @@ def build_parser() -> argparse.ArgumentParser: list_data_items_parser = subparsers.add_parser( "list-data-items", + aliases=["list_data_items"], help="List defined data items.", ) list_data_items_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") @@ -195,6 +393,7 @@ def build_parser() -> argparse.ArgumentParser: list_classes_parser = subparsers.add_parser( "list-classes", + aliases=["list_classes"], help="List class namespaces.", ) list_classes_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") @@ -202,14 +401,16 @@ def build_parser() -> argparse.ArgumentParser: list_strings_parser = subparsers.add_parser( "list-strings", + aliases=["list_strings"], help="List defined strings in the program.", ) list_strings_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") list_strings_parser.add_argument("--limit", type=int, default=2000, help="Maximum strings to print.") - list_strings_parser.add_argument("--filter", help="Optional substring filter.") + list_strings_parser.add_argument("--filter", "--filter-text", dest="filter", help="Optional substring filter.") list_imports_parser = subparsers.add_parser( "list-imports", + aliases=["list_imports"], help="List imported external symbols.", ) list_imports_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") @@ -217,6 +418,7 @@ def build_parser() -> argparse.ArgumentParser: list_exports_parser = subparsers.add_parser( "list-exports", + aliases=["list_exports"], help="List exported entry points and symbols.", ) list_exports_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") @@ -224,6 +426,7 @@ def build_parser() -> argparse.ArgumentParser: list_namespaces_parser = subparsers.add_parser( "list-namespaces", + aliases=["list_namespaces"], help="List non-global namespaces, classes, and libraries.", ) list_namespaces_parser.add_argument("--offset", type=int, default=0, help="Pagination offset.") @@ -231,6 +434,7 @@ def build_parser() -> argparse.ArgumentParser: search_functions_parser = subparsers.add_parser( "search-functions-by-name", + aliases=["search_functions_by_name"], help="List functions whose names contain a substring.", ) search_functions_parser.add_argument("--query", required=True, help="Substring to match.") @@ -239,6 +443,7 @@ def build_parser() -> argparse.ArgumentParser: decompile_name_parser = subparsers.add_parser( "decompile-function", + aliases=["decompile_function"], help="Decompile an exact-named function.", ) decompile_name_parser.add_argument("--name", required=True, help="Exact function name.") @@ -246,6 +451,7 @@ def build_parser() -> argparse.ArgumentParser: decompile_address_parser = subparsers.add_parser( "decompile-function-by-address", + aliases=["decompile_function_by_address"], help="Decompile a function by entry address.", ) decompile_address_parser.add_argument("--address", required=True, help="Function entry address.") @@ -253,12 +459,14 @@ def build_parser() -> argparse.ArgumentParser: disassemble_parser = subparsers.add_parser( "disassemble-function", + aliases=["disassemble_function"], help="Disassemble a function body by entry address.", ) disassemble_parser.add_argument("--address", required=True, help="Function entry address.") read_region_parser = subparsers.add_parser( "read-region", + aliases=["read_region"], help="Dump raw bytes for an inclusive address range.", ) read_region_parser.add_argument("--start", required=True, help="Start address.") @@ -266,6 +474,7 @@ def build_parser() -> argparse.ArgumentParser: run_script_parser = subparsers.add_parser( "run-script", + aliases=["run_script"], help="Execute a Python file with project/program context to avoid interactive shell quoting issues.", ) run_script_parser.add_argument("--script", required=True, help="Path to the Python script file.") @@ -277,6 +486,7 @@ def build_parser() -> argparse.ArgumentParser: xrefs_to_parser = subparsers.add_parser( "get-xrefs-to", + aliases=["get_xrefs_to"], help="List references to an address.", ) xrefs_to_parser.add_argument("--address", required=True, help="Target address.") @@ -285,6 +495,7 @@ def build_parser() -> argparse.ArgumentParser: xrefs_from_parser = subparsers.add_parser( "get-xrefs-from", + aliases=["get_xrefs_from"], help="List references from an address.", ) xrefs_from_parser.add_argument("--address", required=True, help="Source address.") @@ -293,6 +504,7 @@ def build_parser() -> argparse.ArgumentParser: function_xrefs_parser = subparsers.add_parser( "get-function-xrefs", + aliases=["get_function_xrefs"], help="List references to a function entry by exact function name.", ) function_xrefs_parser.add_argument("--name", required=True, help="Exact function name.") @@ -301,6 +513,7 @@ def build_parser() -> argparse.ArgumentParser: plan_parser = subparsers.add_parser( "apply-plan", + aliases=["apply_plan"], help="Apply a JSON edit plan containing function and comment operations.", ) plan_parser.add_argument("--plan", required=True, help="Path to the JSON plan file.") @@ -324,9 +537,21 @@ def build_config(args: argparse.Namespace) -> ProjectConfig: ) +def _canonical_command_name(command_name: str) -> str: + return command_name.replace("_", "-") + + def _emit(args: argparse.Namespace, payload, text: str | None = None) -> int: if args.format == "json": - print(json.dumps(payload, indent=2, sort_keys=True)) + command_name = _canonical_command_name(args.command) + response = { + "schema_version": OUTPUT_SCHEMA_VERSION, + "command": command_name, + "ok": True, + "schema": JSON_SCHEMAS.get(command_name, {"type": "object"}), + "data": payload, + } + print(json.dumps(response, indent=2, sort_keys=True)) return 0 if text is not None: print(text) @@ -343,16 +568,12 @@ def _emit(args: argparse.Namespace, payload, text: str | None = None) -> int: def _function_to_dict(function) -> dict[str, str]: - summary_text = format_function_summary(function) - lines = summary_text.splitlines() - body_line = lines[3].split(": ", 1)[1] - body_start, body_end = body_line.split(" - ", 1) return { "name": function.getName(), - "signature": lines[1].split(": ", 1)[1], + "signature": function_signature(function), "entry": str(function.getEntryPoint()), - "body_start": body_start, - "body_end": body_end, + "body_start": str(function.getBody().getMinAddress()), + "body_end": str(function.getBody().getMaxAddress()), } @@ -519,15 +740,23 @@ def command_get_function_containing(config: ProjectConfig, args: argparse.Namesp def command_list_functions(config: ProjectConfig, args: argparse.Namespace) -> int: with open_program(config, read_only=True) as (_project, program): functions = search_functions_by_name(program, "", offset=args.offset, limit=args.limit) - payload = [{"name": function.getName(), "entry": str(function.getEntryPoint())} for function in functions] + payload = [_function_to_dict(function) for function in functions] text = _text_or_empty([_function_line(function) for function in functions], "no functions found") return _emit(args, payload, text) +def command_list_methods(config: ProjectConfig, args: argparse.Namespace) -> int: + with open_program(config, read_only=True) as (_project, program): + functions = search_functions_by_name(program, "", offset=args.offset, limit=args.limit) + payload = [_function_to_dict(function) for function in functions] + text = _text_or_empty([_function_line(function) for function in functions], "no methods found") + return _emit(args, payload, text) + + def command_search_functions_by_name(config: ProjectConfig, args: argparse.Namespace) -> int: with open_program(config, read_only=True) as (_project, program): functions = search_functions_by_name(program, args.query, offset=args.offset, limit=args.limit) - payload = [{"name": function.getName(), "entry": str(function.getEntryPoint())} for function in functions] + payload = [_function_to_dict(function) for function in functions] text = _text_or_empty([_function_line(function) for function in functions], "no matching functions found") return _emit(args, payload, text) @@ -774,6 +1003,7 @@ def command_apply_plan(config: ProjectConfig, args: argparse.Namespace) -> int: def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) + args.command = _canonical_command_name(args.command) config = build_config(args) command_map = { @@ -789,6 +1019,7 @@ def main(argv: list[str] | None = None) -> int: "get-function-by-address": command_get_function_by_address, "get-function-containing": command_get_function_containing, "list-functions": command_list_functions, + "list-methods": command_list_methods, "list-segments": command_list_segments, "list-data-items": command_list_data_items, "list-classes": command_list_classes,