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