dasmig-core 1.2.0
Shared random engine, distributions and data tiers for the dasmig generator libraries
Loading...
Searching...
No Matches
utf8.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_CORE_UTF8_HPP
5#define DASMIG_CORE_UTF8_HPP
6
7#include <cstddef>
8#include <cstdint>
9#include <stdexcept>
10#include <string>
11#include <string_view>
12
13/// @file utf8.hpp
14/// @brief Conversion between UTF-8 and `std::wstring`.
15/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
16///
17/// The core works in UTF-8 (`std::string`). Libraries with a `std::wstring`
18/// API convert at the boundary. `wchar_t` is UTF-16 on Windows and UTF-32
19/// elsewhere; both are handled, so the UTF-8 is the same on every platform.
20
21namespace dasmig::core
22{
23
24/// @brief Whether a byte string is well-formed UTF-8.
25///
26/// Rejects overlong forms, surrogates (U+D800 to U+DFFF) and code points
27/// above U+10FFFF.
28///
29/// @param text Bytes to check.
30/// @return The offset of the first invalid byte, or `std::string_view::npos`
31/// when the text is valid.
32[[nodiscard]] constexpr std::size_t utf8_invalid_offset(std::string_view text) noexcept
33{
34 std::size_t i = 0;
35 while (i < text.size())
36 {
37 const auto b0 = static_cast<std::uint8_t>(text[i]);
38 std::size_t len = 0;
39 std::uint32_t cp = 0;
40 if (b0 < 0x80U) { ++i; continue; }
41 if ((b0 & 0xE0U) == 0xC0U) { len = 2; cp = b0 & 0x1FU; }
42 else if ((b0 & 0xF0U) == 0xE0U) { len = 3; cp = b0 & 0x0FU; }
43 else if ((b0 & 0xF8U) == 0xF0U) { len = 4; cp = b0 & 0x07U; }
44 else { return i; }
45 if (i + len > text.size()) return i;
46 for (std::size_t k = 1; k < len; ++k)
47 {
48 const auto b = static_cast<std::uint8_t>(text[i + k]);
49 if ((b & 0xC0U) != 0x80U) return i;
50 cp = (cp << 6U) | (b & 0x3FU);
51 }
52 const bool overlong = (len == 2 && cp < 0x80U) ||
53 (len == 3 && cp < 0x800U) ||
54 (len == 4 && cp < 0x10000U);
55 if (overlong || (cp >= 0xD800U && cp <= 0xDFFFU) || cp > 0x10FFFFU)
56 {
57 return i;
58 }
59 i += len;
60 }
61 return std::string_view::npos;
62}
63
64/// @brief Whether a byte string is well-formed UTF-8.
65/// @param text Bytes to check.
66/// @return True when valid.
67[[nodiscard]] constexpr bool is_utf8(std::string_view text) noexcept
68{
69 return utf8_invalid_offset(text) == std::string_view::npos;
70}
71
72/// @brief Convert a wide string to UTF-8.
73/// @param text UTF-16 (Windows) or UTF-32 text.
74/// @return The UTF-8 encoding.
75/// @throws std::invalid_argument On an unpaired surrogate or a value above
76/// U+10FFFF.
77[[nodiscard]] inline std::string to_utf8(std::wstring_view text)
78{
79 std::string out;
80 out.reserve(text.size());
81 for (std::size_t i = 0; i < text.size(); ++i)
82 {
83 auto cp = static_cast<std::uint32_t>(text[i]);
84 if constexpr (sizeof(wchar_t) == 2)
85 {
86 cp &= 0xFFFFU;
87 if (cp >= 0xD800U && cp <= 0xDBFFU && i + 1 < text.size())
88 {
89 const auto lo = static_cast<std::uint32_t>(text[i + 1]) & 0xFFFFU;
90 if (lo >= 0xDC00U && lo <= 0xDFFFU)
91 {
92 cp = 0x10000U + ((cp - 0xD800U) << 10U) + (lo - 0xDC00U);
93 ++i;
94 }
95 }
96 }
97 if ((cp >= 0xD800U && cp <= 0xDFFFU) || cp > 0x10FFFFU)
98 {
99 throw std::invalid_argument("to_utf8: invalid code point");
100 }
101 if (cp < 0x80U)
102 {
103 out += static_cast<char>(cp);
104 }
105 else if (cp < 0x800U)
106 {
107 out += static_cast<char>(0xC0U | (cp >> 6U));
108 out += static_cast<char>(0x80U | (cp & 0x3FU));
109 }
110 else if (cp < 0x10000U)
111 {
112 out += static_cast<char>(0xE0U | (cp >> 12U));
113 out += static_cast<char>(0x80U | ((cp >> 6U) & 0x3FU));
114 out += static_cast<char>(0x80U | (cp & 0x3FU));
115 }
116 else
117 {
118 out += static_cast<char>(0xF0U | (cp >> 18U));
119 out += static_cast<char>(0x80U | ((cp >> 12U) & 0x3FU));
120 out += static_cast<char>(0x80U | ((cp >> 6U) & 0x3FU));
121 out += static_cast<char>(0x80U | (cp & 0x3FU));
122 }
123 }
124 return out;
125}
126
127/// @brief Convert UTF-8 to a wide string.
128/// @param text UTF-8 text.
129/// @return UTF-16 (Windows) or UTF-32 text.
130/// @throws std::invalid_argument If @p text is not well-formed UTF-8.
131[[nodiscard]] inline std::wstring from_utf8(std::string_view text)
132{
133 if (const auto bad = utf8_invalid_offset(text); bad != std::string_view::npos)
134 {
135 throw std::invalid_argument("from_utf8: invalid UTF-8 at byte " +
136 std::to_string(bad));
137 }
138 std::wstring out;
139 out.reserve(text.size());
140 std::size_t i = 0;
141 while (i < text.size())
142 {
143 const auto b0 = static_cast<std::uint8_t>(text[i]);
144 std::uint32_t cp = 0;
145 std::size_t len = 1;
146 if (b0 < 0x80U) { cp = b0; }
147 else if ((b0 & 0xE0U) == 0xC0U) { cp = b0 & 0x1FU; len = 2; }
148 else if ((b0 & 0xF0U) == 0xE0U) { cp = b0 & 0x0FU; len = 3; }
149 else { cp = b0 & 0x07U; len = 4; }
150 for (std::size_t k = 1; k < len; ++k)
151 {
152 cp = (cp << 6U) | (static_cast<std::uint8_t>(text[i + k]) & 0x3FU);
153 }
154 i += len;
155 if constexpr (sizeof(wchar_t) == 2)
156 {
157 if (cp >= 0x10000U)
158 {
159 cp -= 0x10000U;
160 out += static_cast<wchar_t>(0xD800U + (cp >> 10U));
161 out += static_cast<wchar_t>(0xDC00U + (cp & 0x3FFU));
162 continue;
163 }
164 }
165 out += static_cast<wchar_t>(cp);
166 }
167 return out;
168}
169
170} // namespace dasmig::core
171
172#endif // DASMIG_CORE_UTF8_HPP
constexpr bool is_utf8(std::string_view text) noexcept
Whether a byte string is well-formed UTF-8.
Definition utf8.hpp:67
std::string to_utf8(std::wstring_view text)
Convert a wide string to UTF-8.
Definition utf8.hpp:77
std::wstring from_utf8(std::string_view text)
Convert UTF-8 to a wide string.
Definition utf8.hpp:131
constexpr std::size_t utf8_invalid_offset(std::string_view text) noexcept
Whether a byte string is well-formed UTF-8.
Definition utf8.hpp:32