1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
// this file has a hissing snake twin in functional2/testlib/terminal_code_eater.py
#include "terminal-code-eater.hh"
#include "lix/libutil/escape-char.hh"
#include <assert.h>
#include <cstdint>
#include <iostream>

namespace nix {

static constexpr const bool DEBUG_EATER = false;

void TerminalCodeEater::feed(char c, std::function<void(char)> on_char)
{
    auto isParamChar = [](char v) -> bool { return v >= 0x30 && v <= 0x3f; };
    auto isIntermediateChar = [](char v) -> bool { return v >= 0x20 && v <= 0x2f; };
    auto isFinalChar = [](char v) -> bool { return v >= 0x40 && v <= 0x7e; };
    if constexpr (DEBUG_EATER) {
        std::cerr << "eater" << MaybeHexEscapedChar{c} << "\n";
    }

    switch (state) {
    case State::ExpectESC:
        switch (c) {
        case '\e':
            transition(State::ExpectESCSeq);
            return;
        // Just eat \r, since it is part of clearing a line
        case '\r':
            return;
        default:
            break;
        }
        if constexpr (DEBUG_EATER) {
            std::cerr << "eater uneat" << MaybeHexEscapedChar{c} << "\n";
        }
        on_char(c);
        break;
    case State::ExpectESCSeq:
        switch (c) {
        // CSI
        case '[':
            transition(State::InCSIParams);
            return;
        case ']':
            transition(State::InOSCParams);
            return;
        // FIXME(jade): whatever this was, we do not know how to delimit it, so
        // we just eat the next character and keep going
        default:
            transition(State::ExpectESC);
            return;
        }
        break;
    // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
    // A CSI sequence is: CSI [\x30-\x3f]* [\x20-\x2f]* [\x40-\x7e]
    //                        ^ params     ^ intermediates ^ final byte
    case State::InCSIParams:
        if (isFinalChar(c)) {
            transition(State::ExpectESC);
            return;
        } else if (isIntermediateChar(c)) {
            transition(State::InCSIIntermediates);
            return;
        } else if (isParamChar(c)) {
            return;
        } else {
            // Corrupt escape sequence? Throw an assert, for now.
            // transition(State::ExpectESC);
            assert(false && "Corrupt terminal escape sequence");
            return;
        }
        break;
    case State::InCSIIntermediates:
        if (isFinalChar(c)) {
            transition(State::ExpectESC);
            return;
        } else if (isIntermediateChar(c)) {
            return;
        } else {
            // Corrupt escape sequence? Throw an assert, for now.
            // transition(State::ExpectESC);
            assert(false && "Corrupt terminal escape sequence in intermediates");
            return;
        }
        break;
    // An OSC is OSC [\x20-\x7e]* ST
    // where OSC is \x1b ] and ST is \x1b \.
    case State::InOSCParams:
        if (c == '\e') {
            // first part of ST
            transition(State::InOSCST);
        } else if (c == '\a') {
            // OSC sequences can be ended by BEL on old xterms
            transition(State::ExpectESC);
        } else if (c < 0x20 or c > 0x7e) {
            assert(false && "Corrupt OSC sequence");
        }
        // either way, eat it
        return;
    case State::InOSCST:
        // ST ends by \.
        if (c == '\\') {
            transition(State::ExpectESC);
        } else if (c < 0x20 || c == 0x7f) {
            assert(false && "Corrupt OSC sequence, in ST");
        } else {
            transition(State::InOSCParams);
        }
        return;
    }
}

void TerminalCodeEater::transition(State new_state)
{
    state = new_state;
}
};