aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorblasty <blasty@local>2026-07-23 01:53:36 +0200
committerblasty <blasty@local>2026-07-23 01:53:36 +0200
commit0aba8653f4b585165d06714be32a0560ec5d5341 (patch)
tree6f66c42fb8f0067dd1d94880ade26932293aa342
parentperf: derive segment map from file_regions, not survey_binary (hex open ~3400x) (diff)
downloadida-tui-0aba8653f4b585165d06714be32a0560ec5d5341.tar.gz
ida-tui-0aba8653f4b585165d06714be32a0560ec5d5341.tar.xz
ida-tui-0aba8653f4b585165d06714be32a0560ec5d5341.zip
app: 'a' — make string literal in the listing (M4 richness)
IDA's 'A' key. New make_string server tool (ida_bytes.create_strlit, auto-length to the terminator, undefines items in the way first, returns size + decoded contents). Program.make_string wraps it; 'a' on a listing head routes through the existing edit plumbing (EditItemRequested kind=string -> _do_edit_item -> make_string -> bump_items -> reopen). Verified: make_string at a .rodata addr creates the literal and IDA auto-names it (aUsage for 'usage'). New listing_make_string scenario drives it through the UI; listing suite 15/0. Needs a supervisor restart.
-rw-r--r--idatui/app.py9
-rw-r--r--idatui/domain.py10
-rw-r--r--server/patch_server.py35
-rw-r--r--tests/test_scenarios.py49
4 files changed, 102 insertions, 1 deletions
diff --git a/idatui/app.py b/idatui/app.py
index 570ed0a..cc9ab03 100644
--- a/idatui/app.py
+++ b/idatui/app.py
@@ -900,6 +900,7 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
Binding("G,end", "goto_bottom", "Bottom", show=False),
Binding("c", "define_code", "Code", show=False),
Binding("d", "make_data", "Data", show=False),
+ Binding("a", "make_string", "Str", show=False),
Binding("p", "define_func", "Func", show=False),
Binding("u", "undefine", "Undef", show=False),
*SearchMixin.SEARCH_BINDINGS,
@@ -1071,6 +1072,9 @@ class ListingView(SearchMixin, NavMixin, ColumnCursor, ScrollView, can_focus=Tru
def action_make_data(self) -> None:
self.post_message(MakeDataRequested(self))
+ def action_make_string(self) -> None:
+ self.post_message(EditItemRequested(self, "string"))
+
def cur_head(self) -> Head | None:
return self._head(self.cursor)
@@ -3079,12 +3083,15 @@ class IdaTui(App):
def _do_edit_item(self, kind: str, ea: int) -> None: # worker context
assert self.program is not None
verb = {"code": "defined code", "func": "created function",
- "undef": "undefined"}[kind]
+ "undef": "undefined", "string": "made string"}[kind]
try:
if kind == "code":
self.program.define_code(ea)
elif kind == "func":
self.program.define_func(ea)
+ elif kind == "string":
+ s = self.program.make_string(ea)
+ verb = f"made string ({s[:24]!r})" if s else verb
else:
self.program.undefine(ea)
except Exception as e: # noqa: BLE001 -- surface soft/hard tool errors
diff --git a/idatui/domain.py b/idatui/domain.py
index 58e4be9..f26354c 100644
--- a/idatui/domain.py
+++ b/idatui/domain.py
@@ -1127,6 +1127,16 @@ class Program:
raise IDAToolError(
f"make data @ {ea:#x}: {res.get('error') or 'rejected'}")
+ def make_string(self, ea: int, length: int = 0, kind: str = "c") -> str:
+ """Create a string literal at ``ea`` (IDA's 'A'); auto-length when 0.
+ Returns the decoded contents."""
+ r = self.client.call("make_string", addr=hex(ea), length=int(length), kind=kind)
+ res = r if isinstance(r, dict) else {}
+ if not res.get("ok"):
+ raise IDAToolError(
+ f"make string @ {ea:#x}: {res.get('error') or 'rejected'}")
+ return res.get("text", "")
+
def region_label(self, ea: int) -> str:
"""Display name for a non-function address (segment-qualified)."""
try:
diff --git a/server/patch_server.py b/server/patch_server.py
index 4b2661f..e283cab 100644
--- a/server/patch_server.py
+++ b/server/patch_server.py
@@ -187,6 +187,41 @@ def file_regions() -> dict:
@tool
@idasync
+def make_string(
+ addr: Annotated[str, "Address of the string start"],
+ length: Annotated[int, "Length in bytes (0 = auto-detect to the terminator)"] = 0,
+ kind: Annotated[str, "String kind: c | c16 | c32 | pascal"] = "c",
+) -> dict:
+ """Create a string literal at ``addr`` (IDA's 'A'). ``length`` 0 auto-detects
+ to the terminator. Undefines any items in the way first, like the UI does.
+ Returns the created byte size and the decoded contents."""
+ import ida_bytes
+ import ida_nalt
+
+ ea = parse_address(addr)
+ strtype = {
+ "c": ida_nalt.STRTYPE_C,
+ "c16": ida_nalt.STRTYPE_C_16,
+ "c32": ida_nalt.STRTYPE_C_32,
+ "pascal": ida_nalt.STRTYPE_PASCAL,
+ }.get(str(kind).lower(), ida_nalt.STRTYPE_C)
+ n = max(int(length), 0)
+ # Free any existing item(s) so create_strlit can carve the literal.
+ ida_bytes.del_items(ea, ida_bytes.DELIT_SIMPLE, n if n > 0 else 1)
+ ok = bool(ida_bytes.create_strlit(ea, n, strtype))
+ if not ok:
+ return {"addr": addr, "ok": False, "error": "create_strlit failed"}
+ size = int(ida_bytes.get_item_size(ea))
+ try:
+ raw = ida_bytes.get_strlit_contents(ea, -1, strtype)
+ text = raw.decode("utf-8", "replace") if raw else ""
+ except Exception:
+ text = ""
+ return {"addr": addr, "ok": True, "size": size, "text": text}
+
+
+@tool
+@idasync
def read_raw(
addr: Annotated[str, "Start address (hex or name)"],
size: Annotated[int, "Number of bytes to read"],
diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py
index 26e916f..62d86dd 100644
--- a/tests/test_scenarios.py
+++ b/tests/test_scenarios.py
@@ -1507,6 +1507,55 @@ async def s_listing_name_addr(c: Ctx):
c.prog.bump_items()
+@scenario("listing_make_string")
+async def s_listing_make_string(c: Ctx):
+ """'a' in the listing makes a string literal at the cursor (IDA's 'A')."""
+ app = c.app
+ target = None
+ for start, end, name in c.prog.sections():
+ if start >= end:
+ continue
+ lm = c.prog.listing(start)
+ if lm is None:
+ continue
+ lm.ensure(80)
+ h = next((h for h in lm.window(0, 80)
+ if h.kind == "data" and "'" in h.text), None)
+ if h is not None:
+ target = h.ea
+ break
+ if target is None:
+ c.check("found a string data item to remake", False)
+ return
+ A = target
+ try:
+ c.prog.undefine(A, size=8)
+ c.prog.bump_items()
+ await c.goto_ui(hex(A))
+ await c.wait(lambda: app._active == "listing" and app._cur is not None
+ and app._cur.ea == A, 25)
+ c.check("target is undefined before 'a'",
+ c.lst.cur_head() is not None and c.lst.cur_head().kind == "unknown",
+ f"head={c.lst.cur_head()}")
+ c.lst.focus()
+ await c.press("a")
+ await c.wait(lambda: c.lst.model.index_of_ea(A) >= 0
+ and c.lst.model.get(c.lst.model.index_of_ea(A)) is not None
+ and c.lst.model.get(c.lst.model.index_of_ea(A)).kind == "data"
+ and "'" in c.lst.model.get(c.lst.model.index_of_ea(A)).text, 25)
+ hi = c.lst.model.index_of_ea(A)
+ c.check("'a' creates a string literal at the cursor",
+ hi >= 0 and c.lst.model.get(hi).kind == "data"
+ and "'" in c.lst.model.get(hi).text,
+ f"head={c.lst.model.get(hi) if hi >= 0 else None}")
+ finally:
+ try:
+ c.prog.make_string(A) # restore the original string
+ except Exception: # noqa: BLE001
+ pass
+ c.prog.bump_items()
+
+
# --------------------------------------------------------------------------- #
# Runner
# --------------------------------------------------------------------------- #