/** * tcsgrep.c - match input against terminal control sequence patterns * * Copyright (C) 2020,2021 CismonX * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifdef HAVE_CONFIG_H # include "config.h" #endif // HAVE_CONFIG_H #include #include #include #include #include #include #include #include #include #define NINTH_ARG_(a1, a2, a3, a4, a5, a6, a7, a8, a9, ...) a9 #define COUNT_ARGS_(...) NINTH_ARG_(__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0) #define CONCAT_1_(a1) a1 #define CONCAT_2_(a1, a2) a1 a2 #define CONCAT_3_(a1, a2, a3) a1 a2 a3 #define CONCAT_4_(a1, a2, a3, a4) a1 a2 a3 a4 #define CONCAT_5_(a1, a2, a3, a4, a5) a1 a2 a3 a4 a5 #define CONCAT_6_(a1, a2, a3, a4, a5, a6) a1 a2 a3 a4 a5 a6 #define CONCAT_7_(a1, a2, a3, a4, a5, a6, a7) a1 a2 a3 a4 a5 a6 a7 #define CONCAT_8_(a1, a2, a3, a4, a5, a6, a7, a8) a1 a2 a3 a4 a5 a6 a7 a8 #define CONCAT_(nargs) CONCAT_##nargs##_ #define DEFARGS_(nargs, ...) CONCAT_(nargs)(__VA_ARGS__) #define DEFSEQ_NOARGS(name) { #name, CTLSEQS_##name(), "" } #define DEFSEQ(name, ...) \ { #name, CTLSEQS_##name(__VA_ARGS__), DEFARGS_(COUNT_ARGS_(__VA_ARGS__), __VA_ARGS__) } #define DEFAULT_MAX_BUFFER_LEN 4096 struct tcsgrep_sequence { char const *name; char const *pattern; char const args[8]; }; struct tcsgrep_ctx { char const *prog_name; int timeout; int limit; bool purge_long_seqs; bool verbose; bool not_tty; }; static inline void print_error(struct tcsgrep_ctx const *ctx, char const *msg) { dprintf(STDERR_FILENO, "%s: [error] %s.\n", ctx->prog_name, msg); } static inline bool parse_int(char const *str, int *dest) { errno = 0; unsigned long result = strtoul(str, NULL, 10); if (errno || result > 4096) { return false; } *dest = result; return true; } static inline void print_char(int ch) { static char const *ascii_table[] = { "NUL", "SOH", "STX", "ETX", "EOT", "ENQ", "ACK", "BEL", "BS", "HT", "LF", "VT", "FF", "CR", "SO", "SI", "DLE", "DC1", "DC2", "DC3", "DC4", "NAK", "SYN", "ETB", "CAN", "EM", "SUB", "ESC", "FS", "GS", "RS", "US" }; if (ch == ' ') { printf(" SP"); } else if (isprint(ch)) { printf(" %c", ch); } else if (ch == 0x7f) { printf(" DEL"); } else if (!iscntrl(ch)) { printf(" \\x%02x", (unsigned char)ch); } else { printf(" %s", ascii_table[ch]); } } static void print_generic_seq(char const *header, union ctlseqs_value *result, bool newline) { size_t length = result[0].len; char const *seq = result[1].str; printf("%s %zu", header, length); for (size_t idx = 0; idx < length; ++idx) { print_char((unsigned)seq[idx]); } if (newline) { printf("\n"); } } static void print_matching_seq(struct tcsgrep_sequence *seq, union ctlseqs_value *result, bool verbose) { if (verbose) { print_generic_seq("OK", result, false); result += 2; } else { printf("OK"); } printf(" %s", seq->name); for (int idx = 0; idx < 8; ++idx) { char placeholder = seq->args[idx]; switch (placeholder) { case 0x0e: // CTLSEQS_PH_NUM printf(" %lu", result[idx].num); break; case 0x10: // CTLSEQS_PH_STR printf(" %.*s", (int)result[idx].len, result[idx + 1].str); break; case 0x0f: // CTLSEQS_PH_NUMS for (size_t i = 1; i <= result[idx].len; ++i) { printf(" %lu", result[idx + i].num); } break; } } printf("\n"); } int main(int argc, char **argv) { struct tcsgrep_ctx ctx = { .prog_name = argv[0], .timeout = -1, .limit = DEFAULT_MAX_BUFFER_LEN, }; int opt; while (-1 != (opt = getopt(argc, argv, "t:l:pv"))) { switch (opt) { case 't': if (!parse_int(optarg, &ctx.timeout)) { print_error(&ctx, "invalid timeout option"); return 1; } break; case 'l': if (!parse_int(optarg, &ctx.limit)) { print_error(&ctx, "invalid limit option"); return 1; } break; case 'p': ctx.purge_long_seqs = true; break; case 'v': ctx.verbose = true; break; case '?': default: print_error(&ctx, "invalid arguments"); return 1; } } int flags = fcntl(STDIN_FILENO, F_GETFL); if (flags == -1) { print_error(&ctx, "failed to get file status flags"); return 1; } if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) { print_error(&ctx, "failed to set file status flags"); return 1; } struct ctlseqs_matcher *matcher = ctlseqs_matcher_init(); if (matcher == NULL) { print_error(&ctx, "failed to initialize matcher"); return 1; } static struct tcsgrep_sequence seqs[] = { DEFSEQ_NOARGS(S7C1T), DEFSEQ_NOARGS(S8C1T), DEFSEQ_NOARGS(DECDHL_TOP), DEFSEQ_NOARGS(DECDHL_BOTOM), DEFSEQ_NOARGS(DECSWL), DEFSEQ_NOARGS(DECDWL), DEFSEQ_NOARGS(DECALN), DEFSEQ_NOARGS(DECBI), DEFSEQ_NOARGS(DECSC), DEFSEQ_NOARGS(DECRC), DEFSEQ_NOARGS(DECFI), DEFSEQ_NOARGS(DECKPAM), DEFSEQ_NOARGS(DECKPNM), DEFSEQ_NOARGS(RIS), DEFSEQ(DECUDK, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), DEFSEQ(DECRQSS, CTLSEQS_PH_STR), DEFSEQ(DECRSPS, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), DEFSEQ(XTGETXRES, CTLSEQS_PH_STR), DEFSEQ(XTSETTCAP, CTLSEQS_PH_STR), DEFSEQ(XTGETTCAP, CTLSEQS_PH_STR), DEFSEQ(ICH, CTLSEQS_PH_NUM), DEFSEQ(SL, CTLSEQS_PH_NUM), DEFSEQ(CUU, CTLSEQS_PH_NUM), DEFSEQ(CUD, CTLSEQS_PH_NUM), DEFSEQ(CUF, CTLSEQS_PH_NUM), DEFSEQ(CUB, CTLSEQS_PH_NUM), DEFSEQ(CNL, CTLSEQS_PH_NUM), DEFSEQ(CPL, CTLSEQS_PH_NUM), DEFSEQ(CHA, CTLSEQS_PH_NUM), DEFSEQ(CUP, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(CHT, CTLSEQS_PH_NUM), DEFSEQ(ED, CTLSEQS_PH_NUM), DEFSEQ(DECSED, CTLSEQS_PH_NUM), DEFSEQ(EL, CTLSEQS_PH_NUM), DEFSEQ(DECSEL, CTLSEQS_PH_NUM), DEFSEQ(IL, CTLSEQS_PH_NUM), DEFSEQ(DL, CTLSEQS_PH_NUM), DEFSEQ(DCH, CTLSEQS_PH_NUM), DEFSEQ(SU, CTLSEQS_PH_NUM), DEFSEQ(XTSMGRAPHICS, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUMS), DEFSEQ(SD, CTLSEQS_PH_NUM), DEFSEQ(XTHIMOUSE, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(XTRMTITLE, CTLSEQS_PH_STR), DEFSEQ(ECH, CTLSEQS_PH_NUM), DEFSEQ(CBT, CTLSEQS_PH_NUM), DEFSEQ(HPA, CTLSEQS_PH_NUM), DEFSEQ(HPR, CTLSEQS_PH_NUM), DEFSEQ(REP, CTLSEQS_PH_NUM), DEFSEQ_NOARGS(PRIMARY_DA), DEFSEQ_NOARGS(TERTIARY_DA), DEFSEQ_NOARGS(SECONDARY_DA), DEFSEQ(VPA, CTLSEQS_PH_NUM), DEFSEQ(VPR, CTLSEQS_PH_NUM), DEFSEQ(HVP, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(TBC, CTLSEQS_PH_NUM), DEFSEQ(SM, CTLSEQS_PH_NUM), DEFSEQ(DECSET, CTLSEQS_PH_NUM), DEFSEQ(MC, CTLSEQS_PH_NUM), DEFSEQ(MC_DEC, CTLSEQS_PH_NUM), DEFSEQ(RM, CTLSEQS_PH_NUM), DEFSEQ(DECRST, CTLSEQS_PH_NUM), DEFSEQ(SGR, CTLSEQS_PH_NUM), DEFSEQ(XTMODKEYS, CTLSEQS_PH_NUMS), DEFSEQ(DSR, CTLSEQS_PH_NUM), DEFSEQ(XTMODKEYS_DISABLE, CTLSEQS_PH_NUM), DEFSEQ(DSR_DEC, CTLSEQS_PH_NUM), DEFSEQ(XTSMPOINTER, CTLSEQS_PH_NUM), DEFSEQ_NOARGS(DECSTR), DEFSEQ(DECSCL, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(DECRQM_ANSI, CTLSEQS_PH_NUM), DEFSEQ(DECRQM, CTLSEQS_PH_NUM), DEFSEQ_NOARGS(XTVERSION), DEFSEQ(DECLL, CTLSEQS_PH_NUM), DEFSEQ(DECSCUSR, CTLSEQS_PH_NUM), DEFSEQ(DECSCA, CTLSEQS_PH_NUM), DEFSEQ(DECSTBM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(XTRESTORE, CTLSEQS_PH_NUMS), DEFSEQ(DECCARA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ_NOARGS(SCOSC), DEFSEQ(DECSLRM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(XTSAVE, CTLSEQS_PH_NUMS), DEFSEQ(XTWINOPS, CTLSEQS_PH_NUMS), DEFSEQ(XTSMTITLE, CTLSEQS_PH_NUMS), DEFSEQ(DECSWBV, CTLSEQS_PH_NUM), DEFSEQ(DECRARA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ_NOARGS(SCORC), DEFSEQ(DECSMBV, CTLSEQS_PH_NUM), DEFSEQ(DECCRA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(DECRQPSR, CTLSEQS_PH_NUM), DEFSEQ(DECEFR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(DECREQTPARM, CTLSEQS_PH_NUM), DEFSEQ(DECEFR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(DECREQTPARM, CTLSEQS_PH_NUM), DEFSEQ(DECSACE, CTLSEQS_PH_NUM), DEFSEQ(DECFRA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(XTCHECKSUM, CTLSEQS_PH_NUM), DEFSEQ(DECRQCRA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(DECELR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(DECERA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(DECSLE, CTLSEQS_PH_NUMS), DEFSEQ(XTPUSHSGR, CTLSEQS_PH_NUMS), DEFSEQ(DECSERA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(XTREPORTSGR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(DECSCPP, CTLSEQS_PH_NUM), DEFSEQ_NOARGS(DECRQLP), DEFSEQ(DECSNLS, CTLSEQS_PH_NUM), DEFSEQ_NOARGS(XTPOPSGR), DEFSEQ(DECIC, CTLSEQS_PH_NUM), DEFSEQ(DECDC, CTLSEQS_PH_NUM), DEFSEQ(OSC_SEQ, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), DEFSEQ(RESP_DECRQSS, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), DEFSEQ(RESP_XTGETXRES, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), DEFSEQ(RESP_XTGETTCAP, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), DEFSEQ(RESP_PRIMARY_DA, CTLSEQS_PH_NUMS), DEFSEQ(RESP_SECONDARY_DA, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(RESP_DECXCPR, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(RESP_DSR, CTLSEQS_PH_NUMS), DEFSEQ(RESP_DECRQM_ANSI, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(RESP_DECRQM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ(RESP_XTVERSION, CTLSEQS_PH_STR), DEFSEQ(RESP_DECCIR, CTLSEQS_PH_STR), DEFSEQ(RESP_DECTABSR, CTLSEQS_PH_STR), DEFSEQ(RESP_DECRQCRA, CTLSEQS_PH_NUM, CTLSEQS_PH_STR), DEFSEQ(RESP_DECRQLP, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM, CTLSEQS_PH_NUM), DEFSEQ_NOARGS(KEY_UP), DEFSEQ_NOARGS(KEY_DOWN), DEFSEQ_NOARGS(KEY_RIGHT), DEFSEQ_NOARGS(KEY_LEFT), DEFSEQ_NOARGS(KEY_HOME), DEFSEQ_NOARGS(KEY_END), DEFSEQ_NOARGS(KEY_INSERT), DEFSEQ_NOARGS(KEY_DELETE), DEFSEQ_NOARGS(KEY_PGUP), DEFSEQ_NOARGS(KEY_PGDN), DEFSEQ_NOARGS(KEY_F1), DEFSEQ_NOARGS(KEY_F2), DEFSEQ_NOARGS(KEY_F3), DEFSEQ_NOARGS(KEY_F4), DEFSEQ_NOARGS(KEY_F5), DEFSEQ_NOARGS(KEY_F6), DEFSEQ_NOARGS(KEY_F7), DEFSEQ_NOARGS(KEY_F8), DEFSEQ_NOARGS(KEY_F9), DEFSEQ_NOARGS(KEY_F10), DEFSEQ_NOARGS(KEY_F11), DEFSEQ_NOARGS(KEY_F12), }; size_t npatterns = sizeof(seqs) / sizeof(struct tcsgrep_sequence); char const **patterns = malloc(sizeof(char const *) * npatterns); if (patterns == NULL) { print_error(&ctx, "failed to allocate memory"); return 1; } for (size_t idx = 0; idx < npatterns; ++idx) { patterns[idx] = seqs[idx].pattern; } struct ctlseqs_matcher_options matcher_options = { .patterns = patterns, .npatterns = npatterns, }; if (ctlseqs_matcher_config(matcher, &matcher_options) != CTLSEQS_OK) { print_error(&ctx, "matcher setopt failed"); return 1; } struct ctlseqs_reader *reader = ctlseqs_reader_init(); if (reader == NULL) { print_error(&ctx, "failed to initialize reader"); } static union ctlseqs_value result[4096]; struct ctlseqs_reader_options reader_options = { .fd = STDIN_FILENO, .maxlen = ctx.limit, .result = result, .flags = ctx.verbose ? CTLSEQS_READER_SAVE_MATCHED_SEQS : 0, }; if (ctlseqs_reader_config(reader, &reader_options) != CTLSEQS_OK) { print_error(&ctx, "reader setopt failed"); return 1; } struct termios old_termios; if (tcgetattr(STDIN_FILENO, &old_termios) != 0) { ctx.not_tty = true; } else { struct termios new_termios = old_termios; new_termios.c_cc[VMIN] = 0; new_termios.c_cc[VTIME] = 0; new_termios.c_lflag &= ~(ICANON | ISIG | ECHO); if (tcsetattr(STDIN_FILENO, TCSANOW, &new_termios) != 0) { print_error(&ctx, "failed to set terminal attributes"); return 1; } } int status = 0; while (true) { ssize_t retval = ctlseqs_read(reader, matcher, ctx.timeout); switch (retval) { case CTLSEQS_ERROR: print_error(&ctx, "unexpected error"); status = 1; goto terminate; case CTLSEQS_TIMEOUT: printf("TIMEOUT\n"); status = 1; goto terminate; case CTLSEQS_INTR: printf("INTR\n"); break; case CTLSEQS_EOF: printf("EOF\n"); goto terminate; case CTLSEQS_PARTIAL: if (ctx.verbose) { print_generic_seq("PARTIAL", result, true); } break; case CTLSEQS_NOMATCH: print_generic_seq("NOMATCH", result, true); break; case CTLSEQS_NOMEM: print_generic_seq("NOMEM", result, true); if (ctx.purge_long_seqs) { ctlseqs_purge(reader, result[0].len); break; } else { status = 1; goto terminate; } case CTLSEQS_NOSEQ: print_generic_seq("NOSEQ", result, true); if (result[1].str[0] == 0x04) { goto terminate; } break; default: print_matching_seq(&seqs[retval], result, ctx.verbose); break; } } terminate: ctlseqs_matcher_free(matcher); ctlseqs_reader_free(reader); if (!ctx.not_tty) { tcsetattr(STDIN_FILENO, TCSANOW, &old_termios); } return status; }