Skip to content
API Documentation
BastionGuard™
hfs_unicode.cpp
Go to the documentation of this file.
1// hfs_unicode.cpp -- UTF-16BE decoding + best-effort NFD -> NFC.
2//
3// We decode UTF-16BE into a std::vector<uint32_t> of Unicode code points,
4// run the NFC compositor over THAT (so we don't need to parse multi-byte
5// UTF-8 sequences), and finally encode to UTF-8 for output.
6//
7// The composition table is finite -- see comments in the file. It covers
8// the practical European-language cases. Out-of-table sequences pass
9// through unchanged.
10
11#include "hfs_unicode.hpp"
12
13#include <algorithm>
14#include <cstdint>
15#include <cstring>
16#include <string>
17#include <unordered_map>
18#include <vector>
19
20namespace hfs {
21
22namespace {
23
24// ----- UTF-16BE -> code-point vector -----
25
26std::vector<uint32_t> utf16be_to_codepoints(const unsigned char* src,
27 size_t code_units) {
28 std::vector<uint32_t> out;
29 out.reserve(code_units);
30 for (size_t i = 0; i < code_units; ++i) {
31 const unsigned char* p = src + i * 2;
32 const uint16_t u = static_cast<uint16_t>((p[0] << 8) | p[1]);
33 if (u >= 0xD800 && u <= 0xDBFF) {
34 // High surrogate, expect a low surrogate next
35 if (i + 1 < code_units) {
36 const unsigned char* q = src + (i + 1) * 2;
37 const uint16_t v = static_cast<uint16_t>((q[0] << 8) | q[1]);
38 if (v >= 0xDC00 && v <= 0xDFFF) {
39 const uint32_t cp = 0x10000u
40 + ((static_cast<uint32_t>(u - 0xD800) << 10)
41 | (v - 0xDC00));
42 out.push_back(cp);
43 ++i;
44 continue;
45 }
46 }
47 out.push_back(0xFFFD); // lone high surrogate
48 } else if (u >= 0xDC00 && u <= 0xDFFF) {
49 out.push_back(0xFFFD); // unexpected low surrogate
50 } else {
51 out.push_back(u);
52 }
53 }
54 return out;
55}
56
57// ----- Code-point vector -> UTF-8 -----
58
59void append_utf8(std::string& out, uint32_t cp) {
60 if (cp < 0x80) {
61 out.push_back(static_cast<char>(cp));
62 } else if (cp < 0x800) {
63 out.push_back(static_cast<char>(0xC0 | (cp >> 6)));
64 out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
65 } else if (cp < 0x10000) {
66 out.push_back(static_cast<char>(0xE0 | (cp >> 12)));
67 out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
68 out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
69 } else if (cp < 0x110000) {
70 out.push_back(static_cast<char>(0xF0 | (cp >> 18)));
71 out.push_back(static_cast<char>(0x80 | ((cp >> 12) & 0x3F)));
72 out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
73 out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
74 } else {
75 // Out of Unicode range -- replace with U+FFFD
76 out.push_back(static_cast<char>(0xEF));
77 out.push_back(static_cast<char>(0xBF));
78 out.push_back(static_cast<char>(0xBD));
79 }
80}
81
82std::string codepoints_to_utf8(const std::vector<uint32_t>& cps) {
83 std::string out;
84 out.reserve(cps.size() * 2);
85 for (uint32_t cp : cps) append_utf8(out, cp);
86 return out;
87}
88
89// ----- NFC composition table -----
90//
91// Key: (base << 32) | combining_mark
92// Value: precomposed code point
93//
94// We populate the table lazily on first call. Coverage:
95// * Latin-1 vowels with grave/acute/circumflex/diaeresis/tilde/ring
96// * c-cedilla, n-tilde
97// * y-acute, y-diaeresis
98// * Latin Extended-A pairs (most accented characters used in
99// Italian, French, Spanish, German, Portuguese, Polish, Czech,
100// Hungarian, Nordic)
101// * Greek tonos (single accent over alpha/epsilon/eta/iota/...)
102//
103// Out-of-table sequences just stay decomposed.
104
105uint64_t make_key(uint32_t base, uint32_t mark) {
106 return (static_cast<uint64_t>(base) << 32) | mark;
107}
108
109const std::unordered_map<uint64_t, uint32_t>& nfc_table() {
110 static const std::unordered_map<uint64_t, uint32_t> t = {
111 // ---- COMBINING GRAVE ACCENT (U+0300) ----
112 { make_key('A', 0x0300), 0x00C0 }, { make_key('a', 0x0300), 0x00E0 },
113 { make_key('E', 0x0300), 0x00C8 }, { make_key('e', 0x0300), 0x00E8 },
114 { make_key('I', 0x0300), 0x00CC }, { make_key('i', 0x0300), 0x00EC },
115 { make_key('O', 0x0300), 0x00D2 }, { make_key('o', 0x0300), 0x00F2 },
116 { make_key('U', 0x0300), 0x00D9 }, { make_key('u', 0x0300), 0x00F9 },
117
118 // ---- COMBINING ACUTE ACCENT (U+0301) ----
119 { make_key('A', 0x0301), 0x00C1 }, { make_key('a', 0x0301), 0x00E1 },
120 { make_key('E', 0x0301), 0x00C9 }, { make_key('e', 0x0301), 0x00E9 },
121 { make_key('I', 0x0301), 0x00CD }, { make_key('i', 0x0301), 0x00ED },
122 { make_key('O', 0x0301), 0x00D3 }, { make_key('o', 0x0301), 0x00F3 },
123 { make_key('U', 0x0301), 0x00DA }, { make_key('u', 0x0301), 0x00FA },
124 { make_key('Y', 0x0301), 0x00DD }, { make_key('y', 0x0301), 0x00FD },
125 { make_key('C', 0x0301), 0x0106 }, { make_key('c', 0x0301), 0x0107 },
126 { make_key('L', 0x0301), 0x0139 }, { make_key('l', 0x0301), 0x013A },
127 { make_key('N', 0x0301), 0x0143 }, { make_key('n', 0x0301), 0x0144 },
128 { make_key('R', 0x0301), 0x0154 }, { make_key('r', 0x0301), 0x0155 },
129 { make_key('S', 0x0301), 0x015A }, { make_key('s', 0x0301), 0x015B },
130 { make_key('Z', 0x0301), 0x0179 }, { make_key('z', 0x0301), 0x017A },
131
132 // ---- COMBINING CIRCUMFLEX (U+0302) ----
133 { make_key('A', 0x0302), 0x00C2 }, { make_key('a', 0x0302), 0x00E2 },
134 { make_key('E', 0x0302), 0x00CA }, { make_key('e', 0x0302), 0x00EA },
135 { make_key('I', 0x0302), 0x00CE }, { make_key('i', 0x0302), 0x00EE },
136 { make_key('O', 0x0302), 0x00D4 }, { make_key('o', 0x0302), 0x00F4 },
137 { make_key('U', 0x0302), 0x00DB }, { make_key('u', 0x0302), 0x00FB },
138 { make_key('C', 0x0302), 0x0108 }, { make_key('c', 0x0302), 0x0109 },
139 { make_key('G', 0x0302), 0x011C }, { make_key('g', 0x0302), 0x011D },
140 { make_key('H', 0x0302), 0x0124 }, { make_key('h', 0x0302), 0x0125 },
141 { make_key('J', 0x0302), 0x0134 }, { make_key('j', 0x0302), 0x0135 },
142 { make_key('S', 0x0302), 0x015C }, { make_key('s', 0x0302), 0x015D },
143 { make_key('W', 0x0302), 0x0174 }, { make_key('w', 0x0302), 0x0175 },
144 { make_key('Y', 0x0302), 0x0176 }, { make_key('y', 0x0302), 0x0177 },
145
146 // ---- COMBINING TILDE (U+0303) ----
147 { make_key('A', 0x0303), 0x00C3 }, { make_key('a', 0x0303), 0x00E3 },
148 { make_key('N', 0x0303), 0x00D1 }, { make_key('n', 0x0303), 0x00F1 },
149 { make_key('O', 0x0303), 0x00D5 }, { make_key('o', 0x0303), 0x00F5 },
150 { make_key('I', 0x0303), 0x0128 }, { make_key('i', 0x0303), 0x0129 },
151 { make_key('U', 0x0303), 0x0168 }, { make_key('u', 0x0303), 0x0169 },
152
153 // ---- COMBINING DIAERESIS (U+0308) ----
154 { make_key('A', 0x0308), 0x00C4 }, { make_key('a', 0x0308), 0x00E4 },
155 { make_key('E', 0x0308), 0x00CB }, { make_key('e', 0x0308), 0x00EB },
156 { make_key('I', 0x0308), 0x00CF }, { make_key('i', 0x0308), 0x00EF },
157 { make_key('O', 0x0308), 0x00D6 }, { make_key('o', 0x0308), 0x00F6 },
158 { make_key('U', 0x0308), 0x00DC }, { make_key('u', 0x0308), 0x00FC },
159 { make_key('Y', 0x0308), 0x0178 }, { make_key('y', 0x0308), 0x00FF },
160
161 // ---- COMBINING RING ABOVE (U+030A) ----
162 { make_key('A', 0x030A), 0x00C5 }, { make_key('a', 0x030A), 0x00E5 },
163 { make_key('U', 0x030A), 0x016E }, { make_key('u', 0x030A), 0x016F },
164
165 // ---- COMBINING CEDILLA (U+0327) ----
166 { make_key('C', 0x0327), 0x00C7 }, { make_key('c', 0x0327), 0x00E7 },
167 { make_key('G', 0x0327), 0x0122 }, { make_key('g', 0x0327), 0x0123 },
168 { make_key('K', 0x0327), 0x0136 }, { make_key('k', 0x0327), 0x0137 },
169 { make_key('L', 0x0327), 0x013B }, { make_key('l', 0x0327), 0x013C },
170 { make_key('N', 0x0327), 0x0145 }, { make_key('n', 0x0327), 0x0146 },
171 { make_key('R', 0x0327), 0x0156 }, { make_key('r', 0x0327), 0x0157 },
172 { make_key('S', 0x0327), 0x015E }, { make_key('s', 0x0327), 0x015F },
173 { make_key('T', 0x0327), 0x0162 }, { make_key('t', 0x0327), 0x0163 },
174
175 // ---- COMBINING MACRON (U+0304) ----
176 { make_key('A', 0x0304), 0x0100 }, { make_key('a', 0x0304), 0x0101 },
177 { make_key('E', 0x0304), 0x0112 }, { make_key('e', 0x0304), 0x0113 },
178 { make_key('I', 0x0304), 0x012A }, { make_key('i', 0x0304), 0x012B },
179 { make_key('O', 0x0304), 0x014C }, { make_key('o', 0x0304), 0x014D },
180 { make_key('U', 0x0304), 0x016A }, { make_key('u', 0x0304), 0x016B },
181
182 // ---- COMBINING CARON (U+030C, hacek) ----
183 { make_key('C', 0x030C), 0x010C }, { make_key('c', 0x030C), 0x010D },
184 { make_key('D', 0x030C), 0x010E }, { make_key('d', 0x030C), 0x010F },
185 { make_key('E', 0x030C), 0x011A }, { make_key('e', 0x030C), 0x011B },
186 { make_key('L', 0x030C), 0x013D }, { make_key('l', 0x030C), 0x013E },
187 { make_key('N', 0x030C), 0x0147 }, { make_key('n', 0x030C), 0x0148 },
188 { make_key('R', 0x030C), 0x0158 }, { make_key('r', 0x030C), 0x0159 },
189 { make_key('S', 0x030C), 0x0160 }, { make_key('s', 0x030C), 0x0161 },
190 { make_key('T', 0x030C), 0x0164 }, { make_key('t', 0x030C), 0x0165 },
191 { make_key('Z', 0x030C), 0x017D }, { make_key('z', 0x030C), 0x017E },
192
193 // ---- COMBINING OGONEK (U+0328) ----
194 { make_key('A', 0x0328), 0x0104 }, { make_key('a', 0x0328), 0x0105 },
195 { make_key('E', 0x0328), 0x0118 }, { make_key('e', 0x0328), 0x0119 },
196 { make_key('I', 0x0328), 0x012E }, { make_key('i', 0x0328), 0x012F },
197 { make_key('U', 0x0328), 0x0172 }, { make_key('u', 0x0328), 0x0173 },
198
199 // ---- COMBINING DOUBLE ACUTE (U+030B), Hungarian ----
200 { make_key('O', 0x030B), 0x0150 }, { make_key('o', 0x030B), 0x0151 },
201 { make_key('U', 0x030B), 0x0170 }, { make_key('u', 0x030B), 0x0171 },
202
203 // ---- COMBINING DOT ABOVE (U+0307) ----
204 { make_key('Z', 0x0307), 0x017B }, { make_key('z', 0x0307), 0x017C },
205 };
206 return t;
207}
208
209bool is_combining_mark(uint32_t cp) {
210 // Range of combining diacritical marks (U+0300 .. U+036F) covers
211 // every accent in our table. We don't need the full Unicode
212 // combining-class machinery for this.
213 return cp >= 0x0300 && cp <= 0x036F;
214}
215
216std::vector<uint32_t> compose_nfc(const std::vector<uint32_t>& nfd) {
217 std::vector<uint32_t> out;
218 out.reserve(nfd.size());
219
220 const auto& tbl = nfc_table();
221 size_t i = 0;
222 while (i < nfd.size()) {
223 const uint32_t base = nfd[i];
224 // Look for a single combining mark following.
225 if (i + 1 < nfd.size() && is_combining_mark(nfd[i + 1])) {
226 const uint32_t mark = nfd[i + 1];
227 auto it = tbl.find(make_key(base, mark));
228 if (it != tbl.end()) {
229 out.push_back(it->second);
230 i += 2;
231 // Try to fold any further combining marks if they
232 // recompose with the new precomposed character.
233 while (i < nfd.size() && is_combining_mark(nfd[i])) {
234 auto it2 = tbl.find(make_key(out.back(), nfd[i]));
235 if (it2 == tbl.end()) {
236 out.push_back(nfd[i]);
237 ++i;
238 } else {
239 out.back() = it2->second;
240 ++i;
241 }
242 }
243 continue;
244 }
245 }
246 out.push_back(base);
247 ++i;
248 }
249 return out;
250}
251
252} // anonymous namespace
253
254std::string utf16be_to_utf8(const unsigned char* src, size_t code_units) {
255 if (src == nullptr || code_units == 0) return {};
256 auto cps = utf16be_to_codepoints(src, code_units);
257 return codepoints_to_utf8(cps);
258}
259
260std::string nfd_to_nfc(const std::string& utf8) {
261 // Decode UTF-8 -> code points (we assume well-formed UTF-8 since
262 // it came from utf16be_to_utf8 above).
263 std::vector<uint32_t> cps;
264 cps.reserve(utf8.size());
265 size_t i = 0;
266 while (i < utf8.size()) {
267 const unsigned char b = static_cast<unsigned char>(utf8[i]);
268 uint32_t cp = 0;
269 size_t len = 0;
270 if (b < 0x80) { cp = b; len = 1; }
271 else if ((b & 0xE0) == 0xC0) { cp = b & 0x1F; len = 2; }
272 else if ((b & 0xF0) == 0xE0) { cp = b & 0x0F; len = 3; }
273 else if ((b & 0xF8) == 0xF0) { cp = b & 0x07; len = 4; }
274 else { cp = 0xFFFD; len = 1; }
275 if (i + len > utf8.size()) { cps.push_back(0xFFFD); break; }
276 for (size_t j = 1; j < len; ++j) {
277 cp = (cp << 6) | (static_cast<unsigned char>(utf8[i + j]) & 0x3F);
278 }
279 cps.push_back(cp);
280 i += len;
281 }
282 return codepoints_to_utf8(compose_nfc(cps));
283}
284
285std::string hfs_filename_to_host(const unsigned char* src, size_t code_units) {
286 auto cps = utf16be_to_codepoints(src, code_units);
287 cps = compose_nfc(cps);
288 std::string raw = codepoints_to_utf8(cps);
289
290 // Sanitise for host filesystem: replace NUL and '/' with '_'.
291 // (We don't apply Windows-specific stripping here; the project's
292 // text_utils helpers handle that elsewhere. We just produce a
293 // string safe for posix paths.)
294 std::string out;
295 out.reserve(raw.size());
296 for (char c : raw) {
297 unsigned char uc = static_cast<unsigned char>(c);
298 if (uc == 0 || uc == '/') out.push_back('_');
299 else out.push_back(c);
300 }
301 if (out.empty()) out = "_";
302 return out;
303}
304
305} // namespace hfs
std::vector< uint32_t > compose_nfc(const std::vector< uint32_t > &nfd)
void append_utf8(std::string &out, uint32_t cp)
uint64_t make_key(uint32_t base, uint32_t mark)
std::string codepoints_to_utf8(const std::vector< uint32_t > &cps)
const std::unordered_map< uint64_t, uint32_t > & nfc_table()
std::vector< uint32_t > utf16be_to_codepoints(const unsigned char *src, size_t code_units)
std::string hfs_filename_to_host(const unsigned char *src, size_t code_units)
std::string utf16be_to_utf8(const unsigned char *src, size_t code_units)
std::string nfd_to_nfc(const std::string &utf8)