1 #include "git-compat-util.h"
2 #include "compat/terminal.h"
3 #include "sigchain.h"
4 #include "strbuf.h"
5 #include "run-command.h"
6 #include "string-list.h"
7 #include "hashmap.h"
8 
9 #if defined(HAVE_DEV_TTY) || defined(GIT_WINDOWS_NATIVE)
10 
restore_term_on_signal(int sig)11 static void restore_term_on_signal(int sig)
12 {
13 	restore_term();
14 	sigchain_pop(sig);
15 	raise(sig);
16 }
17 
18 #ifdef HAVE_DEV_TTY
19 
20 #define INPUT_PATH "/dev/tty"
21 #define OUTPUT_PATH "/dev/tty"
22 
23 static int term_fd = -1;
24 static struct termios old_term;
25 
restore_term(void)26 void restore_term(void)
27 {
28 	if (term_fd < 0)
29 		return;
30 
31 	tcsetattr(term_fd, TCSAFLUSH, &old_term);
32 	close(term_fd);
33 	term_fd = -1;
34 }
35 
save_term(int full_duplex)36 int save_term(int full_duplex)
37 {
38 	if (term_fd < 0)
39 		term_fd = open("/dev/tty", O_RDWR);
40 
41 	return (term_fd < 0) ? -1 : tcgetattr(term_fd, &old_term);
42 }
43 
disable_bits(tcflag_t bits)44 static int disable_bits(tcflag_t bits)
45 {
46 	struct termios t;
47 
48 	if (save_term(0) < 0)
49 		goto error;
50 
51 	t = old_term;
52 	sigchain_push_common(restore_term_on_signal);
53 
54 	t.c_lflag &= ~bits;
55 	if (!tcsetattr(term_fd, TCSAFLUSH, &t))
56 		return 0;
57 
58 error:
59 	close(term_fd);
60 	term_fd = -1;
61 	return -1;
62 }
63 
disable_echo(void)64 static int disable_echo(void)
65 {
66 	return disable_bits(ECHO);
67 }
68 
enable_non_canonical(void)69 static int enable_non_canonical(void)
70 {
71 	return disable_bits(ICANON | ECHO);
72 }
73 
74 #elif defined(GIT_WINDOWS_NATIVE)
75 
76 #define INPUT_PATH "CONIN$"
77 #define OUTPUT_PATH "CONOUT$"
78 #define FORCE_TEXT "t"
79 
80 static int use_stty = 1;
81 static struct string_list stty_restore = STRING_LIST_INIT_DUP;
82 static HANDLE hconin = INVALID_HANDLE_VALUE;
83 static HANDLE hconout = INVALID_HANDLE_VALUE;
84 static DWORD cmode_in, cmode_out;
85 
restore_term(void)86 void restore_term(void)
87 {
88 	if (use_stty) {
89 		int i;
90 		struct child_process cp = CHILD_PROCESS_INIT;
91 
92 		if (stty_restore.nr == 0)
93 			return;
94 
95 		strvec_push(&cp.args, "stty");
96 		for (i = 0; i < stty_restore.nr; i++)
97 			strvec_push(&cp.args, stty_restore.items[i].string);
98 		run_command(&cp);
99 		string_list_clear(&stty_restore, 0);
100 		return;
101 	}
102 
103 	if (hconin == INVALID_HANDLE_VALUE)
104 		return;
105 
106 	SetConsoleMode(hconin, cmode_in);
107 	CloseHandle(hconin);
108 	if (cmode_out) {
109 		assert(hconout != INVALID_HANDLE_VALUE);
110 		SetConsoleMode(hconout, cmode_out);
111 		CloseHandle(hconout);
112 	}
113 
114 	hconin = hconout = INVALID_HANDLE_VALUE;
115 }
116 
save_term(int full_duplex)117 int save_term(int full_duplex)
118 {
119 	hconin = CreateFileA("CONIN$", GENERIC_READ | GENERIC_WRITE,
120 	    FILE_SHARE_READ, NULL, OPEN_EXISTING,
121 	    FILE_ATTRIBUTE_NORMAL, NULL);
122 	if (hconin == INVALID_HANDLE_VALUE)
123 		return -1;
124 
125 	if (full_duplex) {
126 		hconout = CreateFileA("CONOUT$", GENERIC_READ | GENERIC_WRITE,
127 			FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
128 			FILE_ATTRIBUTE_NORMAL, NULL);
129 		if (hconout == INVALID_HANDLE_VALUE)
130 			goto error;
131 
132 		GetConsoleMode(hconout, &cmode_out);
133 	}
134 
135 	GetConsoleMode(hconin, &cmode_in);
136 	use_stty = 0;
137 	return 0;
138 error:
139 	CloseHandle(hconin);
140 	hconin = INVALID_HANDLE_VALUE;
141 	return -1;
142 }
143 
disable_bits(DWORD bits)144 static int disable_bits(DWORD bits)
145 {
146 	if (use_stty) {
147 		struct child_process cp = CHILD_PROCESS_INIT;
148 
149 		strvec_push(&cp.args, "stty");
150 
151 		if (bits & ENABLE_LINE_INPUT) {
152 			string_list_append(&stty_restore, "icanon");
153 			strvec_push(&cp.args, "-icanon");
154 		}
155 
156 		if (bits & ENABLE_ECHO_INPUT) {
157 			string_list_append(&stty_restore, "echo");
158 			strvec_push(&cp.args, "-echo");
159 		}
160 
161 		if (bits & ENABLE_PROCESSED_INPUT) {
162 			string_list_append(&stty_restore, "-ignbrk");
163 			string_list_append(&stty_restore, "intr");
164 			string_list_append(&stty_restore, "^c");
165 			strvec_push(&cp.args, "ignbrk");
166 			strvec_push(&cp.args, "intr");
167 			strvec_push(&cp.args, "");
168 		}
169 
170 		if (run_command(&cp) == 0)
171 			return 0;
172 
173 		/* `stty` could not be executed; access the Console directly */
174 		use_stty = 0;
175 	}
176 
177 	if (save_term(0) < 0)
178 		return -1;
179 
180 	sigchain_push_common(restore_term_on_signal);
181 	if (!SetConsoleMode(hconin, cmode_in & ~bits)) {
182 		CloseHandle(hconin);
183 		hconin = INVALID_HANDLE_VALUE;
184 		return -1;
185 	}
186 
187 	return 0;
188 }
189 
disable_echo(void)190 static int disable_echo(void)
191 {
192 	return disable_bits(ENABLE_ECHO_INPUT);
193 }
194 
enable_non_canonical(void)195 static int enable_non_canonical(void)
196 {
197 	return disable_bits(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);
198 }
199 
200 /*
201  * Override `getchar()`, as the default implementation does not use
202  * `ReadFile()`.
203  *
204  * This poses a problem when we want to see whether the standard
205  * input has more characters, as the default of Git for Windows is to start the
206  * Bash in a MinTTY, which uses a named pipe to emulate a pty, in which case
207  * our `poll()` emulation calls `PeekNamedPipe()`, which seems to require
208  * `ReadFile()` to be called first to work properly (it only reports 0
209  * available bytes, otherwise).
210  *
211  * So let's just override `getchar()` with a version backed by `ReadFile()` and
212  * go our merry ways from here.
213  */
mingw_getchar(void)214 static int mingw_getchar(void)
215 {
216 	DWORD read = 0;
217 	unsigned char ch;
218 
219 	if (!ReadFile(GetStdHandle(STD_INPUT_HANDLE), &ch, 1, &read, NULL))
220 		return EOF;
221 
222 	if (!read) {
223 		error("Unexpected 0 read");
224 		return EOF;
225 	}
226 
227 	return ch;
228 }
229 #define getchar mingw_getchar
230 
231 #endif
232 
233 #ifndef FORCE_TEXT
234 #define FORCE_TEXT
235 #endif
236 
git_terminal_prompt(const char * prompt,int echo)237 char *git_terminal_prompt(const char *prompt, int echo)
238 {
239 	static struct strbuf buf = STRBUF_INIT;
240 	int r;
241 	FILE *input_fh, *output_fh;
242 
243 	input_fh = fopen(INPUT_PATH, "r" FORCE_TEXT);
244 	if (!input_fh)
245 		return NULL;
246 
247 	output_fh = fopen(OUTPUT_PATH, "w" FORCE_TEXT);
248 	if (!output_fh) {
249 		fclose(input_fh);
250 		return NULL;
251 	}
252 
253 	if (!echo && disable_echo()) {
254 		fclose(input_fh);
255 		fclose(output_fh);
256 		return NULL;
257 	}
258 
259 	fputs(prompt, output_fh);
260 	fflush(output_fh);
261 
262 	r = strbuf_getline_lf(&buf, input_fh);
263 	if (!echo) {
264 		putc('\n', output_fh);
265 		fflush(output_fh);
266 	}
267 
268 	restore_term();
269 	fclose(input_fh);
270 	fclose(output_fh);
271 
272 	if (r == EOF)
273 		return NULL;
274 	return buf.buf;
275 }
276 
277 /*
278  * The `is_known_escape_sequence()` function returns 1 if the passed string
279  * corresponds to an Escape sequence that the terminal capabilities contains.
280  *
281  * To avoid depending on ncurses or other platform-specific libraries, we rely
282  * on the presence of the `infocmp` executable to do the job for us (failing
283  * silently if the program is not available or refused to run).
284  */
285 struct escape_sequence_entry {
286 	struct hashmap_entry entry;
287 	char sequence[FLEX_ARRAY];
288 };
289 
sequence_entry_cmp(const void * hashmap_cmp_fn_data,const struct escape_sequence_entry * e1,const struct escape_sequence_entry * e2,const void * keydata)290 static int sequence_entry_cmp(const void *hashmap_cmp_fn_data,
291 			      const struct escape_sequence_entry *e1,
292 			      const struct escape_sequence_entry *e2,
293 			      const void *keydata)
294 {
295 	return strcmp(e1->sequence, keydata ? keydata : e2->sequence);
296 }
297 
is_known_escape_sequence(const char * sequence)298 static int is_known_escape_sequence(const char *sequence)
299 {
300 	static struct hashmap sequences;
301 	static int initialized;
302 
303 	if (!initialized) {
304 		struct child_process cp = CHILD_PROCESS_INIT;
305 		struct strbuf buf = STRBUF_INIT;
306 		char *p, *eol;
307 
308 		hashmap_init(&sequences, (hashmap_cmp_fn)sequence_entry_cmp,
309 			     NULL, 0);
310 
311 		strvec_pushl(&cp.args, "infocmp", "-L", "-1", NULL);
312 		if (pipe_command(&cp, NULL, 0, &buf, 0, NULL, 0))
313 			strbuf_setlen(&buf, 0);
314 
315 		for (eol = p = buf.buf; *p; p = eol + 1) {
316 			p = strchr(p, '=');
317 			if (!p)
318 				break;
319 			p++;
320 			eol = strchrnul(p, '\n');
321 
322 			if (starts_with(p, "\\E")) {
323 				char *comma = memchr(p, ',', eol - p);
324 				struct escape_sequence_entry *e;
325 
326 				p[0] = '^';
327 				p[1] = '[';
328 				FLEX_ALLOC_MEM(e, sequence, p, comma - p);
329 				hashmap_entry_init(&e->entry,
330 						   strhash(e->sequence));
331 				hashmap_add(&sequences, &e->entry);
332 			}
333 			if (!*eol)
334 				break;
335 		}
336 		initialized = 1;
337 	}
338 
339 	return !!hashmap_get_from_hash(&sequences, strhash(sequence), sequence);
340 }
341 
read_key_without_echo(struct strbuf * buf)342 int read_key_without_echo(struct strbuf *buf)
343 {
344 	static int warning_displayed;
345 	int ch;
346 
347 	if (warning_displayed || enable_non_canonical() < 0) {
348 		if (!warning_displayed) {
349 			warning("reading single keystrokes not supported on "
350 				"this platform; reading line instead");
351 			warning_displayed = 1;
352 		}
353 
354 		return strbuf_getline(buf, stdin);
355 	}
356 
357 	strbuf_reset(buf);
358 	ch = getchar();
359 	if (ch == EOF) {
360 		restore_term();
361 		return EOF;
362 	}
363 	strbuf_addch(buf, ch);
364 
365 	if (ch == '\033' /* ESC */) {
366 		/*
367 		 * We are most likely looking at an Escape sequence. Let's try
368 		 * to read more bytes, waiting at most half a second, assuming
369 		 * that the sequence is complete if we did not receive any byte
370 		 * within that time.
371 		 *
372 		 * Start by replacing the Escape byte with ^[ */
373 		strbuf_splice(buf, buf->len - 1, 1, "^[", 2);
374 
375 		/*
376 		 * Query the terminal capabilities once about all the Escape
377 		 * sequences it knows about, so that we can avoid waiting for
378 		 * half a second when we know that the sequence is complete.
379 		 */
380 		while (!is_known_escape_sequence(buf->buf)) {
381 			struct pollfd pfd = { .fd = 0, .events = POLLIN };
382 
383 			if (poll(&pfd, 1, 500) < 1)
384 				break;
385 
386 			ch = getchar();
387 			if (ch == EOF)
388 				return 0;
389 			strbuf_addch(buf, ch);
390 		}
391 	}
392 
393 	restore_term();
394 	return 0;
395 }
396 
397 #else
398 
save_term(int full_duplex)399 int save_term(int full_duplex)
400 {
401 	/* full_duplex == 1, but no support available */
402 	return -full_duplex;
403 }
404 
restore_term(void)405 void restore_term(void)
406 {
407 }
408 
git_terminal_prompt(const char * prompt,int echo)409 char *git_terminal_prompt(const char *prompt, int echo)
410 {
411 	return getpass(prompt);
412 }
413 
read_key_without_echo(struct strbuf * buf)414 int read_key_without_echo(struct strbuf *buf)
415 {
416 	static int warning_displayed;
417 	const char *res;
418 
419 	if (!warning_displayed) {
420 		warning("reading single keystrokes not supported on this "
421 			"platform; reading line instead");
422 		warning_displayed = 1;
423 	}
424 
425 	res = getpass("");
426 	strbuf_reset(buf);
427 	if (!res)
428 		return EOF;
429 	strbuf_addstr(buf, res);
430 	return 0;
431 }
432 
433 #endif
434