/* * 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 library. If not, see . */ #include "config.h" #include #include #include #include #include #include #include #include #include #include "sixel-parser.hh" #include "sixel-context.hh" using namespace std::literals; using Command = vte::sixel::Command; using Context = vte::sixel::Context; using Mode = vte::sixel::Parser::Mode; using ParseStatus = vte::sixel::Parser::ParseStatus; // Parser tests static inline constexpr auto param_to_color_register(unsigned reg) { return reg + 2; /* Public colour registers start at 2 */ } 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 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; using ItemList = std::vector; #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(str.data()); auto const endptr = reinterpret_cast(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 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(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 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 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 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 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 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(¶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(parsed_items[1]); for (auto n = 2; n < 8; ++n) g_assert_cmpint(item1.param(n), ==, -1); auto& item2 = std::get(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 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 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}); } } // Context tests class TestContext: public Context { public: using base_type = Context; using base_type::base_type; auto parse(std::string_view const& str) { auto const beginptr = reinterpret_cast(str.data()); auto const endptr = reinterpret_cast(beginptr + str.size()); return Context::parse(beginptr, endptr, true); } }; // class TestContext template static void parse_image(C& context, std::string_view const& str, unsigned fg_red, unsigned fg_green, unsigned fg_blue, unsigned bg_red, unsigned bg_green, unsigned bg_blue, bool private_color_registers = true, int line = __builtin_LINE()) { context.reset(); context.prepare(0x50 /* C0 DCS */, fg_red, fg_green, fg_blue, bg_red, bg_green, bg_blue, false /* bg transparent */, private_color_registers); auto str_st = std::string{str}; str_st.append(ST(StType::C0)); auto [status, ip] = context.parse(str_st); g_assert_cmpint(int(status), ==, int(ParseStatus::COMPLETE)); } template static void parse_image(C& context, ItemList const& items, unsigned fg_red, unsigned fg_green, unsigned fg_blue, unsigned bg_red, unsigned bg_green, unsigned bg_blue, bool private_color_registers = true, int line = __builtin_LINE()) { parse_image(context, ItemStringifier(items).string(), fg_red, fg_green, fg_blue, bg_red, bg_green, bg_blue, private_color_registers, line); } template static void parse_image(C& context, std::string_view const& str, int line = __builtin_LINE()) { parse_image(context, str, 0xffu, 0xffu, 0xffu, 0xff8, 0xffu, 0xffu, true, line); } template static void parse_image(C& context, ItemList const& items, int line = __builtin_LINE()) { parse_image(context, ItemStringifier{items, Mode::UTF8}.string_view(), line); } template static auto parse_pixels(C& context, std::string_view const& str, unsigned extra_width_stride = 0, int line = __builtin_LINE()) { parse_image(context, str, line); auto size = size_t{}; auto ptr = vte::glib::take_free_ptr(context.image_data_indexed(&size, extra_width_stride)); return std::pair{std::move(ptr), size}; } /* BEGIN */ /* The following code is copied from xterm/graphics.c where it is under the * licence below; and modified and used here under the GNU Lesser General Public * Licence, version 3 (or, at your option), any later version. */ /* * Copyright 2013-2019,2020 by Ross Combs * Copyright 2013-2019,2020 by Thomas E. Dickey * * All Rights Reserved * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE ABOVE LISTED COPYRIGHT HOLDER(S) BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * Except as contained in this notice, the name(s) of the above copyright * holders shall not be used in advertising or otherwise to promote the * sale, use or other dealings in this Software without prior written * authorization. */ static void hls2rgb_double(int h, int l, int s, int* r, int* g, int* b) noexcept { const int hs = ((h + 240) / 60) % 6; const double lv = l / 100.0; const double sv = s / 100.0; double c, x, m, c2; double r1, g1, b1; if (s == 0) { *r = *g = *b = (short) (lv * 255. + 0.5); return; } c2 = (2.0 * lv) - 1.0; if (c2 < 0.0) c2 = -c2; c = (1.0 - c2) * sv; x = (hs & 1) ? c : 0.0; m = lv - 0.5 * c; switch (hs) { case 0: r1 = c; g1 = x; b1 = 0.0; break; case 1: r1 = x; g1 = c; b1 = 0.0; break; case 2: r1 = 0.0; g1 = c; b1 = x; break; case 3: r1 = 0.0; g1 = x; b1 = c; break; case 4: r1 = x; g1 = 0.0; b1 = c; break; case 5: r1 = c; g1 = 0.0; b1 = x; break; default: *r = (short) 255; *g = (short) 255; *b = (short) 255; return; } *r = (short) ((r1 + m) * 255.0 + 0.5); *g = (short) ((g1 + m) * 255.0 + 0.5); *b = (short) ((b1 + m) * 255.0 + 0.5); if (*r < 0) *r = 0; else if (*r > 255) *r = 255; if (*g < 0) *g = 0; else if (*g > 255) *g = 255; if (*b < 0) *b = 0; else if (*b > 255) *b = 255; } /* This is essentially Context::make_color_hls from sixel-context.cc, * only changed to return the colour components separately. */ static void hls2rgb_int(int h, int l, int s, int* r, int* g, int* b) noexcept { auto const c2p = std::abs(2 * l - 100); auto const cp = ((100 - c2p) * s) << 1; auto const hs = ((h + 240) / 60) % 6; auto const xp = (hs & 1) ? cp : 0; auto const mp = 200 * l - (cp >> 1); int r1p, g1p, b1p; switch (hs) { case 0: r1p = cp; g1p = xp; b1p = 0; break; case 1: r1p = xp; g1p = cp; b1p = 0; break; case 2: r1p = 0; g1p = cp; b1p = xp; break; case 3: r1p = 0; g1p = xp; b1p = cp; break; case 4: r1p = xp; g1p = 0; b1p = cp; break; case 5: r1p = cp; g1p = 0; b1p = xp; break; default: __builtin_unreachable(); } *r = ((r1p + mp) * 255 + 10000) / 20000; *g = ((g1p + mp) * 255 + 10000) / 20000; *b = ((b1p + mp) * 255 + 10000) / 20000; } /* END */ static void test_context_color_hls(void) { /* Test that our HLS colour conversion gives the right results * by comparing it against the xterm/libsixel implementation. * * The values may differ by 1, which happen only for (L, S) in * {(5, 100), (40, 75), (50, 80), (60, 75), (75, 60), (95, 100)}. * There, one or more of the R, G, B components' unscaled values, * times 255, produces an exact fraction of .5 in hsl2rgb_double, * which, plus 0.5,, and due to inexactness, result in the truncated * value "(short)v" being one less than the result of the integer * computation. */ for (auto h = 0; h <= 360; ++h) { for (auto l = 0; l <= 100; ++l) { for (auto s = 0; s <= 100; ++s) { int rd, gd, bd, ri, gi, bi; hls2rgb_double(h, l, s, &rd, &gd, &bd); hls2rgb_int(h, l, s, &ri, &gi, &bi); g_assert_true((rd == ri || (rd + 1) == ri) && (gd == gi || (gd + 1) == gi) && (bd == bi || (bd + 1) == bi)); } } } } template static void assert_image_dimensions(C& context, unsigned width, unsigned height, int line = __builtin_LINE()) { g_assert_cmpuint(context.image_width(), ==, width); g_assert_cmpuint(context.image_height(), ==, height); } static void test_context_raster_attributes(void) { /* Test that DECGRA sets the image dimensions */ auto context = TestContext{}; parse_image(context, "\"0;0;64;128"sv); assert_image_dimensions(context, 64, 128); } static void test_context_repeat(void) { /* Test that DECGRI repetition works */ auto context = TestContext{}; auto [pixels, size] = parse_pixels(context, "#1!5@"sv); assert_image_dimensions(context, 5, 1); auto data = pixels.get(); auto const v = *data++; for (auto x = 1u; x < context.image_width(); ++x) g_assert_cmpuint(*data++, ==, v); g_assert_cmpuint(size_t(data - pixels.get()), <=, size); /* Check that repeat param 0 is trated as 1 */ parse_image(context, {DECGRI(0), Sixel(1u << 0)}); assert_image_dimensions(context, 1, 1); /* Check that omitted param is treated as default */ parse_image(context, {DECGRI(-1), Sixel(1u << 0)}); assert_image_dimensions(context, 1, 1); } static void test_context_scanlines_grow(void) { /* Test that scanlines grow on demand */ auto context = TestContext{}; parse_image(context, "@$AA$?$??~-~"sv); assert_image_dimensions(context, 3, 12); } static void test_context_scanlines_underfull(void) { /* Test that the image height is determined by the last set sixel, not * necessarily the number of scanlines. */ auto context = TestContext{}; parse_image(context, "?"sv); assert_image_dimensions(context, 1, 0); for (auto n = 0; n < 6; ++n) { parse_image(context, {Sixel(1u << n)}); assert_image_dimensions(context, 1, n + 1); parse_image(context, {Sixel(0), Sixel(0), DECGNL(), Sixel(1u << n)}); assert_image_dimensions(context, 2, 6 + n + 1); } } static void test_context_scanlines_max_width(void) { /* Test that scanlines up to max_width() work, and scanlines longer than that * are accepted but do not write outside the maximum width. */ auto context = TestContext{}; parse_image(context, {Sixel(1u << 0), DECGNL(), DECGRI(context.max_width() - 1), Sixel(0x3f)}); assert_image_dimensions(context, context.max_width() - 1, 12); parse_image(context, {Sixel(1u << 0), DECGNL(), DECGRI(context.max_width()), Sixel(0x3f)}); assert_image_dimensions(context, context.max_width(), 12); parse_image(context, {Sixel(1u << 0), DECGNL(), DECGRI(context.max_width() + 1), Sixel(0x3f)}); assert_image_dimensions(context, context.max_width(), 12); } static void test_context_scanlines_max_height(void) { /* Test that scanlines up to max_height() work, and scanlines beyond that * are accepted but do nothing. */ auto context = TestContext{}; auto items = ItemList{}; for (auto n = 0u; n < (context.max_height() / 6 - 1); ++n) { if (n > 0) items.emplace_back(DECGNL()); items.emplace_back(Sixel(1u << 5)); } parse_image(context, items); assert_image_dimensions(context, 1, context.max_height() - 6); items.emplace_back(DECGNL()); items.emplace_back(Sixel(1u << 4)); parse_image(context, items); assert_image_dimensions(context, 1, context.max_height() - 1); items.emplace_back(DECGCR()); items.emplace_back(Sixel(1u << 5)); parse_image(context, items); assert_image_dimensions(context, 1, context.max_height()); /* Image cannot grow further */ items.emplace_back(DECGNL()); items.emplace_back(Sixel(1u << 0)); parse_image(context, items); assert_image_dimensions(context, 1, context.max_height()); items.emplace_back(DECGNL()); items.emplace_back(Sixel(1u << 5)); parse_image(context, items); assert_image_dimensions(context, 1, context.max_height()); } static void test_context_image_stride(void) { /* Test that data in the stride padding is set to background */ auto context = TestContext{}; auto const extra_stride = 3u; auto [pixels, size] = parse_pixels(context, "#1~~-~~"sv, extra_stride); assert_image_dimensions(context, 2, 12); auto data = pixels.get(); auto const reg = param_to_color_register(1); for (auto y = 0u; y < context.image_height(); ++y) { for (auto x = 0u; x < context.image_width(); ++x) g_assert_cmpuint(*data++, ==, unsigned(reg)); for (auto e = 0u; e < extra_stride; ++e) g_assert_cmpuint(*data++, ==, 0); } g_assert_cmpuint(size_t(data - pixels.get()), <=, size); } class RGB { public: uint8_t r{0}; uint8_t g{0}; uint8_t b{0}; RGB() = default; ~RGB() = default; RGB(int rv, int gv, int bv) : r(rv), g(gv), b(bv) { } }; static void test_context_image_palette(void) { /* Test that the colour palette is recognised, and that colour registers * wrap around. */ auto make_color_rgb = [](unsigned rp, unsigned gp, unsigned bp) constexpr noexcept -> auto { auto scale = [](unsigned value) constexpr noexcept -> auto { return (value * 255u + 50u) / 100u; }; auto make_color = [](unsigned r, unsigned g, unsigned b) constexpr noexcept -> Context::color_t { if constexpr (std::endian::native == std::endian::little) { return b | g << 8 | r << 16 | 0xffu << 24 /* opaque */; } else if constexpr (std::endian::native == std::endian::big) { return 0xffu /* opaque */ | r << 8 | g << 16 | b << 24; } else { __builtin_unreachable(); } }; return make_color(scale(rp), scale(gp), scale(bp)); }; auto context = TestContext{}; std::array palette; for (auto& p : palette) { p = RGB(g_test_rand_int_range(0, 100), g_test_rand_int_range(0, 100), g_test_rand_int_range(0, 100)); } auto items = ItemList{}; auto reg = context.num_colors(); for (auto const& p : palette) { items.emplace_back(DECGCI_RGB(reg++, p.r, p.g, p.b)); } parse_image(context, items); for (auto n = 0; n < context.num_colors(); ++n) { g_assert_cmpuint(make_color_rgb(palette[n].r, palette[n].g, palette[n].b), ==, context.color(param_to_color_register(n))); } } static void test_context_image_compositing(void) { /* Test that multiple sixels in different colours are composited. */ auto context = TestContext{}; auto [pixels, size] = parse_pixels(context, "#256!24F$#257!24w-#258!24F$#259!24w-#260!24F$#261!24w"sv); auto data = pixels.get(); for (auto y = 0u; y < context.image_height(); ++y) { auto const reg = param_to_color_register((256 + y / 3)); for (auto x = 0u; x < context.image_width(); ++x) g_assert_cmpuint(*data++, ==, reg); } g_assert_cmpuint(size_t(data - pixels.get()), <=, size); } // 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); g_test_add_func("/vte/sixel/context/color/hls", test_context_color_hls); g_test_add_func("/vte/sixel/context/raster-attributes", test_context_raster_attributes); g_test_add_func("/vte/sixel/context/repeat", test_context_repeat); g_test_add_func("/vte/sixel/context/scanlines/grow", test_context_scanlines_grow); g_test_add_func("/vte/sixel/context/scanlines/underfull", test_context_scanlines_underfull); g_test_add_func("/vte/sixel/context/scanlines/max-width", test_context_scanlines_max_width); g_test_add_func("/vte/sixel/context/scanlines/max-height", test_context_scanlines_max_height); g_test_add_func("/vte/sixel/context/image/stride", test_context_image_stride); g_test_add_func("/vte/sixel/context/image/palette", test_context_image_palette); g_test_add_func("/vte/sixel/context/image/compositing", test_context_image_compositing); return g_test_run(); }