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