ACMX 2.136.0
Dual-Backend Real-Time GPU Video Synthesis
Loading...
Searching...
No Matches
input_validation.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cctype>
6#include <fstream>
7#include <stdexcept>
8#include <system_error>
9
10namespace acmxvk::input {
11 namespace {
12
13 [[nodiscard]] std::size_t maximumLength(StringKind kind) {
14 switch (kind) {
16 return MAX_ARGUMENT_BYTES;
20 case StringKind::Url:
21 return MAX_PATH_BYTES;
27 return MAX_TOKEN_BYTES;
30 }
31 return 0;
32 }
33
34 [[nodiscard]] bool isUnicodeNoncharacter(std::uint32_t codepoint) {
35 return (codepoint >= 0xFDD0U && codepoint <= 0xFDEFU) ||
36 (codepoint & 0xFFFFU) == 0xFFFEU ||
37 (codepoint & 0xFFFFU) == 0xFFFFU;
38 }
39
40 [[nodiscard]] bool isUnsafeUnicodeControl(std::uint32_t codepoint) {
41 return codepoint == 0x00ADU || codepoint == 0x061CU ||
42 (codepoint >= 0x200BU && codepoint <= 0x200FU) ||
43 codepoint == 0x2028U || codepoint == 0x2029U ||
44 (codepoint >= 0x202AU && codepoint <= 0x202EU) ||
45 (codepoint >= 0x2060U && codepoint <= 0x206FU) ||
46 codepoint == 0x3164U || codepoint == 0xFEFFU ||
47 isUnicodeNoncharacter(codepoint);
48 }
49
50 [[nodiscard]] std::uint32_t decodeCodepoint(std::string_view value,
51 std::size_t &offset,
52 std::string_view context) {
53 const auto fail = [&]() -> std::uint32_t {
54 throw std::runtime_error(std::string(context) +
55 " contains malformed UTF-8");
56 };
57
58 const auto first = static_cast<unsigned char>(value[offset++]);
59 if (first < 0x80U) {
60 return first;
61 }
62
63 std::size_t continuation_count = 0;
64 std::uint32_t codepoint = 0;
65 std::uint32_t minimum = 0;
66 if (first >= 0xC2U && first <= 0xDFU) {
67 continuation_count = 1;
68 codepoint = first & 0x1FU;
69 minimum = 0x80U;
70 } else if (first >= 0xE0U && first <= 0xEFU) {
71 continuation_count = 2;
72 codepoint = first & 0x0FU;
73 minimum = 0x800U;
74 } else if (first >= 0xF0U && first <= 0xF4U) {
75 continuation_count = 3;
76 codepoint = first & 0x07U;
77 minimum = 0x10000U;
78 } else {
79 return fail();
80 }
81
82 if (continuation_count > value.size() - offset) {
83 return fail();
84 }
85 for (std::size_t index = 0; index < continuation_count; ++index) {
86 const auto next = static_cast<unsigned char>(value[offset++]);
87 if ((next & 0xC0U) != 0x80U) {
88 return fail();
89 }
90 codepoint = (codepoint << 6U) | (next & 0x3FU);
91 }
92 if (codepoint < minimum || codepoint > 0x10FFFFU ||
93 (codepoint >= 0xD800U && codepoint <= 0xDFFFU)) {
94 return fail();
95 }
96 return codepoint;
97 }
98
99 [[nodiscard]] bool isTokenCharacter(unsigned char character) {
100 return std::isalnum(character) != 0 || character == '_' ||
101 character == '-' || character == '.' || character == '+';
102 }
103
104 [[nodiscard]] bool isStructuredCharacter(unsigned char character) {
105 constexpr std::string_view PUNCTUATION = "_.,:=+\\-/@%{}[]() ";
106 return character == '\t' || std::isalnum(character) != 0 ||
107 PUNCTUATION.find(static_cast<char>(character)) !=
108 std::string_view::npos;
109 }
110
111 void validateUrl(std::string_view value, std::string_view context) {
112 const std::size_t separator = value.find("://");
113 if (separator == std::string_view::npos || separator == 0 ||
114 separator + 3 >= value.size()) {
115 throw std::runtime_error(std::string(context) +
116 " is not a valid URL");
117 }
118 const std::string_view scheme = value.substr(0, separator);
119 if (std::isalpha(static_cast<unsigned char>(scheme.front())) == 0 ||
120 !std::all_of(scheme.begin() + 1, scheme.end(), [](char value) {
121 const auto character = static_cast<unsigned char>(value);
122 return std::isalnum(character) != 0 || character == '+' ||
123 character == '-' || character == '.';
124 })) {
125 throw std::runtime_error(std::string(context) +
126 " has an invalid URL scheme");
127 }
128 constexpr std::array<std::string_view, 5> ALLOWED_SCHEMES{
129 "http", "https", "file", "rtsp", "rtmp"};
130 std::string lowered(scheme);
131 std::transform(lowered.begin(), lowered.end(), lowered.begin(),
132 [](unsigned char character) {
133 return static_cast<char>(std::tolower(character));
134 });
135 if (std::find(ALLOWED_SCHEMES.begin(), ALLOWED_SCHEMES.end(),
136 lowered) == ALLOWED_SCHEMES.end()) {
137 throw std::runtime_error(std::string(context) +
138 " uses an unsupported URL scheme");
139 }
140 const std::string_view payload = value.substr(separator + 3);
141 if (std::any_of(payload.begin(), payload.end(), [](char value) {
142 const auto character = static_cast<unsigned char>(value);
143 return character == ' ' || character == '\\' ||
144 character == '<' || character == '>' ||
145 character == '"' || character == '{' ||
146 character == '}';
147 })) {
148 throw std::runtime_error(std::string(context) +
149 " contains an invalid URL character");
150 }
151 if (lowered != "file") {
152 const std::size_t authority_end = payload.find_first_of("/?#");
153 if (authority_end == 0) {
154 throw std::runtime_error(std::string(context) +
155 " is missing a URL host");
156 }
157 }
158 for (std::size_t index = 0; index < payload.size(); ++index) {
159 if (payload[index] != '%') {
160 continue;
161 }
162 if (index + 2 >= payload.size() ||
163 std::isxdigit(static_cast<unsigned char>(
164 payload[index + 1])) == 0 ||
165 std::isxdigit(static_cast<unsigned char>(
166 payload[index + 2])) == 0) {
167 throw std::runtime_error(std::string(context) +
168 " contains an invalid URL escape");
169 }
170 index += 2;
171 }
172 }
173
174 } // namespace
175
176 void validate_string(std::string_view value, StringKind kind,
177 std::string_view context, bool allow_empty) {
178 if (value.empty()) {
179 if (allow_empty) {
180 return;
181 }
182 throw std::runtime_error(std::string(context) + " must not be empty");
183 }
184 if (value.size() > maximumLength(kind)) {
185 throw std::runtime_error(std::string(context) + " is too long");
186 }
187
188 std::size_t offset = 0;
189 while (offset < value.size()) {
190 const std::uint32_t codepoint =
191 decodeCodepoint(value, offset, context);
192 const bool allowed_tab =
195 codepoint == '\t';
196 if ((!allowed_tab && codepoint < 0x20U) || codepoint == 0x7FU ||
197 (codepoint >= 0x80U && codepoint < 0xA0U) ||
198 isUnsafeUnicodeControl(codepoint)) {
199 throw std::runtime_error(std::string(context) +
200 " contains a disallowed control character");
201 }
202 if ((kind == StringKind::Identifier || kind == StringKind::Token ||
204 codepoint > 0x7FU) {
205 throw std::runtime_error(std::string(context) +
206 " must contain ASCII characters only");
207 }
208 }
209
210 if (kind == StringKind::Identifier) {
211 const auto first = static_cast<unsigned char>(value.front());
212 if ((std::isalpha(first) == 0 && first != '_') ||
213 !std::all_of(value.begin() + 1, value.end(), [](char value) {
214 const auto character = static_cast<unsigned char>(value);
215 return std::isalnum(character) != 0 || character == '_';
216 })) {
217 throw std::runtime_error(std::string(context) +
218 " contains an invalid identifier");
219 }
220 } else if (kind == StringKind::Token) {
221 if (!std::all_of(value.begin(), value.end(), [](char value) {
222 return isTokenCharacter(
223 static_cast<unsigned char>(value));
224 })) {
225 throw std::runtime_error(std::string(context) +
226 " contains a disallowed token character");
227 }
228 } else if (kind == StringKind::StructuredValue) {
229 if (!std::all_of(value.begin(), value.end(), [](char value) {
230 return isStructuredCharacter(
231 static_cast<unsigned char>(value));
232 })) {
233 throw std::runtime_error(
234 std::string(context) +
235 " contains a disallowed structured-value character");
236 }
237 } else if (kind == StringKind::Url) {
238 validateUrl(value, context);
239 }
240 }
241
242 void validate_file_size(const std::filesystem::path &path,
243 std::string_view context,
244 std::uintmax_t maximum_bytes) {
245 std::error_code error;
246 const std::uintmax_t size = std::filesystem::file_size(path, error);
247 if (error) {
248 throw std::runtime_error("unable to inspect " +
249 std::string(context));
250 }
251 if (size > maximum_bytes) {
252 throw std::runtime_error(std::string(context) +
253 " exceeds the allowed file size");
254 }
255 }
256
257 void validate_spirv_file(const std::filesystem::path &path,
258 std::string_view context) {
259 constexpr std::uintmax_t MAX_SHADER_BYTES = 64U * 1024U * 1024U;
260 std::error_code error;
261 const std::uintmax_t size = std::filesystem::file_size(path, error);
262 if (error || size < 20U || size > MAX_SHADER_BYTES || size % 4U != 0U) {
263 throw std::runtime_error(std::string(context) +
264 " has an invalid SPIR-V file size");
265 }
266
267 std::ifstream shader(path, std::ios::binary);
268 std::array<unsigned char, 4> magic{};
269 if (!shader.read(reinterpret_cast<char *>(magic.data()),
270 static_cast<std::streamsize>(magic.size()))) {
271 throw std::runtime_error("unable to read " + std::string(context));
272 }
273 const std::uint32_t word = static_cast<std::uint32_t>(magic[0]) |
274 (static_cast<std::uint32_t>(magic[1]) << 8U) |
275 (static_cast<std::uint32_t>(magic[2]) << 16U) |
276 (static_cast<std::uint32_t>(magic[3]) << 24U);
277 if (word != 0x07230203U) {
278 throw std::runtime_error(std::string(context) +
279 " does not contain SPIR-V bytecode");
280 }
281 }
282
283 void validate_text_file(const std::filesystem::path &path,
284 std::string_view context,
285 std::uintmax_t maximum_bytes,
286 std::size_t maximum_line_bytes) {
287 validate_file_size(path, context, maximum_bytes);
288 std::ifstream text_file(path, std::ios::binary);
289 if (!text_file) {
290 throw std::runtime_error("unable to open " + std::string(context));
291 }
292 std::string line;
293 std::size_t line_number = 1;
294 while (read_bounded_line(text_file, line, context, line_number++,
295 maximum_line_bytes)) {
296 }
297 }
298
299 std::string truncate_utf8(std::string_view value, std::size_t maximum_bytes,
300 std::string_view suffix) {
301 if (value.size() <= maximum_bytes) {
302 return std::string(value);
303 }
304 if (suffix.size() >= maximum_bytes) {
305 return std::string(suffix.substr(0, maximum_bytes));
306 }
307
308 std::size_t end = maximum_bytes - suffix.size();
309 while (end > 0 && end < value.size() &&
310 (static_cast<unsigned char>(value[end]) & 0xC0U) == 0x80U) {
311 --end;
312 }
313 std::string result(value.substr(0, end));
314 result.append(suffix);
315 return result;
316 }
317
318 bool read_bounded_line(std::istream &input, std::string &line,
319 std::string_view context, std::size_t line_number,
320 std::size_t maximum_bytes) {
321 line.clear();
322 char character = 0;
323 bool read_anything = false;
324 while (input.get(character)) {
325 read_anything = true;
326 if (character == '\n') {
327 break;
328 }
329 if (line.size() >= maximum_bytes) {
330 throw std::runtime_error(std::string(context) + " line " +
331 std::to_string(line_number) +
332 " exceeds the allowed length");
333 }
334 line.push_back(character);
335 }
336 if (!read_anything) {
337 return false;
338 }
339 if (!line.empty() && line.back() == '\r') {
340 line.pop_back();
341 }
342 if (line_number == 1 && line.size() >= 3 &&
343 static_cast<unsigned char>(line[0]) == 0xEFU &&
344 static_cast<unsigned char>(line[1]) == 0xBBU &&
345 static_cast<unsigned char>(line[2]) == 0xBFU) {
346 line.erase(0, 3);
347 }
349 return true;
350 }
351
352} // namespace acmxvk::input
void validateUrl(std::string_view value, std::string_view context)
std::uint32_t decodeCodepoint(std::string_view value, std::size_t &offset, std::string_view context)
constexpr std::size_t MAX_CONFIGURATION_LINE_BYTES
bool read_bounded_line(std::istream &input, std::string &line, std::string_view context, std::size_t line_number, std::size_t maximum_bytes)
constexpr std::size_t MAX_PATH_BYTES
void validate_file_size(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes)
void validate_spirv_file(const std::filesystem::path &path, std::string_view context)
constexpr std::size_t MAX_TOKEN_BYTES
constexpr std::size_t MAX_STRUCTURED_VALUE_BYTES
std::string truncate_utf8(std::string_view value, std::size_t maximum_bytes, std::string_view suffix)
constexpr std::size_t MAX_IDENTIFIER_BYTES
constexpr std::size_t MAX_ARGUMENT_BYTES
constexpr std::size_t MAX_DISPLAY_TEXT_BYTES
void validate_text_file(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes, std::size_t maximum_line_bytes)
void validate_string(std::string_view value, StringKind kind, std::string_view context, bool allow_empty)