From fb0a28c2c9fa88f11cad4df48a8d3f9e7d739387 Mon Sep 17 00:00:00 2001 From: yotti Date: Thu, 7 May 2026 21:48:14 +0900 Subject: [PATCH 1/8] Add snapshot diff for comparing memory regions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Take two snapshots of a memory region at different points in time and see which bytes changed. Useful for reverse-engineering save data or finding dynamic values without a known starting value. - CLI: option 23d — interactive snapshot A → wait → snapshot B → diff - API: POST /api/snapshot/take, POST /api/snapshot/diff - Library: TakeSnapshot, DiffSnapshots, WriteDiff in modify package --- internal/app/state.go | 18 ++++++ internal/memory/modify/snapshot.go | 73 +++++++++++++++++++++++ internal/server/handlers.go | 95 ++++++++++++++++++++++++++++++ internal/server/server.go | 4 ++ main.go | 58 ++++++++++++++++++ 5 files changed, 248 insertions(+) create mode 100644 internal/memory/modify/snapshot.go diff --git a/internal/app/state.go b/internal/app/state.go index 9a1c2b7..0e26c80 100644 --- a/internal/app/state.go +++ b/internal/app/state.go @@ -19,6 +19,7 @@ type State struct { valueType search.ValueType session *search.Session bookmarks *store.BookmarkList + snapshots map[uintptr][]byte Freezer *modify.Freezer UndoStack *modify.UndoStack @@ -30,6 +31,7 @@ func NewState(drv driver.Driver) *State { drv: drv, valueType: search.TypeInt32, bookmarks: store.NewBookmarkList(), + snapshots: make(map[uintptr][]byte), Freezer: modify.NewFreezer(), UndoStack: modify.NewUndoStack(), Watcher: watch.NewWatcher(), @@ -103,6 +105,22 @@ func (s *State) GetBookmarks() *store.BookmarkList { return s.bookmarks } +// --- Snapshots --- + +func (s *State) SetSnapshot(addr uintptr, data []byte) { + s.mu.Lock() + defer s.mu.Unlock() + cp := make([]byte, len(data)) + copy(cp, data) + s.snapshots[addr] = cp +} + +func (s *State) GetSnapshot(addr uintptr) []byte { + s.mu.RLock() + defer s.mu.RUnlock() + return s.snapshots[addr] +} + // WithLock runs f while holding the write lock. Use for multi-step mutations. func (s *State) WithLock(f func()) { s.mu.Lock() diff --git a/internal/memory/modify/snapshot.go b/internal/memory/modify/snapshot.go new file mode 100644 index 0000000..b87f3f0 --- /dev/null +++ b/internal/memory/modify/snapshot.go @@ -0,0 +1,73 @@ +package modify + +import ( + "fmt" + "os" + + "memodroid/internal/driver" +) + +// Snapshot holds a captured memory region for comparison. +type Snapshot struct { + Addr uintptr + Data []byte +} + +// TakeSnapshot reads size bytes from addr and returns a Snapshot. +func TakeSnapshot(drv driver.Driver, pid int, addr uintptr, size int) (*Snapshot, error) { + data, err := drv.ReadRegion(pid, addr, size) + if err != nil { + return nil, fmt.Errorf("snapshot 0x%x: %w", addr, err) + } + return &Snapshot{Addr: addr, Data: data}, nil +} + +// DiffEntry represents a single byte difference between two snapshots. +type DiffEntry struct { + Offset int + Addr uintptr + Before byte + After byte +} + +// DiffSnapshots compares two snapshots and returns all differing bytes. +// Both snapshots must have the same base address and length. +func DiffSnapshots(a, b *Snapshot) ([]DiffEntry, error) { + if a.Addr != b.Addr { + return nil, fmt.Errorf("diff: base address mismatch (0x%x vs 0x%x)", a.Addr, b.Addr) + } + minLen := len(a.Data) + if len(b.Data) < minLen { + minLen = len(b.Data) + } + + var diffs []DiffEntry + for i := 0; i < minLen; i++ { + if a.Data[i] != b.Data[i] { + diffs = append(diffs, DiffEntry{ + Offset: i, + Addr: a.Addr + uintptr(i), + Before: a.Data[i], + After: b.Data[i], + }) + } + } + return diffs, nil +} + +// WriteDiff writes a human-readable diff report to the given path. +func WriteDiff(diffs []DiffEntry, baseAddr uintptr, path string) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + + _, _ = fmt.Fprintf(f, "Snapshot diff: %d bytes changed (base: 0x%x)\n", len(diffs), baseAddr) + _, _ = fmt.Fprintf(f, "%-18s %-8s %-6s %-6s\n", "Address", "Offset", "Before", "After") + _, _ = fmt.Fprintf(f, "%-18s %-8s %-6s %-6s\n", "──────────────────", "────────", "──────", "──────") + for _, d := range diffs { + _, _ = fmt.Fprintf(f, "0x%016x +0x%-5x 0x%02x 0x%02x\n", d.Addr, d.Offset, d.Before, d.After) + } + return nil +} diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 0fe6492..f17a515 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -561,6 +561,101 @@ func (h *handler) memoryFrozen(w http.ResponseWriter, _ *http.Request) { writeJSON(w, out) } +// --- snapshot --- + +func (h *handler) snapshotTake(w http.ResponseWriter, r *http.Request) { + pid, ok := requirePID(w, h) + if !ok { + return + } + var req struct { + Addr string `json:"addr"` + Size int `json:"size"` + } + if err := decode(r, &req); err != nil { + writeError(w, 400, err.Error()) + return + } + addr, err := parseHexAddr(req.Addr) + if err != nil { + writeError(w, 400, "invalid addr") + return + } + if req.Size <= 0 { + writeError(w, 400, "size must be positive") + return + } + drv := h.state.GetDriver() + data, readErr := drv.ReadRegion(pid, addr, req.Size) + if readErr != nil { + writeError(w, 500, readErr.Error()) + return + } + // Store snapshot in state for later diff + h.state.SetSnapshot(addr, data) + writeJSON(w, map[string]any{"ok": true, "addr": fmt.Sprintf("0x%x", addr), "size": len(data)}) +} + +func (h *handler) snapshotDiff(w http.ResponseWriter, r *http.Request) { + pid, ok := requirePID(w, h) + if !ok { + return + } + _ = pid + var req struct { + Addr string `json:"addr"` + Size int `json:"size"` + } + if err := decode(r, &req); err != nil { + writeError(w, 400, err.Error()) + return + } + addr, err := parseHexAddr(req.Addr) + if err != nil { + writeError(w, 400, "invalid addr") + return + } + if req.Size <= 0 { + writeError(w, 400, "size must be positive") + return + } + prev := h.state.GetSnapshot(addr) + if prev == nil { + writeError(w, 400, "no snapshot taken for this address — call /api/snapshot/take first") + return + } + drv := h.state.GetDriver() + cur, readErr := drv.ReadRegion(h.state.GetPID(), addr, req.Size) + if readErr != nil { + writeError(w, 500, readErr.Error()) + return + } + minLen := len(prev) + if len(cur) < minLen { + minLen = len(cur) + } + type diffEntry struct { + Addr string `json:"addr"` + Offset int `json:"offset"` + Before int `json:"before"` + After int `json:"after"` + } + var diffs []diffEntry + for i := 0; i < minLen; i++ { + if prev[i] != cur[i] { + diffs = append(diffs, diffEntry{ + Addr: fmt.Sprintf("0x%x", addr+uintptr(i)), + Offset: i, + Before: int(prev[i]), + After: int(cur[i]), + }) + } + } + // Update stored snapshot to current + h.state.SetSnapshot(addr, cur) + writeJSON(w, map[string]any{"total": len(diffs), "diffs": diffs}) +} + // --- bookmarks --- func (h *handler) bookmarkList(w http.ResponseWriter, _ *http.Request) { diff --git a/internal/server/server.go b/internal/server/server.go index a35a594..24ef799 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -80,6 +80,10 @@ func Start(addr string, state *app.State, d *adb.ADB) error { mux.HandleFunc("/api/memory/unfreeze", h.memoryUnfreeze) mux.HandleFunc("/api/memory/frozen", h.memoryFrozen) + // Snapshot + mux.HandleFunc("/api/snapshot/take", h.snapshotTake) + mux.HandleFunc("/api/snapshot/diff", h.snapshotDiff) + // Bookmarks mux.HandleFunc("/api/bookmark/list", h.bookmarkList) mux.HandleFunc("/api/bookmark/add", h.bookmarkAdd) diff --git a/main.go b/main.go index bee0e7c..b484762 100644 --- a/main.go +++ b/main.go @@ -304,6 +304,59 @@ func handleDump(st *app.State) { } } +func handleSnapshotDiff(st *app.State) { + addr, ok := parseAddr("Start address (hex): ") + if !ok { + return + } + size, err := strconv.Atoi(prompt("Size (bytes, decimal): ")) + if err != nil || size <= 0 { + fmt.Println("Invalid size") + return + } + fmt.Println("Taking snapshot A...") + snapA, err := modify.TakeSnapshot(st.GetDriver(), st.GetPID(), addr, size) + if err != nil { + fmt.Printf("Snapshot failed: %v\n", err) + return + } + fmt.Printf("Snapshot A: %d bytes at 0x%x\n", len(snapA.Data), addr) + prompt("Make changes in the target process, then press Enter...") + fmt.Println("Taking snapshot B...") + snapB, err := modify.TakeSnapshot(st.GetDriver(), st.GetPID(), addr, size) + if err != nil { + fmt.Printf("Snapshot failed: %v\n", err) + return + } + diffs, err := modify.DiffSnapshots(snapA, snapB) + if err != nil { + fmt.Printf("Diff failed: %v\n", err) + return + } + if len(diffs) == 0 { + fmt.Println("No differences found") + return + } + fmt.Printf("Found %d changed bytes:\n", len(diffs)) + shown := 0 + for _, d := range diffs { + fmt.Printf(" 0x%x (+0x%x): 0x%02x -> 0x%02x\n", d.Addr, d.Offset, d.Before, d.After) + shown++ + if shown >= 50 { + fmt.Printf(" ... (%d total)\n", len(diffs)) + break + } + } + path := prompt("Save diff to file [empty = skip]: ") + if path != "" { + if err := modify.WriteDiff(diffs, addr, path); err != nil { + fmt.Printf("Write failed: %v\n", err) + } else { + fmt.Printf("Diff saved to %s\n", path) + } + } +} + func handlePointerScan(st *app.State) { addr, ok := parseAddr("Target address (hex): ") if !ok { @@ -642,6 +695,10 @@ func main() { if requireAttached(pid) { handleDump(st) } + case "23d": + if requireAttached(pid) { + handleSnapshotDiff(st) + } case "23m": if requireAttached(pid) { handleShowMaps(st) @@ -789,6 +846,7 @@ func printMenu(st *app.State, d *adb.ADB) { fmt.Println(" 21. Unwatch Address") fmt.Println(" 22. List Watched") fmt.Println(" 23. Dump Memory Region") + fmt.Println("23d. Snapshot Diff") fmt.Println("23m. Show Memory Maps") fmt.Println("--- Pointer ---") fmt.Println(" pt. Pointer Scan") From 4987acbeabf43f08ecc063941fcf63ef686710a1 Mon Sep 17 00:00:00 2001 From: yotti Date: Thu, 7 May 2026 21:55:31 +0900 Subject: [PATCH 2/8] Add conditional watch with alert actions New AlertWatcher supports condition-based monitoring: - Conditions: above threshold, below threshold, value changed - Actions: notify only, or auto-write a value when triggered - CLI: 22a (set alert), 22r (remove alert) - Alerts are cleaned up on detach --- cli_memory.go | 67 ++++++++++++ cli_process.go | 1 + internal/app/state.go | 22 ++-- internal/memory/watch/alert.go | 192 +++++++++++++++++++++++++++++++++ main.go | 16 +++ 5 files changed, 288 insertions(+), 10 deletions(-) create mode 100644 internal/memory/watch/alert.go diff --git a/cli_memory.go b/cli_memory.go index 4dbf123..f093a12 100644 --- a/cli_memory.go +++ b/cli_memory.go @@ -8,6 +8,8 @@ import ( "memodroid/internal/app" "memodroid/internal/memory/modify" "memodroid/internal/memory/pointer" + "memodroid/internal/memory/search" + "memodroid/internal/memory/watch" ) func handleSetFreezeInterval(st *app.State) { @@ -184,3 +186,68 @@ func handleBookmarkList(st *app.State) { fmt.Printf("[%d] 0x%x %-20s %s = %s\n", i, b.Addr, b.Label, b.VType, vals[b.Addr]) } } + +func handleSetAlert(st *app.State) { + addr, ok := parseAddr("Address (hex): ") + if !ok { + return + } + fmt.Println("Condition: above, below, changed") + cond, err := watch.ParseAlertCondition(prompt("Condition: ")) + if err != nil { + fmt.Printf("%v\n", err) + return + } + vt := st.GetValueType() + cfg := watch.AlertConfig{ + Addr: addr, + Condition: cond, + Action: watch.ActionNotify, + } + if cond != watch.AlertChanged { + threshold, ok := parseValue("Threshold value: ", vt) + if !ok { + return + } + cfg.Threshold = threshold + } + action := prompt("Action (notify / write) [default: notify]: ") + if action == "write" { + cfg.Action = watch.ActionWrite + writeVal, ok := parseValue("Value to write when triggered: ", vt) + if !ok { + return + } + cfg.WriteVal = writeVal + } + intervalStr := prompt("Poll interval [default: 500ms]: ") + if intervalStr == "" { + intervalStr = "500ms" + } + interval, err := time.ParseDuration(intervalStr) + if err != nil { + fmt.Println("Invalid interval") + return + } + if err := st.AlertWatcher.WatchWithAlert(st.GetDriver(), st.GetPID(), vt, cfg, interval); err != nil { + fmt.Printf("Alert failed: %v\n", err) + return + } + fmt.Printf("Alert set on 0x%x: condition=%s action=%s\n", addr, cond, action) +} + +func handleRemoveAlert(st *app.State) { + addr, ok := parseAddr("Address (hex): ") + if !ok { + return + } + if err := st.AlertWatcher.RemoveAlert(addr); err != nil { + fmt.Printf("Remove alert: %v\n", err) + } else { + fmt.Printf("Alert removed for 0x%x\n", addr) + } +} + +// suppress unused import warnings +var _ = search.TypeInt32 +var _ = watch.AlertChanged diff --git a/cli_process.go b/cli_process.go index 1a3a1f8..9c0c003 100644 --- a/cli_process.go +++ b/cli_process.go @@ -66,6 +66,7 @@ func handleDetach(st *app.State) { } st.Freezer.UnfreezeAll() st.Watcher.UnwatchAll() + st.AlertWatcher.RemoveAll() st.GetDriver().Detach(pid) fmt.Printf("Detached from PID %d\n", pid) st.SetPID(0) diff --git a/internal/app/state.go b/internal/app/state.go index 0e26c80..64f604a 100644 --- a/internal/app/state.go +++ b/internal/app/state.go @@ -21,20 +21,22 @@ type State struct { bookmarks *store.BookmarkList snapshots map[uintptr][]byte - Freezer *modify.Freezer - UndoStack *modify.UndoStack - Watcher *watch.Watcher + Freezer *modify.Freezer + UndoStack *modify.UndoStack + Watcher *watch.Watcher + AlertWatcher *watch.AlertWatcher } func NewState(drv driver.Driver) *State { return &State{ - drv: drv, - valueType: search.TypeInt32, - bookmarks: store.NewBookmarkList(), - snapshots: make(map[uintptr][]byte), - Freezer: modify.NewFreezer(), - UndoStack: modify.NewUndoStack(), - Watcher: watch.NewWatcher(), + drv: drv, + valueType: search.TypeInt32, + bookmarks: store.NewBookmarkList(), + snapshots: make(map[uintptr][]byte), + Freezer: modify.NewFreezer(), + UndoStack: modify.NewUndoStack(), + Watcher: watch.NewWatcher(), + AlertWatcher: watch.NewAlertWatcher(), } } diff --git a/internal/memory/watch/alert.go b/internal/memory/watch/alert.go new file mode 100644 index 0000000..938978a --- /dev/null +++ b/internal/memory/watch/alert.go @@ -0,0 +1,192 @@ +package watch + +import ( + "fmt" + "sync" + "time" + + "memodroid/internal/driver" + "memodroid/internal/memory/search" +) + +// AlertCondition defines when an alert fires. +type AlertCondition int + +const ( + AlertAbove AlertCondition = iota // value > threshold + AlertBelow // value < threshold + AlertChanged // any change +) + +// AlertAction defines what to do when the condition is met. +type AlertAction int + +const ( + ActionNotify AlertAction = iota // just notify via OnAlert + ActionWrite // write a value when triggered +) + +// AlertConfig configures a conditional watch. +type AlertConfig struct { + Addr uintptr + Condition AlertCondition + Threshold []byte // for Above/Below comparisons + Action AlertAction + WriteVal []byte // value to write when ActionWrite +} + +// AlertEvent is emitted when an alert fires. +type AlertEvent struct { + Addr uintptr + Condition string + Value string + Triggered bool // whether an action was taken +} + +type alertEntry struct { + cfg AlertConfig + drv driver.Driver + pid int + vt search.ValueType + stop chan struct{} +} + +// AlertWatcher manages conditional watches with automatic actions. +type AlertWatcher struct { + mu sync.Mutex + entries map[uintptr]*alertEntry + OnAlert func(AlertEvent) +} + +func NewAlertWatcher() *AlertWatcher { + return &AlertWatcher{entries: make(map[uintptr]*alertEntry)} +} + +func ParseAlertCondition(s string) (AlertCondition, error) { + switch s { + case "above", ">": + return AlertAbove, nil + case "below", "<": + return AlertBelow, nil + case "changed", "!=": + return AlertChanged, nil + default: + return 0, fmt.Errorf("unknown condition: %s (use: above, below, changed)", s) + } +} + +func (c AlertCondition) String() string { + switch c { + case AlertAbove: + return "above" + case AlertBelow: + return "below" + case AlertChanged: + return "changed" + default: + return "?" + } +} + +// WatchWithAlert starts a conditional watch. When the condition fires, the +// action is executed (notify or write). +func (aw *AlertWatcher) WatchWithAlert(drv driver.Driver, pid int, vt search.ValueType, cfg AlertConfig, interval time.Duration) error { + aw.mu.Lock() + defer aw.mu.Unlock() + + if _, exists := aw.entries[cfg.Addr]; exists { + return fmt.Errorf("alert already set for 0x%x", cfg.Addr) + } + + e := &alertEntry{cfg: cfg, drv: drv, pid: pid, vt: vt, stop: make(chan struct{})} + aw.entries[cfg.Addr] = e + + go func() { + ticker := time.NewTicker(interval) + defer ticker.Stop() + var lastVal []byte + for { + select { + case <-ticker.C: + cur, err := e.drv.Peek(e.pid, e.cfg.Addr, e.vt.Size()) + if err != nil { + continue + } + + fired := false + switch e.cfg.Condition { + case AlertAbove: + fired = search.CompareValues(cur, e.cfg.Threshold, e.vt) > 0 + case AlertBelow: + fired = search.CompareValues(cur, e.cfg.Threshold, e.vt) < 0 + case AlertChanged: + if lastVal != nil { + fired = !search.EqualBytes(cur, lastVal) + } + } + + if fired { + triggered := false + if e.cfg.Action == ActionWrite && len(e.cfg.WriteVal) > 0 { + _ = e.drv.Poke(e.pid, e.cfg.Addr, e.cfg.WriteVal) + triggered = true + } + if aw.OnAlert != nil { + aw.OnAlert(AlertEvent{ + Addr: e.cfg.Addr, + Condition: e.cfg.Condition.String(), + Value: search.FormatValue(cur, e.vt), + Triggered: triggered, + }) + } + } + + if lastVal == nil { + lastVal = make([]byte, len(cur)) + } + copy(lastVal, cur) + case <-e.stop: + return + } + } + }() + + return nil +} + +// RemoveAlert stops a conditional watch. +func (aw *AlertWatcher) RemoveAlert(addr uintptr) error { + aw.mu.Lock() + defer aw.mu.Unlock() + + e, ok := aw.entries[addr] + if !ok { + return fmt.Errorf("no alert set for 0x%x", addr) + } + close(e.stop) + delete(aw.entries, addr) + return nil +} + +// RemoveAll stops all alerts. +func (aw *AlertWatcher) RemoveAll() { + aw.mu.Lock() + defer aw.mu.Unlock() + + for addr, e := range aw.entries { + close(e.stop) + delete(aw.entries, addr) + } +} + +// List returns all alert addresses. +func (aw *AlertWatcher) List() []uintptr { + aw.mu.Lock() + defer aw.mu.Unlock() + + addrs := make([]uintptr, 0, len(aw.entries)) + for addr := range aw.entries { + addrs = append(addrs, addr) + } + return addrs +} diff --git a/main.go b/main.go index 540b143..d04c1f3 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,14 @@ func main() { fmt.Printf("[Watch] 0x%x: %s -> %s\n", ev.Addr, ev.Prev, ev.Cur) } + st.AlertWatcher.OnAlert = func(ev watch.AlertEvent) { + action := "notify" + if ev.Triggered { + action = "WRITE" + } + fmt.Printf("[Alert] 0x%x: condition=%s value=%s action=%s\n", ev.Addr, ev.Condition, ev.Value, action) + } + go func() { if err := server.Start(defaultServerAddr, st, d); err != nil { _, _ = fmt.Fprintf(os.Stderr, "HTTP server error: %v\n", err) @@ -295,6 +303,12 @@ func main() { for _, addr := range addrs { fmt.Printf(" 0x%x\n", addr) } + case "22a": + if requireAttached(pid) { + handleSetAlert(st) + } + case "22r": + handleRemoveAlert(st) case "23": if requireAttached(pid) { handleDump(st) @@ -450,6 +464,8 @@ func printMenu(st *app.State, d *adb.ADB) { fmt.Println(" 20. Watch Address") fmt.Println(" 21. Unwatch Address") fmt.Println(" 22. List Watched") + fmt.Println("22a. Set Alert (conditional watch)") + fmt.Println("22r. Remove Alert") fmt.Println(" 23. Dump Memory Region") fmt.Println("23d. Snapshot Diff") fmt.Println("23m. Show Memory Maps") From 1f875884178cec065ef850b6daf3913db600f239 Mon Sep 17 00:00:00 2001 From: yotti Date: Thu, 7 May 2026 21:57:48 +0900 Subject: [PATCH 3/8] Add hex viewer panel to Web UI Add a hex dump endpoint GET /api/memory/hexdump that reads raw bytes from a given address and returns them as structured JSON lines. Add a corresponding "Hex Viewer" panel in the Web UI sidebar that displays memory in classic offset | hex | ASCII format. --- internal/server/handlers.go | 77 +++++++++++++++++++++++++++++++ internal/server/server.go | 1 + internal/server/static/index.html | 35 ++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 4709dae..06bc350 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -615,6 +615,83 @@ func (h *handler) memoryFrozen(w http.ResponseWriter, _ *http.Request) { writeJSON(w, out) } +// --- hexdump --- + +func (h *handler) memoryHexdump(w http.ResponseWriter, r *http.Request) { + pid, ok := requirePID(w, h) + if !ok { + return + } + addrStr := r.URL.Query().Get("addr") + sizeStr := r.URL.Query().Get("size") + if addrStr == "" || sizeStr == "" { + writeError(w, 400, "addr and size required") + return + } + addr, err := parseHexAddr(addrStr) + if err != nil { + writeError(w, 400, "invalid addr") + return + } + size, err := strconv.Atoi(sizeStr) + if err != nil || size <= 0 || size > 4096 { + writeError(w, 400, "size must be 1-4096") + return + } + data, err := h.state.GetDriver().ReadRegion(pid, addr, size) + if err != nil { + writeError(w, 500, err.Error()) + return + } + + type hexLine struct { + Offset int `json:"offset"` + Hex string `json:"hex"` + ASCII string `json:"ascii"` + } + var lines []hexLine + for i := 0; i < len(data); i += 16 { + end := i + 16 + if end > len(data) { + end = len(data) + } + chunk := data[i:end] + + // Build hex string + hexParts := make([]string, len(chunk)) + for j, b := range chunk { + hexParts[j] = fmt.Sprintf("%02x", b) + } + hexStr := "" + for j, p := range hexParts { + if j > 0 { + hexStr += " " + } + hexStr += p + } + + // Build ASCII string + ascii := make([]byte, len(chunk)) + for j, b := range chunk { + if b >= 0x20 && b <= 0x7e { + ascii[j] = b + } else { + ascii[j] = '.' + } + } + + lines = append(lines, hexLine{ + Offset: i, + Hex: hexStr, + ASCII: string(ascii), + }) + } + writeJSON(w, map[string]any{ + "addr": fmt.Sprintf("0x%x", addr), + "hex_lines": lines, + }) +} + // --- snapshot --- func (h *handler) snapshotTake(w http.ResponseWriter, r *http.Request) { diff --git a/internal/server/server.go b/internal/server/server.go index 9d94145..7cb301b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -80,6 +80,7 @@ func Start(addr string, state *app.State, d *adb.ADB) error { mux.HandleFunc("/api/memory/freeze-all", h.memoryFreezeAll) mux.HandleFunc("/api/memory/unfreeze", h.memoryUnfreeze) mux.HandleFunc("/api/memory/frozen", h.memoryFrozen) + mux.HandleFunc("/api/memory/hexdump", h.memoryHexdump) // Snapshot mux.HandleFunc("/api/snapshot/take", h.snapshotTake) diff --git a/internal/server/static/index.html b/internal/server/static/index.html index 426ee46..388795a 100644 --- a/internal/server/static/index.html +++ b/internal/server/static/index.html @@ -215,6 +215,7 @@