Skip to content
API Documentation
BastionGuard™
text_utils.hpp
Go to the documentation of this file.
1// text_utils.hpp -- shared text conversion helpers.
2//
3// Bug #14, #15, #18 fix: previously each filesystem module had its own
4// half-broken UTF-16->ASCII conversion (non-ASCII characters became '_')
5// and an inconsistent sanitize_path_component (some replaced spaces, some
6// silently dropped illegal characters like ':' '*' '\').
7//
8// This header centralises the logic and produces real UTF-8 output, so
9// Italian/European/Asian filenames survive recovery instead of being
10// reduced to underscores.
11
12#pragma once
13
14#include <cstdint>
15#include <string>
16
17namespace text_utils {
18
19// Decode a UTF-16LE buffer of `byte_count` bytes into UTF-8.
20// Stops at the first U+0000 code unit. Handles surrogate pairs.
21inline std::string utf16le_to_utf8(const unsigned char* data, std::size_t byte_count) {
22 std::string out;
23 out.reserve(byte_count);
24 auto read_unit = [&](std::size_t i) -> uint16_t {
25 return static_cast<uint16_t>(data[i]) |
26 (static_cast<uint16_t>(data[i + 1]) << 8);
27 };
28 auto append_utf8 = [&](uint32_t cp) {
29 if (cp < 0x80) {
30 out.push_back(static_cast<char>(cp));
31 } else if (cp < 0x800) {
32 out.push_back(static_cast<char>(0xC0 | (cp >> 6)));
33 out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
34 } else if (cp < 0x10000) {
35 out.push_back(static_cast<char>(0xE0 | (cp >> 12)));
36 out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
37 out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
38 } else if (cp <= 0x10FFFF) {
39 out.push_back(static_cast<char>(0xF0 | (cp >> 18)));
40 out.push_back(static_cast<char>(0x80 | ((cp >> 12) & 0x3F)));
41 out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
42 out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
43 }
44 };
45 std::size_t i = 0;
46 while (i + 1 < byte_count) {
47 const uint16_t unit = read_unit(i);
48 if (unit == 0) break;
49 // High surrogate: combine with next low surrogate
50 if (unit >= 0xD800 && unit <= 0xDBFF && i + 3 < byte_count) {
51 const uint16_t low = read_unit(i + 2);
52 if (low >= 0xDC00 && low <= 0xDFFF) {
53 const uint32_t cp = 0x10000 +
54 ((static_cast<uint32_t>(unit - 0xD800)) << 10) +
55 static_cast<uint32_t>(low - 0xDC00);
56 append_utf8(cp);
57 i += 4;
58 continue;
59 }
60 }
61 // Lone low surrogate or normal BMP code point
62 if (unit >= 0xDC00 && unit <= 0xDFFF) {
63 append_utf8(0xFFFD); // replacement char for invalid surrogate
64 } else {
65 append_utf8(unit);
66 }
67 i += 2;
68 }
69 return out;
70}
71
72// Sanitize one path component for safe writing on any host filesystem.
73// - Allows letters, digits, '.', '-', '_'
74// - Allows printable UTF-8 multibyte sequences (so accented characters survive)
75// - Replaces forbidden ASCII (/, \, :, *, ?, ", <, >, |, control chars) with '_'
76// - Replaces whitespace with '_'
77// - Returns "unnamed" if input is empty after sanitization
78// - Trims trailing dots/spaces (Windows-illegal at end of path component)
79inline std::string sanitize_component(const std::string& in) {
80 // Trim trailing dots and spaces from the INPUT first (Windows rule).
81 std::size_t end = in.size();
82 while (end > 0 && (in[end - 1] == '.' || in[end - 1] == ' ' || in[end - 1] == '\t')) {
83 --end;
84 }
85
86 std::string out;
87 out.reserve(end);
88 for (std::size_t i = 0; i < end; ) {
89 const unsigned char c = static_cast<unsigned char>(in[i]);
90
91 // ASCII branch
92 if (c < 0x80) {
93 // Forbidden filename characters across Windows + POSIX
94 const bool forbidden =
95 c < 0x20 || c == 0x7F ||
96 c == '/' || c == '\\' || c == ':' || c == '*' ||
97 c == '?' || c == '"' || c == '<' || c == '>' || c == '|';
98 if (forbidden) {
99 out.push_back('_');
100 } else if (c == ' ' || c == '\t') {
101 out.push_back('_');
102 } else {
103 out.push_back(static_cast<char>(c));
104 }
105 ++i;
106 continue;
107 }
108
109 // UTF-8 multibyte: count expected length
110 std::size_t expected = 0;
111 if ((c & 0xE0) == 0xC0) expected = 2;
112 else if ((c & 0xF0) == 0xE0) expected = 3;
113 else if ((c & 0xF8) == 0xF0) expected = 4;
114 else {
115 // invalid lead byte
116 out.push_back('_');
117 ++i;
118 continue;
119 }
120 if (i + expected > end) {
121 out.push_back('_');
122 ++i;
123 continue;
124 }
125 // Validate continuation bytes
126 bool valid = true;
127 for (std::size_t k = 1; k < expected; ++k) {
128 if ((static_cast<unsigned char>(in[i + k]) & 0xC0) != 0x80) {
129 valid = false;
130 break;
131 }
132 }
133 if (!valid) {
134 out.push_back('_');
135 ++i;
136 continue;
137 }
138 // Append the whole UTF-8 sequence as-is
139 for (std::size_t k = 0; k < expected; ++k) {
140 out.push_back(in[i + k]);
141 }
142 i += expected;
143 }
144
145 if (out.empty()) out = "unnamed";
146 return out;
147}
148
149} // namespace text_utils
std::string sanitize_component(const std::string &in)
std::string utf16le_to_utf8(const unsigned char *data, std::size_t byte_count)