Skip to content

Commit

Permalink
Attempt to print strings of arbitrary character type
Browse files Browse the repository at this point in the history
Previously, this could cause compiler errors for funky character types
  • Loading branch information
jimporter committed May 3, 2024
1 parent 8cf819a commit ab0b80d
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 87 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- `bencode.hpp` is now installed alongside mettle when `--vendorize` is used
- Arithmetic matchers now use ADL to find `max` and `abs`, allowing custom
arithmetic types to use their own implementations of these functions
- Attempt to print strings of arbitrary character type in messages

### Breaking changes
- Implementation updated to require C++17
Expand Down
1 change: 1 addition & 0 deletions doc/about/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ in progress
- `bencode.hpp` is now installed alongside mettle when `--vendorize` is used
- Arithmetic matchers now use ADL to find `max` and `abs`, allowing custom
arithmetic types to use their own implementations of these functions
- Attempt to print strings of arbitrary character type in messages

### Breaking changes
- Implementation updated to require C++17
Expand Down
4 changes: 2 additions & 2 deletions include/mettle/matchers/regex.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace mettle {
std::regex_constants::ECMAScript,
std::regex_constants::match_flag_type match = {}) {
std::ostringstream ss;
ss << "matched " << escape_string(string_convert(ex), '/');
ss << "matched " << represent_string(ex, '/');
if(syntax & std::regex_constants::icase)
ss << "i";

Expand All @@ -39,7 +39,7 @@ namespace mettle {
std::regex_constants::ECMAScript,
std::regex_constants::match_flag_type match = {}) {
std::ostringstream ss;
ss << "searched " << escape_string(string_convert(ex), '/');
ss << "searched " << represent_string(ex, '/');
if(syntax & std::regex_constants::icase)
ss << "i";

Expand Down
81 changes: 40 additions & 41 deletions include/mettle/output/string.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,20 @@ namespace mettle {

namespace detail {

template<typename Char, typename Traits>
void escape_char(std::basic_ostream<Char, Traits> &os, Char c, Char delim) {
inline void escape_char(std::ostream &os, char c, char delim) {
const char escape = '\\';
if(c < 32 || c == 0x7f) {
os << escape;
switch(c) {
case '\0': os << os.widen('0'); break;
case '\a': os << os.widen('a'); break;
case '\b': os << os.widen('b'); break;
case '\f': os << os.widen('f'); break;
case '\n': os << os.widen('n'); break;
case '\r': os << os.widen('r'); break;
case '\t': os << os.widen('t'); break;
case '\v': os << os.widen('v'); break;
default: os << os.widen('x') << static_cast<unsigned long>(c);
case '\0': os << '0'; break;
case '\a': os << 'a'; break;
case '\b': os << 'b'; break;
case '\f': os << 'f'; break;
case '\n': os << 'n'; break;
case '\r': os << 'r'; break;
case '\t': os << 't'; break;
case '\v': os << 'v'; break;
default: os << 'x' << static_cast<unsigned long>(c);
}
} else if(c == delim || c == escape) {
os << escape << c;
Expand All @@ -35,50 +34,29 @@ namespace mettle {

}

template<typename Char, typename Traits,
typename Alloc = std::allocator<Char>>
std::basic_string<Char, Traits, Alloc>
escape_string(const std::basic_string_view<Char, Traits> &s,
Char delim = '"') {
std::basic_ostringstream<Char, Traits, Alloc> ss;
inline std::string
escape_string(const std::string_view &s, char delim = '"') {
std::ostringstream ss;
ss << std::hex << delim;
for(const auto &c : s)
detail::escape_char(ss, c, delim);
ss << delim;
return ss.str();
}

template<typename Char, typename Traits, typename Alloc>
std::basic_string<Char, Traits>
escape_string(const std::basic_string<Char, Traits, Alloc> &s,
Char delim = '"') {
return escape_string<Char, Traits, Alloc>(
std::basic_string_view<Char, Traits>(s), delim
);
}

template<typename Char, typename Traits = std::char_traits<Char>,
typename Alloc = std::allocator<Char>>
std::basic_string<Char, Traits, Alloc>
escape_string(const Char *s, Char delim = '"') {
return escape_string<Char, Traits, Alloc>(
std::basic_string_view<Char>(s), delim
);
}

inline std::string_view
string_convert(const std::string_view &s) {
convert_string(const std::string_view &s) {
return s;
}

inline std::string_view
string_convert(const std::basic_string_view<unsigned char> &s) {
convert_string(const std::basic_string_view<unsigned char> &s) {
auto begin = reinterpret_cast<const char *>(s.data());
return std::string_view(begin, s.size());
}

inline std::string_view
string_convert(const std::basic_string_view<signed char> &s) {
convert_string(const std::basic_string_view<signed char> &s) {
auto begin = reinterpret_cast<const char *>(s.data());
return std::string_view(begin, s.size());
}
Expand All @@ -93,13 +71,13 @@ namespace mettle {
#endif

inline std::string
string_convert(const std::wstring_view &s) {
convert_string(const std::wstring_view &s) {
std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> conv;
return conv.to_bytes(s.data(), s.data() + s.size());
}

inline std::string
string_convert(const std::u16string_view &s) {
convert_string(const std::u16string_view &s) {
#if defined(_MSC_VER) && !defined(__clang__)
// MSVC's codecvt expects uint16_t instead of char16_t because char16_t
// used to just be a typedef of uint16_t.
Expand All @@ -116,7 +94,7 @@ namespace mettle {
}

inline std::string
string_convert(const std::u32string_view &s) {
convert_string(const std::u32string_view &s) {
#if defined(_MSC_VER) && !defined(__clang__)
// MSVC's codecvt expects uint32_t instead of char32_t because char32_t
// used to just be a typedef of uint32_t.
Expand All @@ -137,6 +115,27 @@ namespace mettle {
# pragma GCC diagnostic pop
#endif

namespace detail {
template<typename, typename = std::void_t<>>
struct is_string_convertible : std::false_type {};

template<typename T>
struct is_string_convertible<T, std::void_t<
decltype(convert_string(std::declval<T&>()))
>> : std::true_type {};
}

template<typename String>
inline auto
represent_string(const String &s,
[[maybe_unused]] char delim = '"') { // Silence GCC < 10.
if constexpr(detail::is_string_convertible<String>::value) {
return escape_string(convert_string(s), delim);
} else {
return std::string("(unrepresentable string)");
}
}

} // namespace mettle

#endif
18 changes: 9 additions & 9 deletions include/mettle/output/to_printable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,45 +28,45 @@ namespace mettle {
template<typename Char, typename Traits, typename Alloc>
inline std::string
to_printable(const std::basic_string<Char, Traits, Alloc> &s) {
return escape_string(string_convert(s));
return represent_string(s);
}

template<typename Char, typename Traits>
inline std::string
to_printable(const std::basic_string_view<Char, Traits> &s) {
return escape_string(string_convert(s));
return represent_string(s);
}

inline std::string to_printable(char c) {
return escape_string(std::string(1, c), '\'');
return represent_string(std::string(1, c), '\'');
}

inline std::string to_printable(unsigned char c) {
return escape_string(std::string(1, c), '\'');
return represent_string(std::string(1, c), '\'');
}

inline std::string to_printable(signed char c) {
return escape_string(std::string(1, c), '\'');
return represent_string(std::string(1, c), '\'');
}

inline std::string to_printable(wchar_t c) {
return escape_string(string_convert(std::wstring(1, c)), '\'');
return represent_string(std::wstring(1, c), '\'');
}

inline std::string to_printable(char16_t c) {
return escape_string(string_convert(std::u16string(1, c)), '\'');
return represent_string(std::u16string(1, c), '\'');
}

inline std::string to_printable(char32_t c) {
return escape_string(string_convert(std::u32string(1, c)), '\'');
return represent_string(std::u32string(1, c), '\'');
}

template<typename T>
inline auto to_printable(const T *s) -> std::enable_if_t<
is_any_char_v<T>, std::string
> {
if(!s) return to_printable(nullptr);
return escape_string(string_convert(s));
return represent_string(s);
}

template<typename Ret, typename ...Args>
Expand Down
97 changes: 62 additions & 35 deletions test/output/test_string_output.cpp
Original file line number Diff line number Diff line change
@@ -1,65 +1,92 @@
#include <mettle.hpp>
using namespace mettle;

#include <cstring>

template<typename Char>
std::basic_string<Char> make_string(const char *s) {
std::size_t n = std::strlen(s);
std::basic_string<Char> result(n, '\0');
for(std::size_t i = 0; i != n; i++)
result[i] = s[i];
return result;
}

suite<> test_string_output("string output", [](auto &_) {
subsuite<>(_, "escape_string()", [](auto &_) {
_.test("char", []() {
_.test("std::string", []() {
using namespace std::literals::string_literals;
expect(escape_string("text"s), equal_to("\"text\""));
});

_.test("std::string_view", []() {
using namespace std::literals::string_view_literals;
expect(escape_string("text"sv), equal_to("\"text\""));
});

_.test("char *", []() {
expect(escape_string("text"), equal_to("\"text\""));
});

_.test("escaping", []() {
expect(escape_string("\"text\""), equal_to("\"\\\"text\\\"\""));
expect(escape_string("te\\xt"), equal_to("\"te\\\\xt\""));
expect(escape_string("text\nmore"), equal_to("\"text\\nmore\""));
expect(escape_string(std::string{'a', '\0', 'b'}), equal_to("\"a\\0b\""));
expect(escape_string("\a\b\f\n\r\t\v\x1f"),
equal_to("\"\\a\\b\\f\\n\\r\\t\\v\\x1f\""));
expect(escape_string(std::string("text")), equal_to("\"text\""));
expect(escape_string(std::string{'a', '\0', 'b'}), equal_to("\"a\\0b\""));
});

_.test("delimiter", []() {
expect(escape_string("'text'", '\''), equal_to("'\\'text\\''"));
expect(escape_string("te\\xt", '\''), equal_to("'te\\\\xt'"));
});
});

_.test("wchar_t", []() {
expect(escape_string(L"text"), equal_to(L"\"text\""));
expect(escape_string(L"\"text\""), equal_to(L"\"\\\"text\\\"\""));
expect(escape_string(L"text\nmore"), equal_to(L"\"text\\nmore\""));
expect(escape_string(L"\a\b\f\n\r\t\v\x1f"),
equal_to(L"\"\\a\\b\\f\\n\\r\\t\\v\\x1f\""));
expect(escape_string(std::wstring(L"text")), equal_to(L"\"text\""));
expect(escape_string(std::wstring{'a', '\0', 'b'}),
equal_to(L"\"a\\0b\""));

expect(escape_string(L"'text'", L'\''), equal_to(L"'\\'text\\''"));
subsuite<
char, unsigned char, signed char, wchar_t, char16_t, char32_t
>(_, "convert_string()", type_only, [](auto &_) {
using Char = fixture_type_t<decltype(_)>;
auto S = &make_string<Char>;

_.test("std::basic_string", [S]() {
expect(convert_string(S("text")), equal_to("text"));
expect(convert_string(std::basic_string<Char>{'a', '\0', 'b'}),
equal_to(std::string{'a', '\0', 'b'}));
});

_.test("std::basic_string_view", [S]() {
using sv = std::basic_string_view<Char>;
expect(convert_string(sv(S("text"))), equal_to("text"));
expect(convert_string(sv(std::basic_string<Char>{'a', '\0', 'b'})),
equal_to(std::string{'a', '\0', 'b'}));
});

_.test("C string", [S]() {
expect(convert_string(S("text").c_str()), equal_to("text"));
});
});

subsuite<>(_, "string_convert()", [](auto &_) {
subsuite<>(_, "represent_string()", [](auto &_) {
_.test("char", []() {
std::string with_nul{'a', '\0', 'b'};
expect(string_convert("text"), equal_to("text"));
expect(string_convert(std::string("text")), equal_to("text"));
expect(string_convert(std::string{'a', '\0', 'b'}),
equal_to(std::string{'a', '\0', 'b'}));
expect(represent_string("text"), equal_to("\"text\""));
});

_.test("wchar_t", []() {
std::wstring with_nul{'a', '\0', 'b'};
expect(string_convert(L"text"), equal_to("text"));
expect(string_convert(std::wstring(L"text")), equal_to("text"));
expect(string_convert(std::wstring{'a', '\0', 'b'}),
equal_to(std::string{'a', '\0', 'b'}));
expect(represent_string(L"text"), equal_to("\"text\""));
});

_.test("char16_t", []() {
std::u16string with_nul{'a', '\0', 'b'};
expect(string_convert(u"text"), equal_to("text"));
expect(string_convert(std::u16string(u"text")), equal_to("text"));
expect(string_convert(std::u16string{'a', '\0', 'b'}),
equal_to(std::string{'a', '\0', 'b'}));
expect(represent_string(u"text"), equal_to("\"text\""));
});

_.test("char32_t", []() {
std::u32string with_nul{'a', '\0', 'b'};
expect(string_convert(U"text"), equal_to("text"));
expect(string_convert(std::u32string(U"text")), equal_to("text"));
expect(string_convert(std::u32string{'a', '\0', 'b'}),
equal_to(std::string{'a', '\0', 'b'}));
expect(represent_string(U"text"), equal_to("\"text\""));
});

_.test("int", []() {
expect(represent_string(std::basic_string<int>{'t', 'e', 'x', 't'}),
equal_to("(unrepresentable string)"));
});
});
});

0 comments on commit ab0b80d

Please sign in to comment.