[vte] lib: Add new SIXEL parser and test suite
- From: Christian Persch <chpe src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [vte] lib: Add new SIXEL parser and test suite
- Date: Sun, 18 Oct 2020 22:17:08 +0000 (UTC)
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>(¶ms[0], ¶ms[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]