--- /dev/null
+/*
+ * Copyright (C) 2017 Denys Vlasenko <vda.linux@googlemail.com>
+ *
+ * Licensed under GPLv2, see file LICENSE in this source tree.
+ */
+//config:config HEXEDIT
+//config: bool "hexedit"
+//config: default y
+//config: help
+//config: Edit file in hexadecimal.
+
+//applet:IF_HEXEDIT(APPLET(hexedit, BB_DIR_USR_BIN, BB_SUID_DROP))
+
+//kbuild:lib-$(CONFIG_HEXEDIT) += hexedit.o
+
+#include "libbb.h"
+
+#define ESC "\033"
+#define HOME ESC"[H"
+#define CLEAR ESC"[H"ESC"[J"
+
+struct globals {
+ smallint half;
+ int fd;
+ unsigned height;
+ uint8_t *addr;
+ uint8_t *current_byte;
+ uint8_t *eof_byte;
+ off_t size;
+ off_t offset;
+ struct termios orig_termios;
+};
+#define G (*ptr_to_globals)
+#define INIT_G() do { \
+ SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
+} while (0)
+
+/* Hopefully there aren't arches with PAGE_SIZE > 64k */
+#define G_mapsize (64*1024)
+
+static void format_line(char *hex, uint8_t *data, off_t offset)
+{
+ char *text = hex + 16*3;
+ uint8_t *end, *end1;
+
+ end1 = data + 15;
+ if ((G.size - offset) > 0) {
+ end = end1;
+ if ((G.size - offset) <= 15)
+ end = data + (G.size - offset) - 1;
+ while (data <= end) {
+ uint8_t c = *data++;
+ *hex++ = bb_hexdigits_upcase[c >> 4];
+ *hex++ = bb_hexdigits_upcase[c & 0xf];
+ *hex++ = ' ';
+ if (c < ' ' || c > 0x7e)
+ c = '.';
+ *text++ = c;
+ }
+ }
+ while (data <= end1) {
+ *hex++ = ' ';
+ *hex++ = ' ';
+ *hex++ = ' ';
+ *text++ = ' ';
+ data++;
+ }
+ *text = '\0';
+}
+
+static void redraw(void)
+{
+ uint8_t *data;
+ off_t offset;
+ unsigned i;
+
+ data = G.addr;
+ offset = 0;
+ i = 0;
+ while (i < G.height) {
+ char buf[16*4 + 8];
+ format_line(buf, data, offset);
+ printf(
+ "\r\n%08"OFF_FMT"x %s" + (!i)*2, /* print \r\n only on 2nd line and later */
+ offset, buf
+ );
+ data += 16;
+ offset += 16;
+ i++;
+ }
+}
+
+static void redraw_cur_line(void)
+{
+ off_t offset;
+ int len;
+ char buf[16*4 + 8];
+ uint8_t *data;
+
+ len = (0xf & (uintptr_t)G.current_byte);
+ data = G.current_byte - len;
+ offset = G.offset + (data - G.addr);
+ format_line(buf, data, offset);
+ printf(
+ "\r%08"OFF_FMT"x %s",
+ offset, buf
+ );
+ printf(
+ "\r%08"OFF_FMT"x %.*s",
+ offset, (len*3 + G.half), buf
+ );
+}
+
+static void remap(unsigned cur_pos)
+{
+ if (G.addr)
+ munmap(G.addr, G_mapsize);
+
+ G.addr = mmap(NULL,
+ G_mapsize,
+ PROT_READ | PROT_WRITE,
+ MAP_SHARED,
+ G.fd,
+ G.offset
+ );
+ if (G.addr == MAP_FAILED)
+//TODO: restore termios?
+ bb_perror_msg_and_die("mmap");
+
+ G.current_byte = G.addr + cur_pos;
+
+ G.eof_byte = G.addr + G_mapsize;
+ if ((G.size - G.offset) < G_mapsize) {
+ /* mapping covers tail of the file */
+ /* we do have a mapped byte which is past eof */
+ G.eof_byte = G.addr + (G.size - G.offset);
+ }
+}
+static void move_mapping_further(void)
+{
+ unsigned pos;
+ unsigned pagesize;
+
+ if ((G.size - G.offset) < G_mapsize)
+ return; /* can't move mapping even further, it's at the end already */
+
+ pagesize = getpagesize(); /* constant on most arches */
+ pos = G.current_byte - G.addr;
+ if (pos >= pagesize) {
+ /* Move offset up until current position is in 1st page */
+ do {
+ G.offset += pagesize;
+ if (G.offset == 0) { /* whoops */
+ G.offset -= pagesize;
+ break;
+ }
+ pos -= pagesize;
+ } while (pos >= pagesize);
+ remap(pos);
+ }
+}
+static void move_mapping_lower(void)
+{
+ unsigned pos;
+ unsigned pagesize;
+
+ if (G.offset == 0)
+ return; /* we are at 0 already */
+
+ pagesize = getpagesize(); /* constant on most arches */
+ pos = G.current_byte - G.addr;
+
+ /* Move offset down until current position is in last page */
+ pos += pagesize;
+ while (pos < G_mapsize) {
+ pos += pagesize;
+ G.offset -= pagesize;
+ if (G.offset == 0)
+ break;
+ }
+ pos -= pagesize;
+
+ remap(pos);
+}
+
+static void sig_catcher(int sig)
+{
+ tcsetattr_stdin_TCSANOW(&G.orig_termios);
+ kill_myself_with_sig(sig);
+}
+
+//usage:#define hexedit_trivial_usage
+//usage: "FILE"
+//usage:#define hexedit_full_usage "\n\n"
+//usage: "Edit FILE in hexadecimal"
+int hexedit_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
+int hexedit_main(int argc UNUSED_PARAM, char **argv)
+{
+ unsigned row = 0;
+
+ INIT_G();
+
+ getopt32(argv, "");
+ argv += optind;
+
+ get_terminal_width_height(-1, NULL, &G.height);
+
+ G.fd = xopen(*argv, O_RDWR);
+ G.size = xlseek(G.fd, 0, SEEK_END);
+
+ /* TERMIOS_RAW_CRNL suppresses \n -> \r\n translation, helps with down-arrow */
+ set_termios_to_raw(STDIN_FILENO, &G.orig_termios, TERMIOS_RAW_CRNL);
+ bb_signals(BB_FATAL_SIGS, sig_catcher);
+
+ remap(0);
+
+ printf(CLEAR);
+ redraw();
+ printf(HOME "%08x ", 0);
+
+//TODO: //PgUp/PgDown; Home/End: start/end of line; '<'/'>': start/end of file
+ //Backspace: undo
+ //Enter: goto specified position
+ //Ctrl-L: redraw
+ //Ctrl-X: save and exit (maybe also Q?)
+ //Ctrl-Z: suspend
+ //'/', Ctrl-S: search
+//TODO: go to end-of-screen on exit (for this, sighandler should interrupt read_key())
+//TODO: detect window resize
+//TODO: read-only mode if open(O_RDWR) fails? hide cursor in this case?
+
+//TODO: smarter redraw: if down-arrow is pressed on last visible line,
+//emit LF, then print the tail of next line, then CR, then beginning -
+//which makes cursor end up exactly where it should be! Same for up-arrow.
+
+ for (;;) {
+ char read_key_buffer[KEYCODE_BUFFER_SIZE];
+ int32_t key;
+ uint8_t byte;
+
+ fflush_all();
+ key = read_key(STDIN_FILENO, read_key_buffer, -1);
+
+ switch (key) {
+ case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+ case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+ /* lowercase, then convert to '0'+10...15 */
+ key = (key | 0x20) - ('a' - '0' - 10);
+ /* fall through */
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ if (G.current_byte == G.eof_byte) {
+ move_mapping_further();
+ if (G.current_byte == G.eof_byte) {
+ /* extend the file */
+ if (++G.size <= 0 /* overflow? */
+ || ftruncate(G.fd, G.size) != 0 /* error extending? (e.g. block dev) */
+ ) {
+ G.size--;
+ break;
+ }
+ G.eof_byte++;
+ }
+ }
+ key -= '0';
+ byte = *G.current_byte & 0xf0;
+ if (!G.half) {
+ byte = *G.current_byte & 0x0f;
+ key <<= 4;
+ }
+ *G.current_byte = byte + key;
+ /* can't just print one updated hex char: need to update right-hand ASCII too */
+ redraw_cur_line();
+ /* fall through */
+ case KEYCODE_RIGHT:
+ if (G.current_byte == G.eof_byte)
+ break; /* eof - don't allow going past it */
+ byte = *G.current_byte;
+ if (!G.half) {
+ G.half = 1;
+ putchar(bb_hexdigits_upcase[byte >> 4]);
+ } else {
+ G.half = 0;
+ G.current_byte++;
+ if ((0xf & (uintptr_t)G.current_byte) == 0) {
+ /* rightmost pos, wrap to next line */
+ if (G.current_byte == G.eof_byte)
+ move_mapping_further();
+ printf(ESC"[46D"); /* cursor left 3*15 + 1 chars */
+ goto down;
+ }
+ putchar(bb_hexdigits_upcase[byte & 0xf]);
+ putchar(' ');
+ }
+ break;
+ case KEYCODE_DOWN:
+ G.current_byte += 16;
+ if (G.current_byte >= G.eof_byte) {
+ move_mapping_further();
+ if (G.current_byte > G.eof_byte) {
+ /* eof - don't allow going past it */
+ G.current_byte -= 16;
+ break;
+ }
+ }
+ down:
+ putchar('\n'); /* down one line, possibly scroll screen */
+ row++;
+ if (row >= G.height) {
+ row--;
+ redraw_cur_line();
+ }
+ break;
+
+ case KEYCODE_LEFT:
+ if (G.half) {
+ G.half = 0;
+ printf(ESC"[D");
+ break;
+ }
+ if ((0xf & (uintptr_t)G.current_byte) == 0) {
+ /* leftmost pos, wrap to prev line */
+ if (G.current_byte == G.addr)
+ move_mapping_lower();
+ if ((G.current_byte - G.addr) < 16)
+ break; /* first line, don't do anything */
+ G.half = 1;
+ G.current_byte--;
+ printf(ESC"[46C"); /* cursor right 3*15 + 1 chars */
+ goto up;
+ }
+ G.half = 1;
+ G.current_byte--;
+ printf(ESC"[2D");
+ break;
+ case KEYCODE_UP:
+ if ((G.current_byte - G.addr) < 16) {
+ move_mapping_lower();
+ if ((G.current_byte - G.addr) < 16)
+ break;
+ }
+ G.current_byte -= 16;
+ up:
+ if (row != 0) {
+ row--;
+ printf(ESC"[A"); /* up (won't scroll) */
+ } else {
+ //printf(ESC"[T"); /* scroll up */ - not implemented on Linux VT!
+ printf(ESC"M"); /* scroll up */
+ redraw_cur_line();
+ }
+ break;
+ }
+ }
+
+ return EXIT_SUCCESS;
+}