Serenity Operating System
1/*
2 * Copyright (c) 2021, Daniel Bertalan <dani@danielbertalan.dev>
3 * Copyright (c) 2022, Alex Major
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#define __USE_MISC
9#define TTYDEFCHARS
10#include <AK/DeprecatedString.h>
11#include <AK/Optional.h>
12#include <AK/Result.h>
13#include <AK/ScopeGuard.h>
14#include <AK/StringView.h>
15#include <AK/Vector.h>
16#include <LibCore/System.h>
17#include <LibMain/Main.h>
18#include <ctype.h>
19#include <fcntl.h>
20#include <getopt.h>
21#include <stdio.h>
22#include <sys/ioctl.h>
23#include <sys/ttydefaults.h>
24#include <termios.h>
25#include <unistd.h>
26
27constexpr option long_options[] = {
28 { "all", no_argument, 0, 'a' },
29 { "save", no_argument, 0, 'g' },
30 { "file", required_argument, 0, 'F' },
31 { 0, 0, 0, 0 }
32};
33
34struct TermiosFlag {
35 StringView name;
36 tcflag_t value;
37 tcflag_t mask;
38};
39
40struct BaudRate {
41 speed_t speed;
42 unsigned long numeric_value;
43};
44
45struct ControlCharacter {
46 StringView name;
47 unsigned index;
48};
49
50constexpr TermiosFlag all_iflags[] = {
51 { "ignbrk"sv, IGNBRK, IGNBRK },
52 { "brkint"sv, BRKINT, BRKINT },
53 { "ignpar"sv, IGNPAR, IGNPAR },
54 { "parmer"sv, PARMRK, PARMRK },
55 { "inpck"sv, INPCK, INPCK },
56 { "istrip"sv, ISTRIP, ISTRIP },
57 { "inlcr"sv, INLCR, INLCR },
58 { "igncr"sv, IGNCR, IGNCR },
59 { "icrnl"sv, ICRNL, ICRNL },
60 { "iuclc"sv, IUCLC, IUCLC },
61 { "ixon"sv, IXON, IXON },
62 { "ixany"sv, IXANY, IXANY },
63 { "ixoff"sv, IXOFF, IXOFF },
64 { "imaxbel"sv, IMAXBEL, IMAXBEL },
65 { "iutf8"sv, IUTF8, IUTF8 }
66};
67
68constexpr TermiosFlag all_oflags[] = {
69 { "opost"sv, OPOST, OPOST },
70 { "olcuc"sv, OLCUC, OPOST },
71 { "onlcr"sv, ONLCR, ONLCR },
72 { "onlret"sv, ONLRET, ONLRET },
73 { "ofill"sv, OFILL, OFILL },
74 { "ofdel"sv, OFDEL, OFDEL },
75};
76
77constexpr TermiosFlag all_cflags[] = {
78 { "cs5"sv, CS5, CSIZE },
79 { "cs6"sv, CS6, CSIZE },
80 { "cs7"sv, CS7, CSIZE },
81 { "cs8"sv, CS8, CSIZE },
82 { "cstopb"sv, CSTOPB, CSTOPB },
83 { "cread"sv, CREAD, CREAD },
84 { "parenb"sv, PARENB, PARENB },
85 { "parodd"sv, PARODD, PARODD },
86 { "hupcl"sv, HUPCL, HUPCL },
87 { "clocal"sv, CLOCAL, CLOCAL },
88};
89
90constexpr TermiosFlag all_lflags[] = {
91 { "isig"sv, ISIG, ISIG },
92 { "icanon"sv, ICANON, ICANON },
93 { "echo"sv, ECHO, ECHO },
94 { "echoe"sv, ECHOE, ECHOE },
95 { "echok"sv, ECHOK, ECHOK },
96 { "echonl"sv, ECHONL, ECHONL },
97 { "noflsh"sv, NOFLSH, NOFLSH },
98 { "tostop"sv, TOSTOP, TOSTOP },
99 { "iexten"sv, IEXTEN, IEXTEN }
100};
101
102constexpr BaudRate baud_rates[] = {
103 { B0, 0 },
104 { B50, 50 },
105 { B75, 75 },
106 { B110, 110 },
107 { B134, 134 },
108 { B150, 150 },
109 { B200, 200 },
110 { B300, 300 },
111 { B600, 600 },
112 { B1200, 1200 },
113 { B1800, 1800 },
114 { B2400, 2400 },
115 { B4800, 4800 },
116 { B9600, 9600 },
117 { B19200, 19200 },
118 { B38400, 38400 },
119 { B57600, 57600 },
120 { B115200, 115200 },
121 { B230400, 230400 },
122 { B460800, 460800 },
123 { B500000, 500000 },
124 { B576000, 576000 },
125 { B921600, 921600 },
126 { B1000000, 1000000 },
127 { B1152000, 1152000 },
128 { B1500000, 1500000 },
129 { B2000000, 2000000 },
130 { B2500000, 2500000 },
131 { B3000000, 3000000 },
132 { B3500000, 3500000 },
133 { B4000000, 4000000 }
134};
135
136constexpr ControlCharacter control_characters[] = {
137 { "intr"sv, VINTR },
138 { "quit"sv, VQUIT },
139 { "erase"sv, VERASE },
140 { "kill"sv, VKILL },
141 { "eof"sv, VEOF },
142 /* time and min are handled separately */
143 { "swtc"sv, VSWTC },
144 { "start"sv, VSTART },
145 { "stop"sv, VSTOP },
146 { "susp"sv, VSUSP },
147 { "eol"sv, VEOL },
148 { "reprint"sv, VREPRINT },
149 { "discard"sv, VDISCARD },
150 { "werase"sv, VWERASE },
151 { "lnext"sv, VLNEXT },
152 { "eol2"sv, VEOL2 }
153};
154
155Optional<speed_t> numeric_value_to_speed(unsigned long);
156Optional<unsigned long> speed_to_numeric_value(speed_t);
157
158void print_stty_readable(termios const&);
159void print_human_readable(termios const&, winsize const&, bool);
160Result<void, int> apply_stty_readable_modes(StringView, termios&);
161Result<void, int> apply_modes(size_t, char**, termios&, winsize&);
162
163Optional<speed_t> numeric_value_to_speed(unsigned long numeric_value)
164{
165 for (auto rate : baud_rates) {
166 if (rate.numeric_value == numeric_value)
167 return rate.speed;
168 }
169 return {};
170}
171
172Optional<unsigned long> speed_to_numeric_value(speed_t speed)
173{
174 for (auto rate : baud_rates) {
175 if (rate.speed == speed)
176 return rate.numeric_value;
177 }
178 return {};
179}
180
181void print_stty_readable(termios const& modes)
182{
183 out("{:x}:{:x}:{:x}:{:x}", modes.c_iflag, modes.c_oflag, modes.c_cflag, modes.c_lflag);
184 for (size_t i = 0; i < NCCS; ++i)
185 out(":{:x}", modes.c_cc[i]);
186 out(":{:x}:{:x}\n", modes.c_ispeed, modes.c_ospeed);
187}
188
189void print_human_readable(termios const& modes, winsize const& ws, bool verbose_mode)
190{
191 auto print_speed = [&] {
192 if (verbose_mode && modes.c_ispeed != modes.c_ospeed) {
193 out("ispeed {} baud; ospeed {} baud;", speed_to_numeric_value(modes.c_ispeed).value(), speed_to_numeric_value(modes.c_ospeed).value());
194
195 } else {
196 out("speed {} baud;", speed_to_numeric_value(modes.c_ispeed).value());
197 }
198 };
199
200 auto print_winsize = [&] {
201 out("rows {}; columns {};", ws.ws_row, ws.ws_col);
202 };
203
204 auto escape_character = [&](u8 ch) {
205 StringBuilder sb;
206 if (ch <= 0x20) {
207 sb.append('^');
208 sb.append(ch + 0x40);
209 } else if (ch == 0x7f) {
210 sb.append("^?"sv);
211 } else {
212 sb.append(ch);
213 }
214 return sb.to_deprecated_string();
215 };
216
217 auto print_control_characters = [&] {
218 bool first_in_line = true;
219 for (auto cc : control_characters) {
220 if (verbose_mode || modes.c_cc[cc.index] != ttydefchars[cc.index]) {
221 out("{}{} = {};", (first_in_line) ? "" : " ", cc.name, escape_character(modes.c_cc[cc.index]));
222 first_in_line = false;
223 }
224 }
225 if (!first_in_line)
226 out("\n");
227 };
228
229 auto print_flags_of_type = [&](const TermiosFlag flags[], size_t flag_count, tcflag_t field_value, tcflag_t field_default) {
230 bool first_in_line = true;
231 for (size_t i = 0; i < flag_count; ++i) {
232 auto& flag = flags[i];
233 if (verbose_mode || (field_value & flag.mask) != (field_default & flag.mask)) {
234 bool set = (field_value & flag.mask) == flag.value;
235 out("{}{}{}", first_in_line ? "" : " ", set ? "" : "-", flag.name);
236 first_in_line = false;
237 }
238 }
239 if (!first_in_line)
240 out("\n");
241 };
242
243 auto print_flags = [&] {
244 print_flags_of_type(all_cflags, sizeof(all_cflags) / sizeof(TermiosFlag), modes.c_cflag, TTYDEF_CFLAG);
245 print_flags_of_type(all_oflags, sizeof(all_oflags) / sizeof(TermiosFlag), modes.c_oflag, TTYDEF_OFLAG);
246 print_flags_of_type(all_iflags, sizeof(all_iflags) / sizeof(TermiosFlag), modes.c_iflag, TTYDEF_IFLAG);
247 print_flags_of_type(all_lflags, sizeof(all_lflags) / sizeof(TermiosFlag), modes.c_lflag, TTYDEF_LFLAG);
248 };
249
250 print_speed();
251 out(" ");
252 print_winsize();
253 out("\n");
254 print_control_characters();
255 print_flags();
256}
257
258Result<void, int> apply_stty_readable_modes(StringView mode_string, termios& t)
259{
260 auto split = mode_string.split_view(':');
261 if (split.size() != 4 + NCCS + 2) {
262 warnln("Save string has an incorrect number of parameters");
263 return 1;
264 }
265 auto parse_hex = [&](StringView v) {
266 tcflag_t ret = 0;
267 for (auto c : v) {
268 c = tolower(c);
269 ret *= 16;
270 if (isdigit(c)) {
271 ret += c - '0';
272 } else {
273 VERIFY(c >= 'a' && c <= 'f');
274 ret += c - 'a';
275 }
276 }
277 return ret;
278 };
279
280 t.c_iflag = parse_hex(split[0]);
281 t.c_oflag = parse_hex(split[1]);
282 t.c_cflag = parse_hex(split[2]);
283 t.c_lflag = parse_hex(split[3]);
284 for (size_t i = 0; i < NCCS; ++i) {
285 t.c_cc[i] = (cc_t)parse_hex(split[4 + i]);
286 }
287 t.c_ispeed = parse_hex(split[4 + NCCS]);
288 t.c_ospeed = parse_hex(split[4 + NCCS + 1]);
289 return {};
290}
291
292Result<void, int> apply_modes(size_t parameter_count, char** raw_parameters, termios& t, winsize& w)
293{
294 Vector<StringView> parameters;
295 parameters.ensure_capacity(parameter_count);
296 for (size_t i = 0; i < parameter_count; ++i)
297 parameters.append(StringView { raw_parameters[i], strlen(raw_parameters[i]) });
298
299 auto parse_baud = [&](size_t idx) -> Optional<speed_t> {
300 auto maybe_numeric_value = parameters[idx].to_uint<uint32_t>();
301 if (maybe_numeric_value.has_value())
302 return numeric_value_to_speed(maybe_numeric_value.value());
303 return {};
304 };
305
306 auto parse_number = [&](size_t idx) -> Optional<cc_t> {
307 return parameters[idx].to_uint<cc_t>();
308 };
309
310 auto looks_like_stty_readable = [&](size_t idx) {
311 bool contains_colon = false;
312 for (auto c : parameters[idx]) {
313 c = tolower(c);
314 if (!isdigit(c) && !(c >= 'a' && c <= 'f') && c != ':')
315 return false;
316 if (c == ':')
317 contains_colon = true;
318 }
319 return contains_colon;
320 };
321
322 auto parse_control_character = [&](size_t idx) -> Optional<cc_t> {
323 VERIFY(!parameters[idx].is_empty());
324 if (parameters[idx] == "^-" || parameters[idx] == "undef") {
325 // FIXME: disabling characters is a bit wonky right now in TTY.
326 // We should add the _POSIX_VDISABLE macro.
327 return 0;
328 } else if (parameters[idx][0] == '^' && parameters[idx].length() == 2) {
329 return toupper(parameters[idx][1]) - 0x40;
330 } else if (parameters[idx].starts_with("0x"sv)) {
331 cc_t value = 0;
332 if (parameters[idx].length() == 2) {
333 warnln("Invalid hexadecimal character code {}", parameters[idx]);
334 return {};
335 }
336 for (size_t i = 2; i < parameters[idx].length(); ++i) {
337 char ch = tolower(parameters[idx][i]);
338 if (!isdigit(ch) && !(ch >= 'a' && ch <= 'f')) {
339 warnln("Invalid hexadecimal character code {}", parameters[idx]);
340 return {};
341 }
342 value = 16 * value + (isdigit(ch)) ? (ch - '0') : (ch - 'a');
343 }
344 return value;
345 } else if (parameters[idx].starts_with("0"sv)) {
346 cc_t value = 0;
347 for (size_t i = 1; i < parameters[idx].length(); ++i) {
348 char ch = parameters[idx][i];
349 if (!(ch >= '0' && ch <= '7')) {
350 warnln("Invalid octal character code {}", parameters[idx]);
351 return {};
352 }
353 value = 8 * value + (ch - '0');
354 }
355 return value;
356 } else if (isdigit(parameters[idx][0])) {
357 auto maybe_value = parameters[idx].to_uint<cc_t>();
358 if (!maybe_value.has_value()) {
359 warnln("Invalid decimal character code {}", parameters[idx]);
360 return {};
361 }
362 return maybe_value.value();
363 } else if (parameters[idx].length() == 1) {
364 return parameters[idx][0];
365 }
366 warnln("Invalid control character {}", parameters[idx]);
367 return {};
368 };
369
370 size_t parameter_idx = 0;
371
372 auto parse_flag_or_char = [&]() -> Result<void, int> {
373 if (parameters[parameter_idx][0] != '-') {
374 if (parameters[parameter_idx] == "min") {
375 auto maybe_number = parse_number(++parameter_idx);
376 if (!maybe_number.has_value()) {
377 warnln("Error parsing min: {} is not a number", parameters[parameter_idx]);
378 return 1;
379 }
380 return {};
381 } else if (parameters[parameter_idx] == "time") {
382 auto maybe_number = parse_number(++parameter_idx);
383 if (!maybe_number.has_value()) {
384 warnln("Error parsing time: {} is not a number", parameters[parameter_idx]);
385 return 1;
386 }
387 return {};
388 } else {
389 for (auto cc : control_characters) {
390 if (cc.name == parameters[parameter_idx]) {
391 if (parameter_idx == parameter_count - 1) {
392 warnln("No control character specified for {}", cc.name);
393 return 1;
394 }
395 auto maybe_control_character = parse_control_character(++parameter_idx);
396 if (!maybe_control_character.has_value())
397 return 1;
398 t.c_cc[cc.index] = maybe_control_character.value();
399 return {};
400 }
401 }
402 }
403 }
404
405 // We fall through to here if what we are setting is not a control character.
406 bool negate = false;
407 if (parameters[parameter_idx][0] == '-') {
408 negate = true;
409 parameters[parameter_idx] = parameters[parameter_idx].substring_view(1);
410 }
411
412 auto perform_masking = [&](tcflag_t value, tcflag_t mask, tcflag_t& dest) {
413 if (negate)
414 dest &= ~mask;
415 else
416 dest = (dest & (~mask)) | value;
417 };
418
419 for (auto flag : all_iflags) {
420 if (flag.name == parameters[parameter_idx]) {
421 perform_masking(flag.value, flag.mask, t.c_iflag);
422 return {};
423 }
424 }
425 for (auto flag : all_oflags) {
426 if (flag.name == parameters[parameter_idx]) {
427 perform_masking(flag.value, flag.mask, t.c_oflag);
428 return {};
429 }
430 }
431 for (auto flag : all_cflags) {
432 if (flag.name == parameters[parameter_idx]) {
433 perform_masking(flag.value, flag.mask, t.c_cflag);
434 return {};
435 }
436 }
437 for (auto flag : all_lflags) {
438 if (flag.name == parameters[parameter_idx]) {
439 perform_masking(flag.value, flag.mask, t.c_lflag);
440 return {};
441 }
442 }
443 warnln("Invalid control flag or control character name {}", parameters[parameter_idx]);
444 return 1;
445 };
446
447 while (parameter_idx < parameter_count) {
448 if (looks_like_stty_readable(parameter_idx)) {
449 auto maybe_error = apply_stty_readable_modes(parameters[parameter_idx], t);
450 if (maybe_error.is_error())
451 return maybe_error.error();
452 } else if (isdigit(parameters[parameter_idx][0])) {
453 auto new_baud = parse_baud(parameter_idx);
454 if (!new_baud.has_value()) {
455 warnln("Invalid baud rate {}", parameters[parameter_idx]);
456 return 1;
457 }
458 t.c_ispeed = t.c_ospeed = new_baud.value();
459 } else if (parameters[parameter_idx] == "ispeed") {
460 if (parameter_idx == parameter_count - 1) {
461 warnln("No baud rate specified for ispeed");
462 return 1;
463 }
464 auto new_baud = parse_baud(++parameter_idx);
465 if (!new_baud.has_value()) {
466 warnln("Invalid input baud rate {}", parameters[parameter_idx]);
467 return 1;
468 }
469 t.c_ispeed = new_baud.value();
470 } else if (parameters[parameter_idx] == "ospeed") {
471 if (parameter_idx == parameter_count - 1) {
472 warnln("No baud rate specified for ospeed");
473 return 1;
474 }
475 auto new_baud = parse_baud(++parameter_idx);
476 if (!new_baud.has_value()) {
477 warnln("Invalid output baud rate {}", parameters[parameter_idx]);
478 return 1;
479 }
480 t.c_ospeed = new_baud.value();
481 } else if (parameters[parameter_idx] == "columns" || parameters[parameter_idx] == "cols") {
482 auto maybe_number = parse_number(++parameter_idx);
483 if (!maybe_number.has_value()) {
484 warnln("Invalid column count {}", parameters[parameter_idx]);
485 return 1;
486 }
487 w.ws_col = maybe_number.value();
488 } else if (parameters[parameter_idx] == "rows") {
489 auto maybe_number = parse_number(++parameter_idx);
490 if (!maybe_number.has_value()) {
491 warnln("Invalid row count {}", parameters[parameter_idx]);
492 return 1;
493 }
494 w.ws_row = maybe_number.value();
495 } else if (parameters[parameter_idx] == "evenp" || parameters[parameter_idx] == "parity") {
496 t.c_cflag &= ~(CSIZE | PARODD);
497 t.c_cflag |= CS7 | PARENB;
498 } else if (parameters[parameter_idx] == "oddp") {
499 t.c_cflag &= ~(CSIZE);
500 t.c_cflag |= CS7 | PARENB | PARODD;
501 } else if (parameters[parameter_idx] == "-parity" || parameters[parameter_idx] == "-evenp" || parameters[parameter_idx] == "-oddp") {
502 t.c_cflag &= ~(PARENB | CSIZE);
503 t.c_cflag |= CS8;
504 } else if (parameters[parameter_idx] == "raw") {
505 cfmakeraw(&t);
506 } else if (parameters[parameter_idx] == "nl") {
507 t.c_iflag &= ~ICRNL;
508 } else if (parameters[parameter_idx] == "-nl") {
509 t.c_cflag &= ~(INLCR & IGNCR);
510 t.c_iflag |= ICRNL;
511 } else if (parameters[parameter_idx] == "ek") {
512 t.c_cc[VERASE] = CERASE;
513 t.c_cc[VKILL] = CKILL;
514 } else if (parameters[parameter_idx] == "sane") {
515 t.c_iflag = TTYDEF_IFLAG;
516 t.c_oflag = TTYDEF_OFLAG;
517 t.c_cflag = TTYDEF_CFLAG;
518 t.c_lflag = TTYDEF_LFLAG;
519 for (size_t i = 0; i < NCCS; ++i)
520 t.c_cc[i] = ttydefchars[i];
521 t.c_ispeed = t.c_ospeed = TTYDEF_SPEED;
522 } else {
523 auto maybe_error = parse_flag_or_char();
524 if (maybe_error.is_error())
525 return maybe_error.error();
526 }
527 ++parameter_idx;
528 }
529 return {};
530}
531
532ErrorOr<int> serenity_main(Main::Arguments arguments)
533{
534 TRY(Core::System::pledge("stdio tty rpath"));
535 TRY(Core::System::unveil("/dev", "r"));
536 TRY(Core::System::unveil(nullptr, nullptr));
537
538 DeprecatedString device_file;
539 bool stty_readable = false;
540 bool all_settings = false;
541
542 // Core::ArgsParser can't handle the weird syntax of stty, so we use getopt_long instead.
543 int argc = arguments.argc;
544 char** argv = arguments.argv;
545 opterr = 0; // We handle unknown flags gracefully by starting to parse the arguments in `apply_modes`.
546 int optc;
547 bool should_quit = false;
548 while (!should_quit && ((optc = getopt_long(argc, argv, "-agF:", long_options, nullptr)) != -1)) {
549 switch (optc) {
550 case 'a':
551 all_settings = true;
552 break;
553 case 'g':
554 stty_readable = true;
555 break;
556 case 'F':
557 if (!device_file.is_empty()) {
558 warnln("Only one device may be specified");
559 exit(1);
560 }
561 device_file = optarg;
562 break;
563 default:
564 should_quit = true;
565 break;
566 }
567 }
568
569 if (stty_readable && all_settings) {
570 warnln("Save mode and all-settings mode are mutually exclusive");
571 exit(1);
572 }
573
574 int terminal_fd = STDIN_FILENO;
575 if (!device_file.is_empty()) {
576 if ((terminal_fd = open(device_file.characters(), O_RDONLY, 0)) < 0) {
577 perror("open");
578 exit(1);
579 }
580 }
581
582 ScopeGuard file_close_guard = [&] { close(terminal_fd); };
583
584 termios initial_termios = TRY(Core::System::tcgetattr(terminal_fd));
585
586 winsize initial_winsize;
587 TRY(Core::System::ioctl(terminal_fd, TIOCGWINSZ, &initial_winsize));
588
589 if (optind < argc) {
590 if (stty_readable || all_settings) {
591 warnln("Modes cannot be set when printing settings");
592 exit(1);
593 }
594
595 auto result = apply_modes(argc - optind, argv + optind, initial_termios, initial_winsize);
596 if (result.is_error())
597 return result.error();
598
599 TRY(Core::System::tcsetattr(terminal_fd, TCSADRAIN, initial_termios));
600 TRY(Core::System::ioctl(terminal_fd, TIOCSWINSZ, &initial_winsize));
601
602 } else if (stty_readable) {
603 print_stty_readable(initial_termios);
604 } else {
605 print_human_readable(initial_termios, initial_winsize, all_settings);
606 }
607 return 0;
608}