hexedit: restore screen on exit
[oweals/busybox.git] / miscutils / hexedit.c
1 /*
2  * Copyright (C) 2017 Denys Vlasenko <vda.linux@googlemail.com>
3  *
4  * Licensed under GPLv2, see file LICENSE in this source tree.
5  */
6 //config:config HEXEDIT
7 //config:       bool "hexedit"
8 //config:       default y
9 //config:       help
10 //config:       Edit file in hexadecimal.
11
12 //applet:IF_HEXEDIT(APPLET(hexedit, BB_DIR_USR_BIN, BB_SUID_DROP))
13
14 //kbuild:lib-$(CONFIG_HEXEDIT) += hexedit.o
15
16 #include "libbb.h"
17
18 #define ESC             "\033"
19 #define HOME            ESC"[H"
20 #define CLEAR           ESC"[H"ESC"[J"
21 #define SET_ALT_SCR     ESC"[?1049h"
22 #define POP_ALT_SCR     ESC"[?1049l"
23
24 #undef CTRL
25 #define CTRL(c)  ((c) & (uint8_t)~0x60)
26
27 struct globals {
28         smallint half;
29         smallint in_read_key;
30         int fd;
31         unsigned height;
32         unsigned row;
33         uint8_t *addr;
34         uint8_t *current_byte;
35         uint8_t *eof_byte;
36         off_t size;
37         off_t offset;
38         /* needs to be zero-inited, thus keeping it in G: */
39         char read_key_buffer[KEYCODE_BUFFER_SIZE];
40         struct termios orig_termios;
41 };
42 #define G (*ptr_to_globals)
43 #define INIT_G() do { \
44         SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
45 } while (0)
46
47 /* Hopefully there aren't arches with PAGE_SIZE > 64k */
48 #define G_mapsize (64*1024)
49
50 /* "12ef5670 (xx )*16 _1_3_5_7_9abcdef\n"NUL */
51 #define LINEBUF_SIZE (8 + 1 + 3*16 + 16 + 1 + 1 /*paranoia:*/ + 13)
52
53 static void restore_term(void)
54 {
55         tcsetattr_stdin_TCSANOW(&G.orig_termios);
56         printf(POP_ALT_SCR);
57         fflush_all();
58 }
59
60 static void sig_catcher(int sig)
61 {
62         if (!G.in_read_key) {
63                 /* now it's not safe to do I/O, just inform the main loop */
64                 bb_got_signal = sig;
65                 return;
66         }
67         restore_term();
68         kill_myself_with_sig(sig);
69 }
70
71 static int format_line(char *hex, uint8_t *data, off_t offset)
72 {
73         int ofs_pos;
74         char *text;
75         uint8_t *end, *end1;
76
77 #if 1
78         /* Can be more than 4Gb, thus >8 chars, thus use a variable - don't assume 8! */
79         ofs_pos = sprintf(hex, "%08"OFF_FMT"x ", offset);
80 #else
81         if (offset <= 0xffff)
82                 ofs_pos = sprintf(hex, "%04"OFF_FMT"x ", offset);
83         else
84                 ofs_pos = sprintf(hex, "%08"OFF_FMT"x ", offset);
85 #endif
86         hex += ofs_pos;
87
88         text = hex + 16*3;
89         end1 = data + 15;
90         if ((G.size - offset) > 0) {
91                 end = end1;
92                 if ((G.size - offset) <= 15)
93                         end = data + (G.size - offset) - 1;
94                 while (data <= end) {
95                         uint8_t c = *data++;
96                         *hex++ = bb_hexdigits_upcase[c >> 4];
97                         *hex++ = bb_hexdigits_upcase[c & 0xf];
98                         *hex++ = ' ';
99                         if (c < ' ' || c > 0x7e)
100                                 c = '.';
101                         *text++ = c;
102                 }
103         }
104         while (data <= end1) {
105                 *hex++ = ' ';
106                 *hex++ = ' ';
107                 *hex++ = ' ';
108                 *text++ = ' ';
109                 data++;
110         }
111         *text = '\0';
112
113         return ofs_pos;
114 }
115
116 static void redraw(void)
117 {
118         uint8_t *data;
119         off_t offset;
120         unsigned i;
121
122         data = G.addr;
123         offset = 0;
124         i = 0;
125         while (i < G.height) {
126                 char buf[LINEBUF_SIZE];
127                 format_line(buf, data, offset);
128                 printf(
129                         "\r\n%s" + (!i)*2, /* print \r\n only on 2nd line and later */
130                         buf
131                 );
132                 data += 16;
133                 offset += 16;
134                 i++;
135         }
136 }
137
138 static void redraw_cur_line(void)
139 {
140         char buf[LINEBUF_SIZE];
141         uint8_t *data;
142         off_t offset;
143         int column;
144
145         column = (0xf & (uintptr_t)G.current_byte);
146         data = G.current_byte - column;
147         offset = G.offset + (data - G.addr);
148
149         column = column*3 + G.half;
150         column += format_line(buf, data, offset);
151         printf("%s"
152                 "\r"
153                 "%.*s",
154                 buf + column,
155                 column, buf
156         );
157 }
158
159 static void remap(unsigned cur_pos)
160 {
161         if (G.addr)
162                 munmap(G.addr, G_mapsize);
163
164         G.addr = mmap(NULL,
165                 G_mapsize,
166                 PROT_READ | PROT_WRITE,
167                 MAP_SHARED,
168                 G.fd,
169                 G.offset
170         );
171         if (G.addr == MAP_FAILED) {
172                 restore_term();
173                 bb_perror_msg_and_die("mmap");
174         }
175
176         G.current_byte = G.addr + cur_pos;
177
178         G.eof_byte = G.addr + G_mapsize;
179         if ((G.size - G.offset) < G_mapsize) {
180                 /* mapping covers tail of the file */
181                 /* we do have a mapped byte which is past eof */
182                 G.eof_byte = G.addr + (G.size - G.offset);
183         }
184 }
185 static void move_mapping_further(void)
186 {
187         unsigned pos;
188         unsigned pagesize;
189
190         if ((G.size - G.offset) < G_mapsize)
191                 return; /* can't move mapping even further, it's at the end already */
192
193         pagesize = getpagesize(); /* constant on most arches */
194         pos = G.current_byte - G.addr;
195         if (pos >= pagesize) {
196                 /* move offset up until current position is in 1st page */
197                 do {
198                         G.offset += pagesize;
199                         if (G.offset == 0) { /* whoops */
200                                 G.offset -= pagesize;
201                                 break;
202                         }
203                         pos -= pagesize;
204                 } while (pos >= pagesize);
205                 remap(pos);
206         }
207 }
208 static void move_mapping_lower(void)
209 {
210         unsigned pos;
211         unsigned pagesize;
212
213         if (G.offset == 0)
214                 return; /* we are at 0 already */
215
216         pagesize = getpagesize(); /* constant on most arches */
217         pos = G.current_byte - G.addr;
218
219         /* move offset down until current position is in last page */
220         pos += pagesize;
221         while (pos < G_mapsize) {
222                 pos += pagesize;
223                 G.offset -= pagesize;
224                 if (G.offset == 0)
225                         break;
226         }
227         pos -= pagesize;
228
229         remap(pos);
230 }
231
232 //usage:#define hexedit_trivial_usage
233 //usage:        "FILE"
234 //usage:#define hexedit_full_usage "\n\n"
235 //usage:        "Edit FILE in hexadecimal"
236 int hexedit_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
237 int hexedit_main(int argc UNUSED_PARAM, char **argv)
238 {
239         INIT_G();
240
241         get_terminal_width_height(-1, NULL, &G.height);
242         if (1) {
243                 /* reduce number of write() syscalls while PgUp/Down: fully buffered output */
244                 unsigned sz = (G.height | 0xf) * LINEBUF_SIZE;
245                 setvbuf(stdout, xmalloc(sz), _IOFBF, sz);
246         }
247
248         getopt32(argv, "^" "" "\0" "=1"/*one arg*/);
249         argv += optind;
250
251         G.fd = xopen(*argv, O_RDWR);
252         G.size = xlseek(G.fd, 0, SEEK_END);
253
254         /* TERMIOS_RAW_CRNL suppresses \n -> \r\n translation, helps with down-arrow */
255         set_termios_to_raw(STDIN_FILENO, &G.orig_termios, TERMIOS_RAW_CRNL);
256         bb_signals(BB_FATAL_SIGS, sig_catcher);
257
258         remap(0);
259
260         printf(SET_ALT_SCR);
261         redraw();
262         printf(ESC"[1;10H"); /* position on 1st hex byte in first line */
263
264 //TODO: //Home/End: start/end of line; '<'/'>': start/end of file
265         //Backspace: undo
266         //Enter: goto specified position
267         //Ctrl-L: redraw
268         //Ctrl-Z: suspend
269         //'/', Ctrl-S: search
270 //TODO: detect window resize
271
272         for (;;) {
273                 unsigned cnt;
274                 int32_t key = key; // for compiler
275                 uint8_t byte;
276
277                 fflush_all();
278                 G.in_read_key = 1;
279                 if (!bb_got_signal)
280                         key = read_key(STDIN_FILENO, G.read_key_buffer, -1);
281                 G.in_read_key = 0;
282                 if (bb_got_signal)
283                         key = CTRL('X');
284
285                 cnt = 1;
286                 switch (key) {
287                 case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
288                 case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
289                         /* lowercase, then convert to '0'+10...15 */
290                         key = (key | 0x20) - ('a' - '0' - 10);
291                         /* fall through */
292                 case '0': case '1': case '2': case '3': case '4':
293                 case '5': case '6': case '7': case '8': case '9':
294                         if (G.current_byte == G.eof_byte) {
295                                 move_mapping_further();
296                                 if (G.current_byte == G.eof_byte) {
297                                         /* extend the file */
298                                         if (++G.size <= 0 /* overflow? */
299                                          || ftruncate(G.fd, G.size) != 0 /* error extending? (e.g. block dev) */
300                                         ) {
301                                                 G.size--;
302                                                 break;
303                                         }
304                                         G.eof_byte++;
305                                 }
306                         }
307                         key -= '0';
308                         byte = *G.current_byte & 0xf0;
309                         if (!G.half) {
310                                 byte = *G.current_byte & 0x0f;
311                                 key <<= 4;
312                         }
313                         *G.current_byte = byte + key;
314                         /* can't just print one updated hex char: need to update right-hand ASCII too */
315                         redraw_cur_line();
316                         /* fall through */
317                 case KEYCODE_RIGHT:
318                         if (G.current_byte == G.eof_byte)
319                                 break; /* eof - don't allow going past it */
320                         byte = *G.current_byte;
321                         if (!G.half) {
322                                 G.half = 1;
323                                 putchar(bb_hexdigits_upcase[byte >> 4]);
324                         } else {
325                                 G.half = 0;
326                                 G.current_byte++;
327                                 if ((0xf & (uintptr_t)G.current_byte) == 0) {
328                                         /* rightmost pos, wrap to next line */
329                                         if (G.current_byte == G.eof_byte)
330                                                 move_mapping_further();
331                                         printf(ESC"[46D"); /* cursor left 3*15 + 1 chars */
332                                         goto down;
333                                 }
334                                 putchar(bb_hexdigits_upcase[byte & 0xf]);
335                                 putchar(' ');
336                         }
337                         break;
338                 case KEYCODE_PAGEDOWN:
339                         cnt = G.height;
340                 case KEYCODE_DOWN:
341  k_down:
342                         G.current_byte += 16;
343                         if (G.current_byte >= G.eof_byte) {
344                                 move_mapping_further();
345                                 if (G.current_byte > G.eof_byte) {
346                                         /* eof - don't allow going past it */
347                                         G.current_byte -= 16;
348                                         break;
349                                 }
350                         }
351  down:
352                         putchar('\n'); /* down one line, possibly scroll screen */
353                         G.row++;
354                         if (G.row >= G.height) {
355                                 G.row--;
356                                 redraw_cur_line();
357                         }
358                         if (--cnt)
359                                 goto k_down;
360                         break;
361
362                 case KEYCODE_LEFT:
363                         if (G.half) {
364                                 G.half = 0;
365                                 printf(ESC"[D");
366                                 break;
367                         }
368                         if ((0xf & (uintptr_t)G.current_byte) == 0) {
369                                 /* leftmost pos, wrap to prev line */
370                                 if (G.current_byte == G.addr) {
371                                         move_mapping_lower();
372                                         if (G.current_byte == G.addr)
373                                                 break; /* first line, don't do anything */
374                                 }
375                                 G.half = 1;
376                                 G.current_byte--;
377                                 printf(ESC"[46C"); /* cursor right 3*15 + 1 chars */
378                                 goto up;
379                         }
380                         G.half = 1;
381                         G.current_byte--;
382                         printf(ESC"[2D");
383                         break;
384                 case KEYCODE_PAGEUP:
385                         cnt = G.height;
386                 case KEYCODE_UP:
387  k_up:
388                         if ((G.current_byte - G.addr) < 16) {
389                                 move_mapping_lower();
390                                 if ((G.current_byte - G.addr) < 16)
391                                         break;
392                         }
393                         G.current_byte -= 16;
394  up:
395                         if (G.row != 0) {
396                                 G.row--;
397                                 printf(ESC"[A"); /* up (won't scroll) */
398                         } else {
399                                 //printf(ESC"[T"); /* scroll up */ - not implemented on Linux VT!
400                                 printf(ESC"M"); /* scroll up */
401                                 redraw_cur_line();
402                         }
403                         if (--cnt)
404                                 goto k_up;
405                         break;
406                 case CTRL('X'):
407                         restore_term();
408                         return EXIT_SUCCESS;
409                 } /* switch */
410         } /* for (;;) */
411
412         /* not reached */
413         return EXIT_SUCCESS;
414 }