I've been using hexedit quite a lot, mainly for _corrupting_ files, and
have been meaning to send this collection of changes for far too long
now. I saw a bug requesting editing in the ASCII pane (which this patch
_doesn't_ add), and wanted to get this sent in before it has to undergo
the third massive merge conflict of its existence...

The main "TODO" in this is that I never got round to implementing
searching for an arbitrary byte sequence. It seems like we ought to have
that feature, but personally I'm far more likely to jump to an offset or
to search for some ASCII. I haven't needed to search for arbitrary byte
sequences in all this time, so I'll fix this if/when I actually need
it...

* Enter (new) read-only mode rather than refusing to open read-only
  files.

* More keys: page up/page down, home/end, and ctrl-home/ctrl-end for
  beginning/end of file.

* Jump with ^J (or vi-like :). Enter absolute address or +12 or -40 for
  relative jumps.

* Find with ^F (or vi-like /). No support for bytes, but useful for
  finding text. (^G or n for next match, ^D or p for previous match.)

* Support all the usual suspects for "quit": vi-like q, desktop-like ^Q,
  panic ^C, or even plain old Esc.

* The ASCII pane is made more readable by (hopefully) reasonable use of
  color. Regular control characters are shown in red using the
  appropriate letter (so a red A is 0x01, etc), printable characters are
  shown normally, and top-bit set characters are just shown as a purple
  question mark (since I couldn't come up with a better representation
  that had any obvious value --- in my experience top-bit set characters
  are either meaningless in ASCII, part of a UTF-8 sequence in modern
  files, or in some random code page in ancient files). The choice of
  red and purple was to deliberately make these not-actually-ASCII
  characters slide into the background; before this patch they have so
  many bright pixels (especially with the use of reverse video) that I
  couldn't clearly see the *actual* ASCII content in the ASCII pane.

* Addresses are now shown in yellow. No real justification other than "it
  looks nice".

* NUL bytes in the hex pane are shown dimmed. I find this helpful
  especially when there's a lot of padding, and it can actually be a
  useful clue when reverse engineering (you can "see" repeated patterns
  more easily), but I can understand if this one's controversial.

* Errors are shown "vim style" in bold white text on a red background,
  waiting briefly to ensure they're seen.

* The status bar shows the filename, whether the file is opened
  read-only, the current offset into the file, and the total
  length of the file.

* SIGWINCH handling has been added.
---
 lib/tty.c            |   4 +
 toys/other/hexedit.c | 291 ++++++++++++++++++++++++++++++-------------
 2 files changed, 210 insertions(+), 85 deletions(-)
From 663cfa3b1fd18a8484dd469273ed68e18142d827 Mon Sep 17 00:00:00 2001
From: Elliott Hughes <e...@google.com>
Date: Mon, 19 Apr 2021 13:57:10 -0700
Subject: [PATCH] hexedit: various improvements.

I've been using hexedit quite a lot, mainly for _corrupting_ files, and
have been meaning to send this collection of changes for far too long
now. I saw a bug requesting editing in the ASCII pane (which this patch
_doesn't_ add), and wanted to get this sent in before it has to undergo
the third massive merge conflict of its existence...

The main "TODO" in this is that I never got round to implementing
searching for an arbitrary byte sequence. It seems like we ought to have
that feature, but personally I'm far more likely to jump to an offset or
to search for some ASCII. I haven't needed to search for arbitrary byte
sequences in all this time, so I'll fix this if/when I actually need
it...

* Enter (new) read-only mode rather than refusing to open read-only
  files.

* More keys: page up/page down, home/end, and ctrl-home/ctrl-end for
  beginning/end of file.

* Jump with ^J (or vi-like :). Enter absolute address or +12 or -40 for
  relative jumps.

* Find with ^F (or vi-like /). No support for bytes, but useful for
  finding text. (^G or n for next match, ^D or p for previous match.)

* Support all the usual suspects for "quit": vi-like q, desktop-like ^Q,
  panic ^C, or even plain old Esc.

* The ASCII pane is made more readable by (hopefully) reasonable use of
  color. Regular control characters are shown in red using the
  appropriate letter (so a red A is 0x01, etc), printable characters are
  shown normally, and top-bit set characters are just shown as a purple
  question mark (since I couldn't come up with a better representation
  that had any obvious value --- in my experience top-bit set characters
  are either meaningless in ASCII, part of a UTF-8 sequence in modern
  files, or in some random code page in ancient files). The choice of
  red and purple was to deliberately make these not-actually-ASCII
  characters slide into the background; before this patch they have so
  many bright pixels (especially with the use of reverse video) that I
  couldn't clearly see the *actual* ASCII content in the ASCII pane.

* Addresses are now shown in yellow. No real justification other than "it
  looks nice".

* NUL bytes in the hex pane are shown dimmed. I find this helpful
  especially when there's a lot of padding, and it can actually be a
  useful clue when reverse engineering (you can "see" repeated patterns
  more easily), but I can understand if this one's controversial.

* Errors are shown "vim style" in bold white text on a red background,
  waiting briefly to ensure they're seen.

* The status bar shows the filename, whether the file is opened
  read-only, the current offset into the file, and the total
  length of the file.

* SIGWINCH handling has been added.
---
 lib/tty.c            |   4 +
 toys/other/hexedit.c | 291 ++++++++++++++++++++++++++++++-------------
 2 files changed, 210 insertions(+), 85 deletions(-)

diff --git a/lib/tty.c b/lib/tty.c
index b0d0c5d5..d7e0f47f 100644
--- a/lib/tty.c
+++ b/lib/tty.c
@@ -150,9 +150,11 @@ struct scan_key_list {
 
   // VT102/VT220 escapes.
   {KEY_HOME, "\033[1~"},
+  {KEY_HOME|KEY_CTRL, "\033[1;5~"},
   {KEY_INSERT, "\033[2~"},
   {KEY_DELETE, "\033[3~"},
   {KEY_END, "\033[4~"},
+  {KEY_END|KEY_CTRL, "\033[4;5~"},
   {KEY_PGUP, "\033[5~"},
   {KEY_PGDN, "\033[6~"},
   // "Normal" "PC" escapes (xterm).
@@ -161,6 +163,8 @@ struct scan_key_list {
   // "Application" "PC" escapes (gnome-terminal).
   {KEY_HOME, "\033[H"},
   {KEY_END, "\033[F"},
+  {KEY_HOME|KEY_CTRL, "\033[1;5H"},
+  {KEY_END|KEY_CTRL, "\033[1;5F"},
 
   {KEY_FN+1, "\033OP"}, {KEY_FN+2, "\033OQ"}, {KEY_FN+3, "\033OR"},
   {KEY_FN+4, "\033OS"}, {KEY_FN+5, "\033[15~"}, {KEY_FN+6, "\033[17~"},
diff --git a/toys/other/hexedit.c b/toys/other/hexedit.c
index 4b628463..398ec15d 100644
--- a/toys/other/hexedit.c
+++ b/toys/other/hexedit.c
@@ -10,18 +10,21 @@ config HEXEDIT
   bool "hexedit"
   default y
   help
-    usage: hexedit FILENAME
+    usage: hexedit FILE
 
-    Hexadecimal file editor. All changes are written to disk immediately.
+    Hexadecimal file editor/viewer. All changes are written to disk immediately.
 
     -r	Read only (display but don't edit)
 
     Keys:
-    Arrows        Move left/right/up/down by one line/column
-    Pg Up/Pg Dn   Move up/down by one page
-    0-9, a-f      Change current half-byte to hexadecimal value
-    u             Undo
-    q/^c/^d/<esc> Quit
+    Arrows         Move left/right/up/down by one line/column
+    PgUp/PgDn      Move up/down by one page
+    Home/End       Start/end of line (start/end of file with ctrl)
+    0-9, a-f       Change current half-byte to hexadecimal value
+    ^J or :        Jump (+/- for relative offset, otherwise absolute address)
+    ^F or /        Find string (^G/n: next, ^D/p: previous match)
+    u              Undo
+    q/^C/^Q/Esc    Quit
 */
 
 #define FOR_hexedit
@@ -31,39 +34,83 @@ GLOBALS(
   char *data;
   long long len, base;
   int numlen, undo, undolen;
-  unsigned height;
+  unsigned rows, cols;
+  long long pos;
+  char keybuf[16];
+  char input[80];
+  char *search;
 )
 
 #define UNDO_LEN (sizeof(toybuf)/(sizeof(long long)+1))
 
-// Render all characters printable, using color to distinguish.
-static int draw_char(FILE *fp, wchar_t broiled)
+static void show_error(char *what)
 {
-  if (fp) {
-    if (broiled<32 || broiled>=127) {
-      if (broiled>127) {
-        tty_esc("2m");
-        broiled &= 127;
-      }
-      if (broiled<32 || broiled==127) {
-        tty_esc("7m");
-        if (broiled==127) broiled = 32;
-        else broiled += 64;
-      }
-      printf("%c", (int)broiled);
-      tty_esc("0m");
-    } else printf("%c", (int)broiled);
+  tty_jump(0, TT.rows);
+  printf("\e[41m\e[37m\e[K\e[1m%s\e[0m", what);
+  xflush(1);
+  msleep(500);
+}
+
+// TODO: support arrow keys, insertion, and scrolling (and reuse in vi)
+static int prompt(char *prompt, char *initial_value)
+{
+  int yes = 0, key, len = strlen(initial_value);
+
+  strcpy(TT.input, initial_value);
+  while (1) {
+    tty_jump(0, TT.rows);
+    tty_esc("K");
+    printf("\e[1m%s: \e[0m%s", prompt, TT.input);
+    tty_esc("?25h");
+    xflush(1);
+
+    key = scan_key(TT.keybuf, -1);
+    if (key < 0 || key == 27) break;
+    if (key == '\r') {
+      yes = len; // Hitting enter with no input counts as cancellation.
+      break;
+    }
+
+    if (key == 0x7f) {
+      if (len > 0) TT.input[--len] = 0;
+    } else if (key == 'U'-'@') {
+      while (len > 0) TT.input[--len] = 0;
+    } else if (key >= ' ' && key < 0x7f && len < sizeof(TT.input)) {
+      TT.input[len++] = key;
+    }
   }
 
-  return 1;
+  tty_esc("?25l");
+  return yes;
+}
+
+// Render all characters printable, using color to distinguish.
+static void draw_char(int ch)
+{
+  if (ch >= ' ' && ch < 0x7f) putchar(ch);
+  else {
+    if (ch < ' ') printf("\e[31m%c", ch + '@');
+    else printf("\e[35m?");
+  }
+  printf("\e[0m");
 }
 
-static void draw_tail(void)
+static void draw_status(void)
 {
-  tty_jump(0, TT.height);
+  char line[80];
+
+  tty_jump(0, TT.rows);
   tty_esc("K");
 
-  draw_trim(*toys.optargs, -1, 71);
+  snprintf(line, sizeof(line), "\"%s\"%s, %#llx/%#llx", *toys.optargs,
+    FLAG(r) ? " [readonly]" : "", TT.pos, TT.len);
+  draw_trim(line, -1, TT.cols);
+}
+
+static void draw_byte(int byte)
+{
+  if (byte) printf("%02x", byte);
+  else printf("\e[2m00\e[0m");
 }
 
 static void draw_line(long long yy)
@@ -74,10 +121,13 @@ static void draw_line(long long yy)
   if (yy+xx>=TT.len) xx = TT.len-yy;
 
   if (yy<TT.len) {
-    printf("\r%0*llX ", TT.numlen, yy);
-    for (x=0; x<xx; x++) printf(" %02X", TT.data[yy+x]);
+    printf("\r\e[33m%0*llx\e[0m ", TT.numlen, yy);
+    for (x=0; x<xx; x++) {
+      putchar(' ');
+      draw_byte(TT.data[yy+x]);
+    }
     printf("%*s", 2+3*(16-xx), "");
-    for (x=0; x<xx; x++) draw_char(stdout, TT.data[yy+x]);
+    for (x=0; x<xx; x++) draw_char(TT.data[yy+x]);
     printf("%*s", 16-xx, "");
   }
   tty_esc("K");
@@ -88,11 +138,11 @@ static void draw_page(void)
   int y;
 
   tty_jump(0, 0);
-  for (y = 0; y<TT.height; y++) {
+  for (y = 0; y<TT.rows; y++) {
     if (y) printf("\r\n");
     draw_line(y);
   }
-  draw_tail();
+  draw_status();
 }
 
 // side: 0 = editing left, 1 = editing right, 2 = clear, 3 = read only
@@ -101,115 +151,177 @@ static void highlight(int xx, int yy, int side)
   char cc = TT.data[16*(TT.base+yy)+xx];
   int i;
 
-  // Display cursor
+  // Display cursor in hex area.
   tty_jump(2+TT.numlen+3*xx, yy);
   tty_esc("0m");
   if (side!=2) tty_esc("7m");
-  if (side>1) printf("%02X", cc);
+  if (side>1) draw_byte(cc);
   else for (i=0; i<2;) {
     if (side==i) tty_esc("32m");
-    printf("%X", (cc>>(4*(1&++i)))&15);
+    printf("%x", (cc>>(4*(1&++i)))&15);
   }
-  tty_esc("0m");
   tty_jump(TT.numlen+17*3+xx, yy);
-  draw_char(stdout, cc);
+
+  // Display cursor in text area.
+  if (side!=2) tty_esc("7m");
+  draw_char(cc);
 }
 
-void hexedit_main(void)
+static void find_next(int pos)
 {
-  long long pos = 0, y;
-  int x, i, side = 0, key, ro = toys.optflags&FLAG_r,
-      fd = xopen(*toys.optargs, ro ? O_RDONLY : O_RDWR);
-  char keybuf[16];
+  char *p;
 
-  *keybuf = 0;
+  p = memmem(TT.data+pos, TT.len-pos, TT.search, strlen(TT.search));
+  if (p) TT.pos = p - TT.data;
+  else show_error("No match!");
+}
+
+static void find_prev(int pos)
+{
+  size_t len = strlen(TT.search);
+
+  for (; pos >= 0; pos--) {
+    if (!memcmp(TT.data+pos, TT.search, len)) {
+      TT.pos = pos;
+      return;
+    }
+  }
+  show_error("No match!");
+}
+
+void hexedit_main(void)
+{
+  long long y;
+  int x, i, side = 0, key, fd;
 
   // Terminal setup
-  TT.height = 25;
-  terminal_size(0, &TT.height);
-  if (TT.height) TT.height--;
+  TT.cols = 80;
+  TT.rows = 24;
+  terminal_size(&TT.cols, &TT.rows);
+  if (TT.rows) TT.rows--;
+  xsignal(SIGWINCH, generic_signal);
   sigatexit(tty_sigreset);
   tty_esc("0m");
   tty_esc("?25l");
-  fflush(0);
+  xflush(1);
   xset_terminal(1, 1, 0, 0);
 
+  if (access(*toys.optargs, W_OK)) toys.optflags |= FLAG_r;
+  fd = xopen(*toys.optargs, FLAG(r) ? O_RDONLY : O_RDWR);
   if ((TT.len = fdlength(fd))<1) error_exit("bad length");
   if (sizeof(long)==32 && TT.len>SIZE_MAX) TT.len = SIZE_MAX;
   // count file length hex in digits, rounded up to multiple of 4
-  for (pos = TT.len, TT.numlen = 0; pos; pos >>= 4, TT.numlen++);
+  for (TT.pos = TT.len, TT.numlen = 0; TT.pos; TT.pos >>= 4, TT.numlen++);
   TT.numlen += (4-TT.numlen)&3;
 
-  TT.data = xmmap(0, TT.len, PROT_READ|(PROT_WRITE*!ro), MAP_SHARED, fd, 0);
+  TT.data=xmmap(0, TT.len, PROT_READ|(PROT_WRITE*!FLAG(r)), MAP_SHARED, fd, 0);
+  close(fd);
   draw_page();
 
   for (;;) {
     // Scroll display if necessary
-    if (pos<0) pos = 0;
-    if (pos>=TT.len) pos = TT.len-1;
-    x = pos&15;
-    y = pos/16;
+    if (TT.pos<0) TT.pos = 0;
+    if (TT.pos>=TT.len) TT.pos = TT.len-1;
+    x = TT.pos&15;
+    y = TT.pos/16;
 
-    i = 0;
     while (y<TT.base) {
-      if (TT.base-y>(TT.height/2)) {
+      if (TT.base-y>(TT.rows/2)) {
         TT.base = y;
         draw_page();
       } else {
         TT.base--;
-        i++;
         tty_jump(0, 0);
         tty_esc("1L");
         draw_line(0);
       }
     }
-    while (y>=TT.base+TT.height) {
-      if (y-(TT.base+TT.height)>(TT.height/2)) {
-        TT.base = y-TT.height-1;
+    while (y>=TT.base+TT.rows) {
+      if (y-(TT.base+TT.rows)>(TT.rows/2)) {
+        TT.base = y-TT.rows-1;
         draw_page();
       } else {
         TT.base++;
-        i++;
         tty_jump(0, 0);
         tty_esc("1M");
-        tty_jump(0, TT.height-1);
-        draw_line(TT.height-1);
+        tty_jump(0, TT.rows-1);
+        draw_line(TT.rows-1);
       }
     }
-    if (i) draw_tail();
+    draw_status();
     y -= TT.base;
 
     // Display cursor and flush output
-    highlight(x, y, ro ? 3 : side);
+    highlight(x, y, FLAG(r) ? 3 : side);
     xflush(1);
 
     // Wait for next key
-    key = scan_key(keybuf, -1);
-    // Exit for q, ctrl-c, ctrl-d, escape, or EOF
-    if (key==-1 || key==3 || key==4 || key==27 || key=='q') break;
+    key = scan_key(TT.keybuf, -1);
+
+    // Window resized?
+    if (key == -3) {
+      toys.signal = 0;
+      terminal_size(&TT.cols, &TT.rows);
+      if (TT.rows) TT.rows--;
+      draw_page();
+      continue;
+    }
+
+    // Various popular ways to quit...
+    if (key==-1||key==('C'-'@')||key==('Q'-'@')||key==27||key=='q') break;
+    highlight(x, y, 2);
+
+    if (key == ('J'-'@') || key == ':' || key == '-' || key == '+') {
+      // Jump (relative or absolute)
+      char initial[2] = {}, *s = 0;
+      long long val;
+
+      if (key == '-' || key == '+') *initial = key;
+      if (!prompt("Jump to", initial)) continue;
+
+      val = estrtol(TT.input, &s, 0);
+      if (!errno && s && !*s) {
+        if (*TT.input == '-' || *TT.input == '+') TT.pos += val;
+        else TT.pos = val;
+      }
+      continue;
+    } else if (key == ('F'-'@') || key == '/') { // Find
+      if (!prompt("Find", TT.search ? TT.search : "")) continue;
+
+      // TODO: parse hex escapes in input, and record length to support \0
+      free(TT.search);
+      TT.search = xstrdup(TT.input);
+      find_next(TT.pos);
+    } else if (TT.search && (key == ('G'-'@') || key == 'n')) { // Find next
+      if (TT.pos < TT.len) find_next(TT.pos+1);
+    } else if (TT.search && (key == ('D'-'@') || key == 'p')) { // Find previous
+      if (TT.pos > 0) find_prev(TT.pos-1);
+    }
+
+    // Remove cursor
     highlight(x, y, 2);
 
     // Hex digit?
     if (key>='a' && key<='f') key-=32;
-    if (!ro && ((key>='0' && key<='9') || (key>='A' && key<='F'))) {
+    if (!FLAG(r) && ((key>='0' && key<='9') || (key>='A' && key<='F'))) {
       if (!side) {
         long long *ll = (long long *)toybuf;
 
-        ll[TT.undo] = pos;
-        toybuf[(sizeof(long long)*UNDO_LEN)+TT.undo++] = TT.data[pos];
+        ll[TT.undo] = TT.pos;
+        toybuf[(sizeof(long long)*UNDO_LEN)+TT.undo++] = TT.data[TT.pos];
         if (TT.undolen < UNDO_LEN) TT.undolen++;
         TT.undo %= UNDO_LEN;
       }
 
       i = key - '0';
       if (i>9) i -= 7;
-      TT.data[pos] &= 15<<(4*side);
-      TT.data[pos] |= i<<(4*!side);
+      TT.data[TT.pos] &= 15<<(4*side);
+      TT.data[TT.pos] |= i<<(4*!side);
 
       if (++side==2) {
         highlight(x, y, side);
         side = 0;
-        ++pos;
+        ++TT.pos;
       }
     } else side = 0;
     if (key=='u') {
@@ -218,26 +330,35 @@ void hexedit_main(void)
 
         TT.undolen--;
         if (!TT.undo) TT.undo = UNDO_LEN;
-        pos = ll[--TT.undo];
-        TT.data[pos] = toybuf[sizeof(long long)*UNDO_LEN+TT.undo];
+        TT.pos = ll[--TT.undo];
+        TT.data[TT.pos] = toybuf[sizeof(long long)*UNDO_LEN+TT.undo];
       }
     }
     if (key>=256) {
       key -= 256;
 
-      if (key==KEY_UP) pos -= 16;
-      else if (key==KEY_DOWN) pos += 16;
+      if (key==KEY_UP) TT.pos -= 16;
+      else if (key==KEY_DOWN) TT.pos += 16;
       else if (key==KEY_RIGHT) {
-        if (x<15) pos++;
+        if (TT.pos<TT.len) TT.pos++;
       } else if (key==KEY_LEFT) {
-        if (x) pos--;
-      } else if (key==KEY_PGUP) pos -= 16*TT.height;
-      else if (key==KEY_PGDN) pos += 16*TT.height;
-      else if (key==KEY_HOME) pos = 0;
-      else if (key==KEY_END) pos = TT.len-1;
+        if (TT.pos>0) TT.pos--;
+      } else if (key==KEY_PGUP) {
+        TT.pos -= 16*TT.rows;
+        if (TT.pos < 0) TT.pos = 0;
+        TT.base = TT.pos/16;
+        draw_page();
+      } else if (key==KEY_PGDN) {
+        TT.pos += 16*TT.rows;
+        if (TT.pos > TT.len-1) TT.pos = TT.len-1;
+        TT.base = TT.pos/16;
+        draw_page();
+      } else if (key==KEY_HOME) TT.pos = TT.pos & ~0xf;
+      else if (key==KEY_END) TT.pos = TT.pos | 0xf;
+      else if (key==(KEY_CTRL|KEY_HOME)) TT.pos = 0;
+      else if (key==(KEY_CTRL|KEY_END)) TT.pos = TT.len-1;
     }
   }
   munmap(TT.data, TT.len);
-  close(fd);
   tty_reset();
 }
-- 
2.31.1.368.gbe11c130af-goog

_______________________________________________
Toybox mailing list
Toybox@lists.landley.net
http://lists.landley.net/listinfo.cgi/toybox-landley.net

Reply via email to