/* * Copyright (C) 2017 Denys Vlasenko * * Licensed under GPLv2, see file LICENSE in this source tree. */ //config:config HEXEDIT //config: bool "hexedit (21 kb)" //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"[J" #define CLEAR_TILL_EOL ESC"[K" #define SET_ALT_SCR ESC"[?1049h" #define POP_ALT_SCR ESC"[?1049l" #undef CTRL #define CTRL(c) ((c) & (uint8_t)~0x60) struct globals { smallint half; smallint in_read_key; int fd; unsigned height; unsigned row; IF_VARIABLE_ARCH_PAGESIZE(unsigned pagesize;) #define G_pagesize cached_pagesize(G.pagesize) uint8_t *baseaddr; uint8_t *current_byte; uint8_t *eof_byte; off_t size; off_t offset; /* needs to be zero-inited, thus keeping it in G: */ char read_key_buffer[KEYCODE_BUFFER_SIZE]; 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) /* "12ef5670 (xx )*16 _1_3_5_7_9abcdef\n"NUL */ #define LINEBUF_SIZE (8 + 1 + 3*16 + 16 + 1 + 1 /*paranoia:*/ + 13) static void restore_term(void) { tcsetattr_stdin_TCSANOW(&G.orig_termios); printf(POP_ALT_SCR); fflush_all(); } static void sig_catcher(int sig) { if (!G.in_read_key) { /* now it's not safe to do I/O, just inform the main loop */ bb_got_signal = sig; return; } restore_term(); kill_myself_with_sig(sig); } static int format_line(char *hex, uint8_t *data, off_t offset) { int ofs_pos; char *text; uint8_t *end, *end1; #if 1 /* Can be more than 4Gb, thus >8 chars, thus use a variable - don't assume 8! */ ofs_pos = sprintf(hex, "%08"OFF_FMT"x ", offset); #else if (offset <= 0xffff) ofs_pos = sprintf(hex, "%04"OFF_FMT"x ", offset); else ofs_pos = sprintf(hex, "%08"OFF_FMT"x ", offset); #endif hex += ofs_pos; text = hex + 16 * 3; 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'; return ofs_pos; } static void redraw(unsigned cursor) { uint8_t *data; off_t offset; unsigned i, pos; printf(HOME CLEAR); /* if cursor is past end of screen, how many lines to move down? */ i = (cursor / 16) - G.height + 1; if ((int)i < 0) i = 0; data = G.baseaddr + i * 16; offset = G.offset + i * 16; cursor -= i * 16; pos = i = 0; while (i < G.height) { char buf[LINEBUF_SIZE]; pos = format_line(buf, data, offset); printf( "\r\n%s" + (!i) * 2, /* print \r\n only on 2nd line and later */ buf ); data += 16; offset += 16; i++; } G.row = cursor / 16; printf(ESC"[%u;%uH", 1 + G.row, 1 + pos + (cursor & 0xf) * 3); } static void redraw_cur_line(void) { char buf[LINEBUF_SIZE]; uint8_t *data; off_t offset; int column; column = (0xf & (uintptr_t)G.current_byte); data = G.current_byte - column; offset = G.offset + (data - G.baseaddr); column = column*3 + G.half; column += format_line(buf, data, offset); printf("%s" "\r" "%.*s", buf + column, column, buf ); } /* if remappers return 0, no change was done */ static int remap(unsigned cur_pos) { if (G.baseaddr) munmap(G.baseaddr, G_mapsize); G.baseaddr = mmap(NULL, G_mapsize, PROT_READ | PROT_WRITE, MAP_SHARED, G.fd, G.offset ); if (G.baseaddr == MAP_FAILED) { restore_term(); bb_simple_perror_msg_and_die("mmap"); } G.current_byte = G.baseaddr + cur_pos; G.eof_byte = G.baseaddr + 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.baseaddr + (G.size - G.offset); } return 1; } static int move_mapping_further(void) { unsigned pos; unsigned pagesize; if ((G.size - G.offset) < G_mapsize) return 0; /* can't move mapping even further, it's at the end already */ pagesize = G_pagesize; /* constant on most arches */ pos = G.current_byte - G.baseaddr; 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); return remap(pos); } return 0; } static int move_mapping_lower(void) { unsigned pos; unsigned pagesize; if (G.offset == 0) return 0; /* we are at 0 already */ pagesize = G_pagesize; /* constant on most arches */ pos = G.current_byte - G.baseaddr; /* 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; return remap(pos); } //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) { INIT_G(); INIT_PAGESIZE(G.pagesize); get_terminal_width_height(-1, NULL, &G.height); if (1) { /* reduce number of write() syscalls while PgUp/Down: fully buffered output */ unsigned sz = (G.height | 0xf) * LINEBUF_SIZE; setvbuf(stdout, xmalloc(sz), _IOFBF, sz); } getopt32(argv, "^" "" "\0" "=1"/*one arg*/); argv += optind; 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 */ printf(SET_ALT_SCR); set_termios_to_raw(STDIN_FILENO, &G.orig_termios, TERMIOS_RAW_CRNL); bb_signals(BB_FATAL_SIGS, sig_catcher); remap(0); redraw(0); //TODO: //Home/End: start/end of line; '<'/'>': start/end of file //Backspace: undo //Ctrl-L: redraw //Ctrl-Z: suspend //'/', Ctrl-S: search //TODO: detect window resize for (;;) { unsigned cnt; int32_t key = key; /* for compiler */ uint8_t byte; fflush_all(); G.in_read_key = 1; if (!bb_got_signal) key = read_key(STDIN_FILENO, G.read_key_buffer, -1); G.in_read_key = 0; if (bb_got_signal) key = CTRL('X'); cnt = 1; if ((unsigned)(key - 'A') <= 'Z' - 'A') key |= 0x20; /* convert A-Z to a-z */ switch (key) { case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': /* convert to '0'+10...15 */ key = key - ('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) { if (!move_mapping_further()) { /* already at EOF; 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_PAGEDOWN: cnt = G.height; case KEYCODE_DOWN: k_down: G.current_byte += 16; if (G.current_byte >= G.eof_byte) { move_mapping_further(); if (G.current_byte > G.eof_byte) { /* _after_ eof - don't allow this */ G.current_byte -= 16; if (G.current_byte < G.baseaddr) move_mapping_lower(); break; } } down: putchar('\n'); /* down one line, possibly scroll screen */ G.row++; if (G.row >= G.height) { G.row--; redraw_cur_line(); } if (--cnt) goto k_down; 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.baseaddr) { if (!move_mapping_lower()) 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_PAGEUP: cnt = G.height; case KEYCODE_UP: k_up: if ((G.current_byte - G.baseaddr) < 16) { if (!move_mapping_lower()) break; /* already at 0, stop */ } G.current_byte -= 16; up: if (G.row != 0) { G.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(); } if (--cnt) goto k_up; break; case '\n': case '\r': /* [Enter]: goto specified position */ { char buf[sizeof(G.offset)*3 + 4]; printf(ESC"[999;1H" CLEAR_TILL_EOL); /* go to last line */ if (read_line_input(NULL, "Go to (dec,0Xhex,0oct): ", buf, sizeof(buf)) > 0) { off_t t; unsigned cursor; t = bb_strtoull(buf, NULL, 0); if (t >= G.size) t = G.size - 1; cursor = t & (G_pagesize - 1); t -= cursor; if (t < 0) cursor = t = 0; if (t != 0 && cursor < 0x1ff) { /* very close to end of page, possibly to EOF */ /* move one page lower */ t -= G_pagesize; cursor += G_pagesize; } G.offset = t; remap(cursor); redraw(cursor); break; } /* ^C/EOF/error: fall through to exiting */ } case CTRL('X'): restore_term(); return EXIT_SUCCESS; } /* switch */ } /* for (;;) */ /* not reached */ return EXIT_SUCCESS; }