[vte] lib: Add new SIXEL parser and test suite



commit 37d658072e564a37584e1292a1cc3dceebaa9223
Author: Christian Persch <chpe src gnome org>
Date:   Mon Oct 19 00:16:36 2020 +0200

    lib: Add new SIXEL parser and test suite

 src/fwd.hh          |    7 +
 src/meson.build     |   26 +-
 src/sixel-parser.hh |  677 ++++++++++++++++++++++++++++++++++
 src/sixel-test.cc   | 1016 +++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 1725 insertions(+), 1 deletion(-)
---
diff --git a/src/fwd.hh b/src/fwd.hh
index 74a0479b..f84b76e1 100644
--- a/src/fwd.hh
+++ b/src/fwd.hh
@@ -34,6 +34,13 @@ class Widget;
 
 } // namespace platform
 
+namespace sixel {
+
+class Parser;
+class Sequence;
+
+} // namespace sixel
+
 namespace view {
 
 class FontInfo;
diff --git a/src/meson.build b/src/meson.build
index f91582df..1e4a8da6 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -99,7 +99,11 @@ regex_sources = files(
   'regex.hh'
 )
 
-sixel_sources = files(
+sixel_parser_sources = files(
+  'sixel-parser.hh',
+)
+
+sixel_sources = sixel_parser_sources + files(
   'image.cc',
   'image.hh',
   'sixelparser.cc',
@@ -495,6 +499,20 @@ if get_option('sixel')
     include_directories: top_inc,
     install: false,
   )
+
+  test_sixel_sources = glib_glue_sources + sixel_parser_sources + files(
+    'cairo-glue.hh',
+    'sixel-test.cc',
+    'vtedefines.hh',
+  )
+
+  test_sixel = executable(
+    'test-sixel',
+    sources: test_sixel_sources,
+    dependencies: [glib_dep,],
+    include_directories: top_inc,
+    install: false,
+  )
 endif
 
 test_tabstops_sources = files(
@@ -570,6 +588,12 @@ test_units = [
   ['vtetypes', test_vtetypes],
 ]
 
+if get_option('sixel')
+  test_units += [
+    ['sixel', test_sixel],
+  ]
+endif
+
 foreach test: test_units
   test(
     test[0],
diff --git a/src/sixel-parser.hh b/src/sixel-parser.hh
new file mode 100644
index 00000000..bef0a606
--- /dev/null
+++ b/src/sixel-parser.hh
@@ -0,0 +1,677 @@
+/*
+ * Copyright © 2018, 2020 Christian Persch
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <initializer_list>
+#include <type_traits>
+#include <utility>
+
+#include "cxx-utils.hh"
+#include "parser-arg.hh"
+
+#define VTE_SIXEL_PARSER_ARG_MAX (8)
+
+namespace vte::sixel {
+
+class Parser;
+
+enum class Command : uint8_t {
+        NONE = 0x20,
+        DECGRI = 0x21, // DEC Graphics Repeat Introducer
+        DECGRA = 0x22, // DEC Set Raster Attributes
+        DECGCI = 0x23, // DEC Graphics Color Introducer
+        DECGCR = 0x24, // DEC Graphics Carriage Return
+        DECGNL = 0x2d, // DEC Graphics Next Line
+        RESERVED_2_05 = 0x25,
+        RESERVED_2_06 = 0x26,
+        RESERVED_2_07 = 0x27,
+        RESERVED_2_08 = 0x28,
+        RESERVED_2_09 = 0x29,
+        RESERVED_2_10 = 0x2a,
+        RESERVED_2_11 = 0x2b,
+        RESERVED_2_12 = 0x2c,
+        RESERVED_2_14 = 0x2e,
+        RESERVED_2_15 = 0x2f,
+        RESERVED_3_12 = 0x3c,
+        RESERVED_3_13 = 0x3d,
+        RESERVED_3_14 = 0x3e,
+};
+
+class Sequence {
+protected:
+        friend class Parser;
+
+        unsigned m_command{(unsigned)Command::NONE};
+        unsigned m_n_args{0};
+        vte_seq_arg_t m_args[VTE_SIXEL_PARSER_ARG_MAX]{0, 0, 0, 0, 0, 0 ,0 ,0};
+
+        constexpr auto capacity() const noexcept
+        {
+                return sizeof(m_args) / sizeof(m_args[0]);
+        }
+
+public:
+
+        constexpr Sequence() noexcept = default;
+
+        Sequence(Command cmd,
+                 std::initializer_list<int> params = {}) noexcept
+                : m_command(vte::to_integral(cmd))
+        {
+                assert(params.size() <= capacity());
+                for (auto p : params)
+                        m_args[m_n_args++] = vte_seq_arg_init(std::min(p, 0xffff));
+        }
+
+        ~Sequence() = default;
+
+        Sequence(Sequence const&) noexcept = default;
+        Sequence(Sequence&&) noexcept = default;
+
+        Sequence& operator=(Sequence const&) noexcept = default;
+        Sequence& operator=(Sequence&&) noexcept = default;
+
+        constexpr bool
+        operator==(Sequence const& rhs) const noexcept
+        {
+                return command() == rhs.command() &&
+                        size() == rhs.size() &&
+                        std::memcmp(m_args, rhs.m_args, m_n_args * sizeof(m_args[0])) == 0;
+        }
+
+        /* command:
+         *
+         * Returns: the command the sequence codes for.
+         */
+        inline constexpr Command command() const noexcept
+        {
+                return Command(m_command);
+        }
+
+        /* size:
+         *
+         * Returns: the number of parameters
+         */
+        inline constexpr unsigned int size() const noexcept
+        {
+                return m_n_args;
+        }
+
+        /* param_default:
+         * @idx:
+         *
+         * Returns: whether the parameter at @idx has default value
+         */
+        inline constexpr bool param_default(unsigned int idx) const noexcept
+        {
+                return __builtin_expect(idx < size(), 1) ? vte_seq_arg_default(m_args[idx]) : true;
+        }
+
+        /* param:
+         * @idx:
+         * @default_v: the value to use for default parameters
+         *
+         * Returns: the value of the parameter at index @idx, or @default_v if
+         *   the parameter at this index has default value, or the index
+         *   is out of bounds
+         */
+        inline constexpr int param(unsigned int idx,
+                                   int default_v = -1) const noexcept
+        {
+                return __builtin_expect(idx < size(), 1) ? vte_seq_arg_value(m_args[idx], default_v) : 
default_v;
+        }
+
+        /* param:
+         * @idx:
+         * @default_v: the value to use for default parameters
+         * @min_v: the minimum value
+         * @max_v: the maximum value
+         *
+         * Returns: the value of the parameter at index @idx, or @default_v if
+         *   the parameter at this index has default value, or the index
+         *   is out of bounds. The returned value is clamped to the
+         *   range @min_v..@max_v (or returns min_v, if min_v > max_v).
+         */
+        inline constexpr int param(unsigned int idx,
+                                   int default_v,
+                                   int min_v,
+                                   int max_v) const noexcept
+        {
+                auto const v = param(idx, default_v);
+                // not using std::clamp() since it's not guaranteed that min_v <= max_v
+                return std::max(std::min(v, max_v), min_v);
+        }
+
+}; // class Sequence
+
+/* SIXEL parser.
+ *
+ * Known differences to the DEC terminal SIXEL parser:
+ *
+ * * Input bytes with the high bit set are ignored, and not processed as if masked
+ *   with ~0x80; except for C1 controls in Mode::EIGHTBIT mode which will abort parsing
+ *
+ * * Supports UTF-8 C1 controls. C1 ST will finish parsing; all other C1 controls
+ *   will abort parsing (in Mode::UTF8)
+ *
+ * * All C0 controls (except CAN, ESC, SUB) and not just the format effector controls
+ *  (HT, BS, LF, VT, FF, CR) are ignored, not executed as if received before the DCS start
+ *
+ * * 3/10 ':' is reserved for future use as subparameter separator analogous to
+ *   the main parser; any parameter sequences including ':' will be ignored.
+ *
+ * * When the number of parameter exceeds the maximum (16), DEC executes the function
+ *   with these parameters, ignoring the excessive parameters; vte ignores the
+ *   whole function instead.
+ */
+
+class Parser {
+public:
+        enum class Mode {
+                UTF8,     /* UTF-8          */
+                EIGHTBIT, /* ECMA-35, 8 bit */
+                SEVENBIT, /* ECMA-35, 7 bit */
+        };
+
+        enum class Status {
+                CONTINUE = 0,
+                COMPLETE,
+                ABORT,
+                ABORT_REWIND_ONE,
+                ABORT_REWIND_TWO,
+        };
+
+        Parser() = default;
+        ~Parser() = default;
+
+        Parser(Mode mode) :
+                m_mode{mode}
+        {
+        }
+
+private:
+        Parser(Parser const&) = delete;
+        Parser(Parser&&) = delete ;
+
+        Parser& operator=(Parser const&) = delete;
+        Parser& operator=(Parser&) = delete;
+
+        enum class Action {
+                IGNORE,
+                CONSUME,
+                PARAM,
+                FINISH_PARAM,
+        };
+
+        enum class State {
+                GROUND,  /* initial state and ground */
+                PARAMS,  /* have command, now parsing parameters */
+                IGNORE,  /* ignore until next command */
+                ESC,     /* have seen ESC, waiting for backslash */
+                UTF8_C2, /* have seen 0xC2, waiting for second UTF-8 byte */
+        };
+
+        Mode m_mode{Mode::UTF8};
+        State m_state{State::GROUND};
+        Sequence m_seq{};
+
+        [[gnu::always_inline]]
+        void
+        params_clear() noexcept
+        {
+                /* The (m_n_args+1)th parameter may have been started but not
+                 * finialised, so it needs cleaning too. All further params
+                 * have not been touched, so need not be cleaned.
+                 */
+                unsigned int n_args = G_UNLIKELY(m_seq.m_n_args >= VTE_SIXEL_PARSER_ARG_MAX)
+                        ? VTE_SIXEL_PARSER_ARG_MAX
+                        : m_seq.m_n_args + 1;
+                memset(m_seq.m_args, 0, n_args * sizeof(m_seq.m_args[0]));
+#ifdef PARSER_EXTRA_CLEAN
+                /* Assert that the assumed-clean params are actually clean. */
+                for (auto n = n_args; n < VTE_SIXEL_PARSER_ARG_MAX; ++n)
+                        g_assert_cmpuint(m_seq.m_args[n], ==, VTE_SEQ_ARG_INIT_DEFAULT);
+#endif
+
+                m_seq.m_n_args = 0;
+        }
+
+        [[gnu::always_inline]]
+        void
+        params_overflow() noexcept
+        {
+                /* An overflow of the parameter number occurs when
+                 * m_n_arg == VTE_SIXEL_PARSER_ARG_MAX, and either an 0…9
+                 * is encountered, starting the next param, or an
+                 * explicit ':' or ';' terminating a (defaulted) (sub)param,
+                 * or when the next command or sixel data character occurs
+                 * after a defaulted (sub)param.
+                 *
+                 * Transition to IGNORE to ignore the whole sequence.
+                 */
+                transition(0, State::IGNORE);
+        }
+
+        [[gnu::always_inline]]
+        void
+        params_finish() noexcept
+        {
+                if (G_LIKELY(m_seq.m_n_args < VTE_SIXEL_PARSER_ARG_MAX)) {
+                        if (m_seq.m_n_args > 0 ||
+                            vte_seq_arg_started(m_seq.m_args[m_seq.m_n_args])) {
+                                vte_seq_arg_finish(&m_seq.m_args[m_seq.m_n_args], false);
+                                ++m_seq.m_n_args;
+                        }
+                }
+        }
+
+        [[gnu::always_inline]]
+        Status
+        param_finish(uint8_t raw) noexcept
+        {
+                if (G_LIKELY(m_seq.m_n_args < VTE_SIXEL_PARSER_ARG_MAX - 1)) {
+                        vte_seq_arg_finish(&m_seq.m_args[m_seq.m_n_args], false);
+                        ++m_seq.m_n_args;
+                } else
+                        params_overflow();
+
+                return Status::CONTINUE;
+        }
+
+        [[gnu::always_inline]]
+        Status
+        param(uint8_t raw) noexcept
+        {
+                if (G_LIKELY(m_seq.m_n_args < VTE_SIXEL_PARSER_ARG_MAX))
+                        vte_seq_arg_push(&m_seq.m_args[m_seq.m_n_args], raw);
+                else
+                        params_overflow();
+
+                return Status::CONTINUE;
+        }
+
+        template<class D, class = std::void_t<>>
+        struct has_SIXEL_CMD_member : std::false_type { };
+
+        template<class D>
+        struct has_SIXEL_CMD_member<D, std::void_t<decltype(&D::SIXEL_CMD)>> : std::true_type { };
+
+        template<class D>
+        [[gnu::always_inline]]
+        std::enable_if_t<has_SIXEL_CMD_member<D>::value>
+        dispatch(uint8_t raw,
+                 D& delegate) noexcept
+        {
+                params_finish();
+                delegate.SIXEL_CMD(m_seq);
+        }
+
+        template<class D>
+        [[gnu::always_inline]]
+        std::enable_if_t<!has_SIXEL_CMD_member<D>::value>
+        dispatch(uint8_t raw,
+                 D& delegate) noexcept
+        {
+                params_finish();
+                switch (m_seq.command()) {
+                case Command::DECGRI: return delegate.DECGRI(m_seq);
+                case Command::DECGRA: return delegate.DECGRA(m_seq);
+                case Command::DECGCI: return delegate.DECGCI(m_seq);
+                case Command::DECGCR: return delegate.DECGCR(m_seq);
+                case Command::DECGNL: return delegate.DECGNL(m_seq);
+                case Command::NONE:
+                case Command::RESERVED_2_05:
+                case Command::RESERVED_2_06:
+                case Command::RESERVED_2_07:
+                case Command::RESERVED_2_08:
+                case Command::RESERVED_2_09:
+                case Command::RESERVED_2_10:
+                case Command::RESERVED_2_11:
+                case Command::RESERVED_2_12:
+                case Command::RESERVED_2_14:
+                case Command::RESERVED_2_15:
+                case Command::RESERVED_3_12:
+                case Command::RESERVED_3_13:
+                case Command::RESERVED_3_14:
+                default:
+                        return;
+                }
+        }
+
+        template<class D>
+        [[gnu::always_inline]]
+        Status
+        data(uint8_t sixel,
+             D& delegate) noexcept
+        {
+                delegate.SIXEL(sixel);
+                return Status::CONTINUE;
+        }
+
+        [[gnu::always_inline]]
+        Status
+        transition(uint8_t raw,
+                   State state) noexcept
+        {
+                m_state = state;
+                return Status::CONTINUE;
+
+        }
+
+        [[gnu::always_inline]]
+        Status
+        abort(uint8_t raw,
+              Status result) noexcept
+        {
+                transition(raw, State::GROUND);
+                return result;
+        }
+
+        template<class D>
+        [[gnu::always_inline]]
+        Status
+        complete(uint8_t raw,
+                 D& delegate) noexcept
+        {
+                transition(raw, State::GROUND);
+                delegate.SIXEL_ST(raw);
+                return Status::COMPLETE;
+        }
+
+        [[gnu::always_inline]]
+        Status
+        consume(uint8_t raw) noexcept
+        {
+                params_clear();
+                m_seq.m_command = raw;
+                return transition(raw, State::PARAMS);
+        }
+
+        [[gnu::always_inline]]
+        Status
+        nop(uint8_t raw) noexcept
+        {
+                return Status::CONTINUE;
+        }
+
+public:
+
+        template<class D>
+        Status
+        feed(uint8_t raw,
+             D& delegate) noexcept
+        {
+                // Refer to Table 2-2 in DECPPLV2 for information how to handle C0 and C1
+                // controls, DEL, and GR data (in 8-bit mode).
+                switch (m_state) {
+                case State::PARAMS:
+                        switch (raw) {
+                        case 0x00 ... 0x17:
+                        case 0x19:
+                        case 0x1c ... 0x1f: /* C0 \ { CAN, SUB, ESC } */
+                                /* FIXMEchpe: maybe only do this for the format effector controls?,
+                                 * and let GROUND handle everything else C0?
+                                 */
+                                return nop(raw);
+                        case 0x30 ... 0x39: /* '0' ... '9' */
+                                return param(raw);
+                        case 0x3a: /* ':' */
+                                // Reserved for subparams; just ignore the whole sequence.
+                                return transition(raw, State::IGNORE);
+                        case 0x3b: /* ';' */
+                                return param_finish(raw);
+                        case 0x7f: /* DEL */
+                        case 0xa0 ... 0xc1:
+                        case 0xc3 ... 0xff:
+                                return nop(raw);
+                        case 0xc2: /* Start byte for UTF-8 C1 controls */
+                                if (m_mode == Mode::EIGHTBIT)
+                                        return nop(raw);
+
+                                [[fallthrough]];
+                        case 0x80 ... 0x9f:
+                                if (m_mode == Mode::SEVENBIT)
+                                        return nop(raw);
+
+                                [[fallthrough]];
+                        case 0x18: /* CAN */
+                        case 0x1b: /* ESC */
+                        case 0x20 ... 0x2f:
+                        case 0x3c ... 0x7e:
+                                // Dispatch the current command and continue parsing
+                                dispatch(raw, delegate);
+                                [[fallthrough]];
+                        case 0x1a: /* SUB */
+                                /* The question is whether SUB should only act like '?' or
+                                 * also dispatch the current sequence. I interpret the DEC
+                                 * docs as indicating it aborts the sequence without dispatching
+                                 * it and only inserts the '?'.
+                                 */
+                                transition(raw, State::GROUND);;
+                        }
+
+                        [[fallthrough]];
+                case State::GROUND:
+                ground:
+                        switch (raw) {
+                        case 0x00 ... 0x17:
+                        case 0x19:
+                        case 0x1c ... 0x1f: /* C0 \ { CAN, SUB, ESC } */
+                                // According to DECPPLV2, the format effector controls
+                                // (HT, BS, LF, VT, FF, CR) should be executed as if
+                                // received before the DECSIXEL DCS, and then processing
+                                // to continue for the control string, and the other C0
+                                // controls should be ignored.
+                                // VTE just ignores all C0 controls except ESC, CAN, SUB
+                                return nop(raw);
+                        case 0x18: /* CAN */
+                                return abort(raw, Status::ABORT_REWIND_ONE);
+                        case 0x1b: /* ESC */
+                                return transition(raw, State::ESC);
+                        case 0x20: /* SP */
+                                return nop(raw);
+                        case 0x21 ... 0x2f:
+                        case 0x3c ... 0x3e:
+                                return consume(raw);
+                        case 0x30 ... 0x3b: /* { '0' .. '9', ':', ';' } */
+                                // Parameters, but we don't have a command yet.
+                                // Ignore the whole sequence.
+                                return transition(raw, State::IGNORE);
+                        case 0x1a: /* SUB */
+                                // Same as 3/15 '?' according to DECPPLV2
+                                raw = 0x3fu;
+                                [[fallthrough]];
+                        case 0x3f ... 0x7e: /* { '?' .. '~' } */
+                                // SIXEL data
+                                return data(raw - 0x3f, delegate);
+                        case 0x7f: /* DEL */
+                                // Ignore according to DECPPLV2
+                                return nop(raw);
+                        case 0xc2: /* Start byte for UTF-8 C1 controls */
+                                if (m_mode == Mode::UTF8)
+                                        return transition(raw, State::UTF8_C2);
+                                return nop(raw);
+                        case 0x9c: /* raw C1 ST */
+                                if (m_mode == Mode::EIGHTBIT)
+                                        return complete(raw, delegate);
+                                [[fallthrough]];
+                        case 0x80 ... 0x9b:
+                        case 0x9d ... 0x9f: /* raw C1 \ { ST } */
+                                // Abort and execute C1 control
+                                if (m_mode == Mode::EIGHTBIT)
+                                        return abort(raw, Status::ABORT_REWIND_ONE);
+                                [[fallthrough]];
+                        case 0xa0 ... 0xc1:
+                        case 0xc3 ... 0xff: /* GR */
+                                return nop(raw);
+
+                        }
+                        break;
+
+                case State::IGNORE:
+                        switch (raw) {
+                        // FIXMEchpe do we need to nop() C0 constrols (except SUB, CAN, ESC) here?
+                        case 0x30 ... 0x3b: /* { '0' .. '9', ':', ';' } */
+                        case 0x7f: /* DEL */
+                                return nop(raw);
+                        case 0x00 ... 0x2f:
+                        case 0x3c ... 0x7e:
+                        case 0x80 ... 0xff:
+                                transition(raw, State::GROUND);
+                                goto ground;
+                        }
+                        break;
+
+                case State::ESC:
+                        switch (raw) {
+                        case 0x5c: /* '\' */
+                                return complete(raw, delegate);
+                        case 0x7f: /* DEL */
+                                // FIXMEchpe is this correct? check with main parser / spec / DEC
+                                return nop(raw);
+                        case 0x00 ... 0x5b:
+                        case 0x5d ... 0x7e:
+                        case 0x80 ... 0xff:
+                                /* Abort and let the outer parser handle the ESC again */
+                                return abort(raw, Status::ABORT_REWIND_TWO);
+                        }
+                        break;
+
+                case State::UTF8_C2:
+                        switch (raw) {
+                        case 0x1b: /* ESC */
+                                return transition(raw, State::ESC);
+                        case 0x80 ... 0x9b:
+                        case 0x9d ... 0x9f: /* C1 \ { ST } */
+                                /* Abort and let the outer parser handle the C1 control again */
+                                return abort(raw, Status::ABORT_REWIND_TWO);
+                        case 0x9c: /* ST */
+                                return complete(raw, delegate);
+                        case 0xc2:
+                                return transition(raw, State::UTF8_C2);
+                        case 0x00 ... 0x1a:
+                        case 0x1c ... 0x7f: /* including DEL */
+                        case 0xa0 ... 0xc1:
+                        case 0xc3 ... 0xff:
+                                transition(raw, State::GROUND);
+                                goto ground;
+                        }
+                        break;
+                default:
+                        break;
+                }
+                __builtin_unreachable();
+                return Status::CONTINUE;
+        }
+
+        template<class D>
+        Status
+        flush(D& delegate) noexcept
+        {
+                switch (m_state) {
+                case State::PARAMS:
+                        dispatch(0, delegate);
+                        [[fallthrough]];
+                case State::GROUND:
+                case State::IGNORE:
+                        return abort(0, Status::ABORT);
+                default:
+                        __builtin_unreachable();
+                        [[fallthrough]];
+                case State::ESC:
+                case State::UTF8_C2:
+                        return abort(0, Status::ABORT_REWIND_ONE);
+                }
+        }
+
+        void
+        reset() noexcept
+        {
+                transition(0, State::GROUND);
+        }
+
+        void
+        set_mode(Mode mode) noexcept
+        {
+                reset();
+                m_mode = mode;
+        }
+
+        constexpr auto const& sequence() const noexcept { return m_seq; }
+
+        enum class ParseStatus {
+                CONTINUE,
+                COMPLETE,
+                ABORT
+        };
+
+        template<class D>
+        std::pair<ParseStatus, uint8_t const*>
+        parse(uint8_t const* const bufstart,
+              uint8_t const* const bufend,
+              bool eos,
+              D& delegate) noexcept
+        {
+                for (auto sptr = bufstart; sptr < bufend; ) {
+                        switch (feed(*(sptr++), delegate)) {
+                        case vte::sixel::Parser::Status::CONTINUE:
+                                break;
+
+                        case vte::sixel::Parser::Status::COMPLETE:
+                                return {ParseStatus::COMPLETE, sptr};
+
+                        case vte::sixel::Parser::Status::ABORT_REWIND_TWO:
+                                --sptr;
+                                [[fallthrough]];
+                        case vte::sixel::Parser::Status::ABORT_REWIND_ONE:
+                                --sptr;
+                                [[fallthrough]];
+                        case vte::sixel::Parser::Status::ABORT:
+                                return {ParseStatus::ABORT, sptr};
+                        }
+                }
+
+                if (eos) {
+                        auto sptr = bufend;
+                        switch (flush(delegate)) {
+                        case vte::sixel::Parser::Status::CONTINUE:
+                                break;
+
+                        case vte::sixel::Parser::Status::COMPLETE:
+                                return {ParseStatus::COMPLETE, sptr};
+
+                        case vte::sixel::Parser::Status::ABORT_REWIND_TWO:
+                                --sptr;
+                                [[fallthrough]];
+                        case vte::sixel::Parser::Status::ABORT_REWIND_ONE:
+                                --sptr;
+                                [[fallthrough]];
+                        case vte::sixel::Parser::Status::ABORT:
+                                return {ParseStatus::ABORT, sptr};
+                        }
+                }
+
+                return {ParseStatus::CONTINUE, bufend};
+        }
+
+}; // class Parser
+
+} // namespace vte::sixel
diff --git a/src/sixel-test.cc b/src/sixel-test.cc
new file mode 100644
index 00000000..05a9a206
--- /dev/null
+++ b/src/sixel-test.cc
@@ -0,0 +1,1016 @@
+/*
+ * Copyright © 2020 Christian Persch
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <initializer_list>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include <glib.h>
+
+#include "sixel-parser.hh"
+
+using namespace std::literals;
+
+using Command = vte::sixel::Command;
+using Mode = vte::sixel::Parser::Mode;
+using ParseStatus = vte::sixel::Parser::ParseStatus;
+
+// Parser tests
+
+static char const*
+cmd_to_str(Command command)
+{
+        switch (command) {
+        case Command::DECGRI: return "DECGRI";
+        case Command::DECGRA: return "DECGRA";
+        case Command::DECGCI: return "DECGCI";
+        case Command::DECGCR: return "DECGCR";
+        case Command::DECGNL: return "DECGNL";
+        case Command::NONE:   return "NONE";
+        default:
+                static char buf[32];
+                snprintf(buf, sizeof(buf), "UNKOWN(%d/%02d)",
+                         (int)command / 16,
+                         (int)command % 16);
+                return buf;
+        }
+}
+
+enum class StType {
+        C0,
+        C1_UTF8,
+        C1_EIGHTBIT
+};
+
+inline constexpr auto
+ST(StType type)
+{
+        switch (type) {
+        case StType::C0:          return "\e\\"sv;
+        case StType::C1_UTF8:     return "\xc2\x9c"sv;
+        case StType::C1_EIGHTBIT: return "\x9c"sv;
+        default: __builtin_unreachable();
+        }
+}
+
+inline constexpr auto
+ST(Mode mode)
+{
+        switch (mode) {
+        case Mode::UTF8:     return ST(StType::C1_UTF8);
+        case Mode::EIGHTBIT: return ST(StType::C1_EIGHTBIT);
+        case Mode::SEVENBIT: return ST(StType::C0);
+        default: __builtin_unreachable();
+        }
+}
+
+class Sequence : public vte::sixel::Sequence {
+public:
+        using Base = vte::sixel::Sequence;
+
+        Sequence(Base const& seq)
+                : Base{seq}
+        {
+        }
+
+        Sequence(Command cmd,
+                 std::vector<int> const& params) noexcept
+                : Base{cmd}
+        {
+                assert(params.size() <= (sizeof(m_args) / sizeof(m_args[0])));
+                for (auto p : params)
+                        m_args[m_n_args++] = vte_seq_arg_init(std::min(p, 0xffff));
+        }
+
+        void append(std::string& str) const
+        {
+                if (command() != Command::NONE)
+                        str.append(1, char(command()));
+                for (auto i = 0u; i < size(); ++i) {
+                        auto const p = param(i);
+                        if (p != -1) {
+                                char buf[12];
+                                auto const len = g_snprintf(buf, sizeof(buf), "%d", p);
+                                str.append(buf, len);
+                        }
+                        if ((i + 1) < size())
+                                str.append(1, ';');
+                }
+        }
+
+        void prettyprint(std::string& str) const
+        {
+                str.append("Sequence(");
+                str.append(cmd_to_str(command()));
+                if (size()) {
+                        str.append(" ");
+                        for (auto i = 0u; i < size(); ++i) {
+                                auto const p = param(i);
+
+                                char buf[12];
+                                auto const len = g_snprintf(buf, sizeof(buf), "%d", p);
+                                str.append(buf, len);
+
+                                if ((i + 1) < size())
+                                        str.append(1, ';');
+                        }
+                }
+                str.append(")");
+        }
+};
+
+constexpr bool operator==(Sequence const& lhs, Sequence const& rhs) noexcept
+{
+        if (lhs.command() != rhs.command())
+                return false;
+
+        auto const m = std::min(lhs.size(), rhs.size());
+        for (auto n = 0u; n < m; ++n)
+                if (lhs.param(n) != rhs.param(n))
+                        return false;
+
+        if (lhs.size() == rhs.size())
+                return true;
+
+        if ((lhs.size() == (rhs.size() + 1)) && lhs.param(rhs.size()) == -1)
+                return true;
+
+        if (((lhs.size() + 1) == rhs.size()) && rhs.param(lhs.size()) == -1)
+                return true;
+
+        return false;
+}
+
+class Sixel {
+public:
+        constexpr Sixel(uint8_t sixel)
+                : m_sixel(sixel)
+        {
+                assert(m_sixel < 0b100'0000);
+        }
+
+        ~Sixel() = default;
+
+        constexpr auto sixel() const noexcept { return m_sixel; }
+
+        void append(std::string& str) const { str.append(1, char(m_sixel + 0x3f)); }
+
+        void prettyprint(std::string& str) const
+        {
+                str.append("Sixel(");
+                char buf[3];
+                auto const len = g_snprintf(buf, sizeof(buf), "%02x", sixel());
+                str.append(buf, len);
+                str.append(")");
+        }
+
+private:
+        uint8_t m_sixel{0};
+};
+
+constexpr bool operator==(Sixel const& lhs, Sixel const& rhs) noexcept
+{
+        return lhs.sixel() == rhs.sixel();
+}
+
+class Unicode {
+public:
+        Unicode(char32_t c) :
+                m_c{c}
+        {
+                m_utf8_len = g_unichar_to_utf8(c, m_utf8_buf);
+        }
+        ~Unicode() = default;
+
+        constexpr auto unicode() const noexcept { return m_c; }
+
+        void append(std::string& str) const { str.append(m_utf8_buf, m_utf8_len); }
+
+        void prettyprint(std::string& str) const
+        {
+                str.append("Unicode(");
+                char buf[7];
+                auto const len = g_snprintf(buf, sizeof(buf), "%04X", unicode());
+                str.append(buf, len);
+                str.append(")");
+        }
+
+private:
+        char32_t m_c{0};
+        size_t m_utf8_len{0};
+        char m_utf8_buf[4]{0, 0, 0, 0};
+};
+
+constexpr bool operator==(Unicode const& lhs, Unicode const& rhs) noexcept
+{
+        return lhs.unicode() == rhs.unicode();
+}
+
+class C0Control {
+public:
+        C0Control(uint8_t c) :
+                m_control{c}
+        {
+                assert(c < 0x20 || c == 0x7f);
+        }
+        ~C0Control() = default;
+
+        constexpr auto control() const noexcept { return m_control; }
+
+        void append(std::string& str) const { str.append(1, char(m_control)); }
+
+        void prettyprint(std::string& str) const
+        {
+                str.append("C0(");
+                char buf[3];
+                auto const len = g_snprintf(buf, sizeof(buf), "%02X", control());
+                str.append(buf, len);
+                str.append(")");
+        }
+
+private:
+        uint8_t m_control{0};
+};
+
+constexpr bool operator==(C0Control const& lhs, C0Control const& rhs) noexcept
+{
+        return lhs.control() == rhs.control();
+}
+
+class C1Control {
+public:
+        C1Control(uint8_t c) :
+                m_control{c}
+        {
+                assert(c >= 0x80 && c < 0xa0);
+                auto const len = g_unichar_to_utf8(c, m_utf8_buf);
+                assert(len == 2);
+        }
+        ~C1Control() = default;
+
+        constexpr auto control() const noexcept { return m_control; }
+
+        void append(std::string& str,
+                    Mode mode) const {
+                switch (mode) {
+                case Mode::UTF8:
+                        str += std::string_view(m_utf8_buf, 2);
+                        break;
+                case Mode::EIGHTBIT:
+                        str.append(1, char(m_control));
+                        break;
+                case Mode::SEVENBIT:
+                        str.append(1, char(0x1b));
+                        str.append(1, char(m_control - 0x40));
+                        break;
+                }
+        }
+
+        void prettyprint(std::string& str) const
+        {
+                str.append("C1(");
+                char buf[3];
+                auto const len = g_snprintf(buf, sizeof(buf), "%02X", control());
+                str.append(buf, len);
+                str.append(")");
+        }
+
+private:
+        uint8_t m_control{0};
+        char m_utf8_buf[2]{0, 0};
+};
+
+constexpr bool operator==(C1Control const& lhs, C1Control const& rhs) noexcept
+{
+        return lhs.control() == rhs.control();
+}
+
+class Raw {
+public:
+        Raw(uint8_t raw) :
+                m_raw{raw}
+        {
+        }
+        ~Raw() = default;
+
+        constexpr auto raw() const noexcept { return m_raw; }
+
+        void append(std::string& str) const { str += char(m_raw); }
+
+        void prettyprint(std::string& str) const
+        {
+                str.append("Raw(");
+                char buf[3];
+                auto const len = g_snprintf(buf, sizeof(buf), "%02X", raw());
+                str.append(buf, len);
+                str.append(")");
+        }
+
+private:
+        uint8_t m_raw{0};
+};
+
+constexpr bool operator==(Raw const& lhs, Raw const& rhs) noexcept
+{
+        return lhs.raw() == rhs.raw();
+}
+
+inline auto
+DECGRI(int count) noexcept
+{
+        return Sequence{Command::DECGRI, {count}};
+}
+
+inline auto
+DECGRA(int an,
+       int ad,
+       int w,
+       int h) noexcept
+{
+        return Sequence{Command::DECGRA, {an, ad, w, h}};
+}
+
+inline auto
+DECGCI(int reg) noexcept
+{
+        return Sequence{Command::DECGCI, {reg}};
+}
+
+inline auto
+DECGCI_HLS(int reg,
+           int h,
+           int l,
+           int s) noexcept
+{
+        return Sequence{Command::DECGCI, {reg, 1, h, l, s}};
+}
+
+inline auto
+DECGCI_RGB(int reg,
+           int r,
+           int g,
+           int b) noexcept
+{
+        return Sequence{Command::DECGCI, {reg, 2, r, g, b}};
+}
+
+inline auto
+DECGCR() noexcept
+{
+        return Sequence{Command::DECGCR};
+}
+
+inline auto
+DECGNL() noexcept
+{
+        return Sequence{Command::DECGNL};
+}
+
+using Item = std::variant<Sequence, Sixel, C0Control, C1Control, Unicode, Raw>;
+using ItemList = std::vector<Item>;
+
+#if 0
+
+class ItemPrinter {
+public:
+        ItemPrinter(Item const& item)
+        {
+                std::visit(*this, item);
+        }
+
+        ~ItemPrinter() = default;
+
+        std::string const& string()    const noexcept { return m_str; }
+        std::string_view string_view() const noexcept { return m_str; }
+
+        void operator()(Sequence const& seq)      { seq.prettyprint(m_str);     }
+        void operator()(Sixel const& sixel)       { sixel.prettyprint(m_str);   }
+        void operator()(C0Control const& control) { control.prettyprint(m_str); }
+        void operator()(C1Control const& control) { control.prettyprint(m_str); }
+        void operator()(Unicode const& unicode)   { unicode.prettyprint(m_str); }
+        void operator()(Raw const& raw)           { raw.prettyprint(m_str);     }
+
+private:
+        std::string m_str{};
+};
+
+static void
+print_items(char const* intro,
+            ItemList const& items)
+{
+        auto str = std::string{};
+
+        for (auto const& item : items) {
+                str += ItemPrinter{item}.string();
+                str += " ";
+        }
+
+        g_printerr("%s: %s\n", intro, str.c_str());
+}
+
+#endif
+
+class ItemStringifier {
+public:
+        ItemStringifier(Mode mode = Mode::UTF8) :
+                m_mode{mode}
+        { }
+
+        ItemStringifier(Item const& item,
+                        Mode mode = Mode::UTF8) :
+                m_mode{mode}
+        {
+                std::visit(*this, item);
+        }
+
+        ItemStringifier(ItemList const& items,
+                        Mode mode = Mode::UTF8) :
+                m_mode{mode}
+        {
+                for (auto&& i : items)
+                        std::visit(*this, i);
+        }
+
+        ~ItemStringifier() = default;
+
+        std::string string() const noexcept { return m_str; }
+        std::string_view string_view() const noexcept { return m_str; }
+
+        void operator()(Sequence const& seq)      { seq.append(m_str);             }
+        void operator()(Sixel const& sixel)       { sixel.append(m_str);           }
+        void operator()(C0Control const& control) { control.append(m_str);         }
+        void operator()(C1Control const& control) { control.append(m_str, m_mode); }
+        void operator()(Unicode const& unicode)   { unicode.append(m_str);         }
+        void operator()(Raw const& raw)           { raw.append(m_str);             }
+
+private:
+        std::string m_str{};
+        Mode m_mode;
+};
+
+class SimpleContext {
+
+        friend class Parser;
+public:
+        SimpleContext() = default;
+        ~SimpleContext() = default;
+
+        auto parse(std::string_view const& str,
+                   size_t end_pos = size_t(-1))
+        {
+                auto const beginptr = reinterpret_cast<uint8_t const*>(str.data());
+                auto const endptr = reinterpret_cast<uint8_t const*>(beginptr + str.size());
+                return m_parser.parse(beginptr, endptr, true, *this);
+        }
+
+        auto parse(Item const& item,
+                   Mode input_mode)
+        {
+                return parse(ItemStringifier{{item}, input_mode}.string_view());
+        }
+
+        auto parse(ItemList const& list,
+                   Mode input_mode)
+        {
+                return parse(ItemStringifier{list, input_mode}.string_view());
+        }
+
+        void set_mode(Mode mode)
+        {
+                m_parser.set_mode(mode);
+        }
+
+        void reset_mode()
+        {
+                set_mode(Mode::UTF8);
+        }
+
+        void reset()
+        {
+                m_parser.reset();
+                m_parsed_items.clear();
+                m_st = 0;
+        }
+
+        auto const& parsed_items() const noexcept { return m_parsed_items; }
+
+        void SIXEL(uint8_t raw) noexcept
+        {
+                m_parsed_items.push_back(Sixel(raw));
+        }
+
+        void SIXEL_CMD(vte::sixel::Sequence const& seq) noexcept
+        {
+                m_parsed_items.push_back(Sequence(seq));
+        }
+
+        void SIXEL_ST(char32_t st) noexcept
+        {
+                m_st = st;
+        }
+
+        vte::sixel::Parser m_parser{};
+        ItemList m_parsed_items{};
+        char32_t m_st{0};
+
+}; // class SimpleContext
+
+/*
+ * assert_parse:
+ * @context:
+ * @mode:
+ * @str:
+ * @str_size:
+ * @expected_parsed_len:
+ * @expected_status:
+ *
+ * Asserts that parsing @str (up to @str_size, or until its size if @str_size is -1)
+ * in mode @mode results in @expected_status, with the endpointer pointing to the end
+ * of @str if @expected_parsed_len is -1, or to @expected_parsed_len otherwise.
+ */
+template<class C>
+static void
+assert_parse(C& context,
+             Mode mode,
+             std::string_view const& str,
+             size_t str_size = size_t(-1),
+             size_t expected_parse_end = size_t(-1),
+             ParseStatus expected_status = ParseStatus::COMPLETE,
+             int line = __builtin_LINE())
+{
+        context.reset();
+        context.set_mode(mode);
+
+        auto const beginptr = reinterpret_cast<uint8_t const*>(str.data());
+        auto const len = str_size == size_t(-1) ? str.size() : str_size;
+        auto const [status, ip] = context.parse(str, len);
+        auto const parsed_len = size_t(ip - beginptr);
+
+        g_assert_cmpint(int(status), ==, int(expected_status));
+        g_assert_cmpint(parsed_len, ==, expected_parse_end == size_t(-1) ? len : expected_parse_end);
+}
+
+/*
+ * assert_parse:
+ * @context:
+ * @mode:
+ * @str:
+ * @expected_items:
+ * @str_size:
+ * @expected_parsed_len:
+ * @expected_status:
+ *
+ * Asserts that parsing @str (up to @str_size, or until its size if @str_size is -1)
+ * in mode @mode results in @expected_status, with the parsed items equal to
+ * @expected_items, and the endpointer pointing to the end of @str if @expected_parsed_len
+ * is -1, or to @expected_parsed_len otherwise.
+ */
+template<class C>
+static void
+assert_parse(C& context,
+             Mode mode,
+             std::string_view const& str,
+             ItemList const& expected_items,
+             size_t str_size = size_t(-1),
+             size_t expected_parse_end = size_t(-1),
+             ParseStatus expected_status = ParseStatus::COMPLETE,
+             int line = __builtin_LINE())
+{
+        assert_parse(context, mode, str, str_size, expected_parse_end, expected_status, line);
+
+        g_assert_true(context.parsed_items() == expected_items);
+}
+
+/*
+ * assert_parse_st:
+ *
+ * Like assert_parse above, but ST-terminates the passed string.
+ */
+template<class C>
+static void
+assert_parse_st(C& context,
+                Mode mode,
+                std::string_view const& str,
+                size_t str_size = size_t(-1),
+                size_t expected_parse_end = size_t(-1),
+                ParseStatus expected_status = ParseStatus::COMPLETE,
+                StType st = StType::C0,
+                int line = __builtin_LINE())
+{
+        auto str_st = std::string{str};
+        str_st.append(ST(st));
+        auto str_st_size = str_size;
+
+        assert_parse(context, mode, str_st, str_st_size, expected_parse_end, expected_status, line);
+}
+
+/*
+ * assert_parse_st:
+ *
+ * Like assert_parse above, but ST-terminates the passed string.
+ */
+template<class C>
+static void
+assert_parse_st(C& context,
+                Mode mode,
+                std::string_view const& str,
+                ItemList const& expected_items,
+                size_t str_size = size_t(-1),
+                size_t expected_parse_end = size_t(-1),
+                ParseStatus expected_status = ParseStatus::COMPLETE,
+                StType st = StType::C0,
+                int line = __builtin_LINE())
+{
+        auto str_st = std::string{str};
+        str_st.append(ST(st));
+        auto str_st_size = str_size == size_t(-1) ? str_st.size() : str_size;
+
+        assert_parse(context, mode, str_st, expected_items, str_st_size, expected_parse_end, 
expected_status, line);
+}
+
+/*
+ * assert_parse_st:
+ *
+ * Like assert_parse above, but ST-terminates the passed string.
+ */
+template<class C>
+static void
+assert_parse_st(C& context,
+                Mode mode,
+                ItemList const& items,
+                ItemList const& expected_items,
+                ParseStatus expected_status = ParseStatus::COMPLETE,
+                StType st = StType::C0,
+                int line = __builtin_LINE())
+{
+        assert_parse_st(context, mode, ItemStringifier{items, mode}.string_view(), expected_items, -1, -1, 
expected_status, st, line);
+}
+
+static void
+test_parser_seq_params(SimpleContext& context,
+                       Mode mode,
+                       std::vector<int> const& params)
+{
+        for (auto i = 0x20; i < 0x3f; ++i) {
+                if (i >= 0x30 && i < 0x3c) // Parameter characters
+                        continue;
+
+
+                auto const items = ItemList{Sequence{Command(i), params}};
+                assert_parse_st(context, mode, items,
+                                (i == 0x20) ? ItemList{} /* 0x20 is ignored */ : items);
+        }
+}
+
+static void
+test_parser_seq_params(SimpleContext& context,
+                       vte_seq_arg_t params[8],
+                       bool as_is = false)
+{
+        for (auto mode : {Mode::UTF8, Mode::EIGHTBIT, Mode::SEVENBIT}) {
+                context.set_mode(mode);
+
+                for (auto n = 0; n <= 8; ++n) {
+                        auto pv = std::vector<int>(&params[0], &params[n]);
+
+                        test_parser_seq_params(context, mode, pv);
+
+                        if (n > 0 && !as_is) {
+                                pv[n - 1] = -1;
+                                test_parser_seq_params(context, mode, pv);
+                        }
+                }
+        }
+
+        context.reset_mode();
+}
+
+static void
+test_parser_seq_params(void)
+{
+        auto context = SimpleContext{};
+
+        /* Tests sixel commands, which have the form I P...P with an initial byte
+         * in the 2/0..2/15, 3/12..3/14 range, and parameter bytes P from 3/0..3/11.
+         */
+        vte_seq_arg_t params1[8]{1, 0, 1000, 10000, 65534, 65535, 65536, 1};
+        test_parser_seq_params(context, params1);
+
+        vte_seq_arg_t params2[8]{1, -1, -1, -1, 1, -1, 1, 1};
+        test_parser_seq_params(context, params2, true);
+}
+
+static void
+test_parser_seq_subparams(void)
+{
+        // Test that subparams cause the whole sequence to be ignored
+
+        auto context = SimpleContext{};
+
+        for (auto mode : {Mode::UTF8, Mode::EIGHTBIT, Mode::SEVENBIT}) {
+
+                assert_parse_st(context, mode, "#0;1:2;#:#;1;3:#;:;;"sv, ItemList{});
+        }
+}
+
+static void
+test_parser_seq_params_clear(void)
+{
+        /* Check that parameters are cleared from the last sequence */
+
+        auto context = SimpleContext{};
+
+        for (auto mode : {Mode::UTF8, Mode::EIGHTBIT, Mode::SEVENBIT}) {
+                auto items = ItemList{Sequence{Command::DECGCI, {0, 1, 2, 3, 4, 5, 6, 7}},
+                                      Sequence{Command::DECGRI, {5, 3}},
+                                      Sequence{Command::DECGNL}};
+                assert_parse_st(context, mode, items, items);
+
+                auto parsed_items = context.parsed_items();
+
+                /* Verify that non-specified paramaters have default value */
+                auto& item1 = std::get<Sequence>(parsed_items[1]);
+                for (auto n = 2; n < 8; ++n)
+                        g_assert_cmpint(item1.param(n), ==, -1);
+
+
+                auto& item2 = std::get<Sequence>(parsed_items[2]);
+                for (auto n = 0; n < 8; ++n)
+                        g_assert_cmpint(item2.param(n), ==, -1);
+        }
+}
+
+static void
+test_parser_seq_params_max(void)
+{
+        /* Check that an excessive number of parameters causes the
+         * sequence to be ignored.
+         */
+
+        auto context = SimpleContext{};
+
+        auto items = ItemList{Sequence{Command::DECGRA, {0, 1, 2, 3, 4, 5, 6, 7}}};
+        auto str = ItemStringifier{items, Mode::SEVENBIT}.string();
+
+        /* The sequence with VTE_SIXEL_PARSER_ARG_MAX args must be parsed */
+        assert_parse_st(context, Mode::UTF8, str, items);
+
+        /* Now test that adding one more parameter (whether with an
+         * explicit value, or default), causes the sequence to be ignored.
+         */
+        assert_parse_st(context, Mode::UTF8, str + ";8"s, ItemList{});
+        assert_parse_st(context, Mode::UTF8, str + ";"s, ItemList{});
+}
+
+static void
+test_parser_seq_glue_arg(void)
+{
+        /* The sixel Sequence's parameter accessors are copied from the main parser's
+         * Sequence class, so we don't need to test them here again.
+         */
+}
+
+static void
+test_parser_st(void)
+{
+        /* Test that ST is recognised in all forms and from all states, and
+         * that different-mode C1 ST is not recognised.
+         */
+
+        auto context = SimpleContext{};
+
+        assert_parse(context, Mode::UTF8, "?\x9c\e\\"sv, {Sixel{0}});
+        assert_parse(context, Mode::UTF8, "!5\x9c\e\\"sv, {Sequence{Command::DECGRI, {5}}});
+        assert_parse(context, Mode::UTF8, "5\x9c\e\\"sv, ItemList{});
+        assert_parse(context, Mode::UTF8, "\x9c\xc2\e\\"sv, ItemList{});
+
+        assert_parse(context, Mode::UTF8, "?\x9c\xc2\x9c"sv, {Sixel{0}});
+        assert_parse(context, Mode::UTF8, "!5\x9c\xc2\x9c"sv, {Sequence{Command::DECGRI, {5}}});
+        assert_parse(context, Mode::UTF8, "5\x9c\xc2\x9c"sv, ItemList{});
+        assert_parse(context, Mode::UTF8, "\x9c\xc2\xc2\x9c"sv, ItemList{});
+
+        assert_parse(context, Mode::EIGHTBIT, "?\e\\"sv, {Sixel{0}});
+        assert_parse(context, Mode::EIGHTBIT, "!5\e\\"sv, {Sequence{Command::DECGRI, {5}}});
+        assert_parse(context, Mode::EIGHTBIT, "5\e\\"sv, ItemList{});
+        assert_parse(context, Mode::EIGHTBIT, "\xc2\e\\"sv, ItemList{});
+
+        assert_parse(context, Mode::EIGHTBIT, "?\xc2\x9c"sv, {Sixel{0}});
+        assert_parse(context, Mode::EIGHTBIT, "!5\xc2\x9c"sv, {Sequence{Command::DECGRI, {5}}});
+        assert_parse(context, Mode::EIGHTBIT, "5\xc2\x9c"sv, ItemList{});
+        assert_parse(context, Mode::EIGHTBIT, "\xc2\xc2\x9c"sv, ItemList{});
+
+        assert_parse(context, Mode::SEVENBIT, "?\xc2\x9c\e\\"sv, {Sixel{0}});
+        assert_parse(context, Mode::SEVENBIT, "!5\xc2\x9c\e\\"sv, {Sequence{Command::DECGRI, {5}}});
+        assert_parse(context, Mode::SEVENBIT, "5\xc2\x9c\e\\"sv, ItemList{});
+        assert_parse(context, Mode::SEVENBIT, "\xc2\x9c\xc2\e\\"sv, ItemList{});
+}
+
+static constexpr auto
+test_string()
+{
+        return "a#22a#22\xc2z22a22\xc2"sv;
+}
+
+template<class C>
+static void
+test_parser_insert(C& context,
+                   Mode mode,
+                   std::string_view const& str,
+                   std::string_view const& insert_str,
+                   ParseStatus expected_status = ParseStatus::COMPLETE,
+                   int line = __builtin_LINE())
+{
+        for (auto pos = 0u; pos <= str.size(); ++pos) {
+                auto estr = std::string{str};
+                estr.insert(pos, insert_str);
+
+                assert_parse_st(context, mode, estr, -1,
+                                expected_status == ParseStatus::COMPLETE ? size_t(-1) : size_t(pos),
+                                expected_status, StType::C0, line);
+
+                if (expected_status == ParseStatus::COMPLETE) {
+                        auto items = context.parsed_items(); // copy
+
+                        assert_parse_st(context, mode, str);
+                        assert(items == context.parsed_items());
+                }
+        }
+}
+
+template<class C>
+static void
+test_parser_insert(C& context,
+                   std::string_view const& str,
+                   std::string_view const& insert_str,
+                   ParseStatus expected_status = ParseStatus::COMPLETE,
+                   int line = __builtin_LINE())
+{
+        for (auto mode : {Mode::UTF8, Mode::EIGHTBIT, Mode::SEVENBIT}) {
+                test_parser_insert(context, mode, str, insert_str, expected_status, line);
+        }
+}
+
+static void
+test_parser_controls_c0_esc(void)
+{
+        /* Test that ESC (except C0 ST) always aborts the parsing at the position of the ESC */
+
+        auto context = SimpleContext{};
+        auto const str = test_string();
+
+        for (auto c = 0x20; c < 0x7f; ++c) {
+                if (c == 0x5c) /* '\' */
+                        continue;
+
+                char esc[2] = {0x1b, char(c)};
+                test_parser_insert(context, str, {esc, 2}, ParseStatus::ABORT);
+        }
+}
+
+static void
+test_parser_controls_c0_can(void)
+{
+        /* Test that CAN is handled correctly in all states */
+
+        auto context = SimpleContext{};
+
+        for (auto mode : {Mode::UTF8, Mode::EIGHTBIT, Mode::SEVENBIT}) {
+
+                assert_parse_st(context, mode, "@\x18"sv, {Sixel{1}}, -1, 1, ParseStatus::ABORT);
+                assert_parse_st(context, mode, "!5\x18"sv, {Sequence{Command::DECGRI, {5}}}, -1, 2, 
ParseStatus::ABORT);
+                assert_parse_st(context, mode, "5\x18"sv, ItemList{}, -1, 1, ParseStatus::ABORT);
+                assert_parse_st(context, mode, "\xc2\x18"sv, ItemList{}, -1, 1, ParseStatus::ABORT);
+        }
+}
+
+static void
+test_parser_controls_c0_sub(void)
+{
+        /* Test that SUB is handled correctly in all states */
+
+        auto context = SimpleContext{};
+
+        for (auto mode : {Mode::UTF8, Mode::EIGHTBIT, Mode::SEVENBIT}) {
+
+                assert_parse_st(context, mode, "@\x1a"sv, {Sixel{1}, Sixel{0}});
+
+                /* The parser chooses to not dispatch the current sequence on SUB; see the
+                 * comment in the Parser class. Otherwise there'd be a
+                 * Sequence{Command::DECGRI, {5}} as the first expected item here.
+                 */
+                assert_parse_st(context, mode, "!5\x1a"sv, {Sixel{0}});
+
+                assert_parse_st(context, mode, "5\x1a"sv, {Sixel{0}});
+                assert_parse_st(context, mode, "\xc2\x1a"sv, {Sixel{0}});
+        }
+}
+
+static void
+test_parser_controls_c0_ignored(void)
+{
+        /* Test that all C0 controls except ESC, CAN, and SUB, are ignored,
+         * that is, parsing a string results in the same parsed item when inserting
+         * the C0 control at any position (except after \xc2 + 0x80..0x9f in UTF-8 mode,
+         * where the \xc2 + C0 produces an U+FFFD (which is ignored) plus the raw C1 which
+         * is itself ignored).
+         */
+
+        auto context = SimpleContext{};
+        auto const str = test_string();
+
+        for (auto c0 = 0; c0 < 0x20; ++c0) {
+                if (c0 == 0x18 /* CAN */ ||
+                    c0 == 0x1a /* SUB */ ||
+                    c0 == 0x1b /* ESC */)
+                        continue;
+
+                char c[1] = {char(c0)};
+                test_parser_insert(context, str, {c, 1});
+
+                assert_parse_st(context, Mode::UTF8, "?\xc2"s + std::string{c, 1} + "\x80@"s, {Sixel{0}, 
Sixel{1}});
+        }
+}
+
+static void
+test_parser_controls_del(void)
+{
+        /* Test that DEL is ignored (except between 0xc2 and 0x80..0x9f in UTF-8 mode) */
+
+        auto context = SimpleContext{};
+
+        for (auto mode : {Mode::UTF8, Mode::EIGHTBIT, Mode::SEVENBIT}) {
+
+                assert_parse_st(context, mode, "!2\x7f;3"sv, {Sequence{Command::DECGRI, {2, 3}}});
+                assert_parse_st(context, mode, "2\x7f;3"sv, ItemList{});
+        }
+
+        assert_parse_st(context, Mode::UTF8, "?\xc2\x7f\x9c", {Sixel{0}});
+}
+
+static void
+test_parser_controls_c1(void)
+{
+        /* Test that any C1 control aborts the parsing at the insertion position,
+         * except in 7-bit mode where C1 controls are ignored.
+         */
+
+        auto context = SimpleContext{};
+        auto const str = test_string();
+        for (auto c1 = 0x80; c1 < 0xa0; ++c1) {
+                if (c1 == 0x9c /* ST */)
+                        continue;
+
+                char c1_utf8[2] = {char(0xc2), char(c1)};
+                test_parser_insert(context, Mode::UTF8, str, {c1_utf8, 2}, ParseStatus::ABORT);
+                test_parser_insert(context, Mode::SEVENBIT, str, {c1_utf8, 2});
+
+                char c1_raw[1] = {char(c1)};
+                test_parser_insert(context, Mode::EIGHTBIT, str, {c1_raw, 2}, ParseStatus::ABORT);
+                test_parser_insert(context, Mode::SEVENBIT, str, {c1_utf8, 2});
+        }
+}
+
+// Main
+
+int
+main(int argc,
+     char* argv[])
+{
+        g_test_init(&argc, &argv, nullptr);
+
+        g_test_add_func("/vte/sixel/parser/sequences/parameters", test_parser_seq_params);
+        g_test_add_func("/vte/sixel/parser/sequences/subparameters", test_parser_seq_subparams);
+        g_test_add_func("/vte/sixel/parser/sequences/parameters-clear", test_parser_seq_params_clear);
+        g_test_add_func("/vte/sixel/parser/sequences/parameters-max", test_parser_seq_params_max);
+        g_test_add_func("/vte/sixel/parser/sequences/glue/arg", test_parser_seq_glue_arg);
+        g_test_add_func("/vte/sixel/parser/st", test_parser_st);
+        g_test_add_func("/vte/sixel/parser/controls/c0/escape", test_parser_controls_c0_esc);
+        g_test_add_func("/vte/sixel/parser/controls/c0/can", test_parser_controls_c0_can);
+        g_test_add_func("/vte/sixel/parser/controls/c0/sub", test_parser_controls_c0_sub);
+        g_test_add_func("/vte/sixel/parser/controls/c0/ignored", test_parser_controls_c0_ignored);
+        g_test_add_func("/vte/sixel/parser/controls/del", test_parser_controls_del);
+        g_test_add_func("/vte/sixel/parser/controls/c1", test_parser_controls_c1);
+
+        return g_test_run();
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]