ACMX 2.139.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) { return (codepoint >= 0xFDD0U && codepoint <= 0xFDEFU) || (codepoint & 0xFFFFU) == 0xFFFEU || (codepoint & 0xFFFFU) == 0xFFFFU; }
35
36 [[nodiscard]] bool isUnsafeUnicodeControl(std::uint32_t codepoint) { return codepoint == 0x00ADU || codepoint == 0x061CU || (codepoint >= 0x200BU && codepoint <= 0x200FU) || codepoint == 0x2028U || codepoint == 0x2029U || (codepoint >= 0x202AU && codepoint <= 0x202EU) || (codepoint >= 0x2060U && codepoint <= 0x206FU) || codepoint == 0x3164U || codepoint == 0xFEFFU || isUnicodeNoncharacter(codepoint); }
37
38 [[nodiscard]] std::uint32_t decodeCodepoint(std::string_view value, std::size_t &offset, std::string_view context) {
39 const auto fail = [&]() -> std::uint32_t { throw std::runtime_error(std::string(context) + " contains malformed UTF-8"); };
40
41 const auto first = static_cast<unsigned char>(value[offset++]);
42 if (first < 0x80U) {
43 return first;
44 }
45
46 std::size_t continuation_count = 0;
47 std::uint32_t codepoint = 0;
48 std::uint32_t minimum = 0;
49 if (first >= 0xC2U && first <= 0xDFU) {
50 continuation_count = 1;
51 codepoint = first & 0x1FU;
52 minimum = 0x80U;
53 } else if (first >= 0xE0U && first <= 0xEFU) {
54 continuation_count = 2;
55 codepoint = first & 0x0FU;
56 minimum = 0x800U;
57 } else if (first >= 0xF0U && first <= 0xF4U) {
58 continuation_count = 3;
59 codepoint = first & 0x07U;
60 minimum = 0x10000U;
61 } else {
62 return fail();
63 }
64
65 if (continuation_count > value.size() - offset) {
66 return fail();
67 }
68 for (std::size_t index = 0; index < continuation_count; ++index) {
69 const auto next = static_cast<unsigned char>(value[offset++]);
70 if ((next & 0xC0U) != 0x80U) {
71 return fail();
72 }
73 codepoint = (codepoint << 6U) | (next & 0x3FU);
74 }
75 if (codepoint < minimum || codepoint > 0x10FFFFU || (codepoint >= 0xD800U && codepoint <= 0xDFFFU)) {
76 return fail();
77 }
78 return codepoint;
79 }
80
81 [[nodiscard]] bool isTokenCharacter(unsigned char character) { return std::isalnum(character) != 0 || character == '_' || character == '-' || character == '.' || character == '+'; }
82
83 [[nodiscard]] bool isStructuredCharacter(unsigned char character) {
84 constexpr std::string_view PUNCTUATION = "_.,:=+\\-/@%{}[]() ";
85 return character == '\t' || std::isalnum(character) != 0 || PUNCTUATION.find(static_cast<char>(character)) != std::string_view::npos;
86 }
87
88 void validateUrl(std::string_view value, std::string_view context) {
89 const std::size_t separator = value.find("://");
90 if (separator == std::string_view::npos || separator == 0 || separator + 3 >= value.size()) {
91 throw std::runtime_error(std::string(context) + " is not a valid URL");
92 }
93 const std::string_view scheme = value.substr(0, separator);
94 if (std::isalpha(static_cast<unsigned char>(scheme.front())) == 0 || !std::all_of(scheme.begin() + 1, scheme.end(), [](char value) {
95 const auto character = static_cast<unsigned char>(value);
96 return std::isalnum(character) != 0 || character == '+' || character == '-' || character == '.';
97 })) {
98 throw std::runtime_error(std::string(context) + " has an invalid URL scheme");
99 }
100 constexpr std::array<std::string_view, 5> ALLOWED_SCHEMES{"http", "https", "file", "rtsp", "rtmp"};
101 std::string lowered(scheme);
102 std::transform(lowered.begin(), lowered.end(), lowered.begin(), [](unsigned char character) { return static_cast<char>(std::tolower(character)); });
103 if (std::find(ALLOWED_SCHEMES.begin(), ALLOWED_SCHEMES.end(), lowered) == ALLOWED_SCHEMES.end()) {
104 throw std::runtime_error(std::string(context) + " uses an unsupported URL scheme");
105 }
106 const std::string_view payload = value.substr(separator + 3);
107 if (std::any_of(payload.begin(), payload.end(), [](char value) {
108 const auto character = static_cast<unsigned char>(value);
109 return character == ' ' || character == '\\' || character == '<' || character == '>' || character == '"' || character == '{' || character == '}';
110 })) {
111 throw std::runtime_error(std::string(context) + " contains an invalid URL character");
112 }
113 if (lowered != "file") {
114 const std::size_t authority_end = payload.find_first_of("/?#");
115 if (authority_end == 0) {
116 throw std::runtime_error(std::string(context) + " is missing a URL host");
117 }
118 }
119 for (std::size_t index = 0; index < payload.size(); ++index) {
120 if (payload[index] != '%') {
121 continue;
122 }
123 if (index + 2 >= payload.size() || std::isxdigit(static_cast<unsigned char>(payload[index + 1])) == 0 || std::isxdigit(static_cast<unsigned char>(payload[index + 2])) == 0) {
124 throw std::runtime_error(std::string(context) + " contains an invalid URL escape");
125 }
126 index += 2;
127 }
128 }
129
130 } // namespace
131
132 void validate_string(std::string_view value, StringKind kind, std::string_view context, bool allow_empty) {
133 if (value.empty()) {
134 if (allow_empty) {
135 return;
136 }
137 throw std::runtime_error(std::string(context) + " must not be empty");
138 }
139 if (value.size() > maximumLength(kind)) {
140 throw std::runtime_error(std::string(context) + " is too long");
141 }
142
143 std::size_t offset = 0;
144 while (offset < value.size()) {
145 const std::uint32_t codepoint = decodeCodepoint(value, offset, context);
146 const bool allowed_tab = (kind == StringKind::ConfigurationLine || kind == StringKind::StructuredValue) && codepoint == '\t';
147 if ((!allowed_tab && codepoint < 0x20U) || codepoint == 0x7FU || (codepoint >= 0x80U && codepoint < 0xA0U) || isUnsafeUnicodeControl(codepoint)) {
148 throw std::runtime_error(std::string(context) + " contains a disallowed control character");
149 }
150 if ((kind == StringKind::Identifier || kind == StringKind::Token || kind == StringKind::StructuredValue) && codepoint > 0x7FU) {
151 throw std::runtime_error(std::string(context) + " must contain ASCII characters only");
152 }
153 }
154
155 if (kind == StringKind::Identifier) {
156 const auto first = static_cast<unsigned char>(value.front());
157 if ((std::isalpha(first) == 0 && first != '_') || !std::all_of(value.begin() + 1, value.end(), [](char value) {
158 const auto character = static_cast<unsigned char>(value);
159 return std::isalnum(character) != 0 || character == '_';
160 })) {
161 throw std::runtime_error(std::string(context) + " contains an invalid identifier");
162 }
163 } else if (kind == StringKind::Token) {
164 if (!std::all_of(value.begin(), value.end(), [](char value) { return isTokenCharacter(static_cast<unsigned char>(value)); })) {
165 throw std::runtime_error(std::string(context) + " contains a disallowed token character");
166 }
167 } else if (kind == StringKind::StructuredValue) {
168 if (!std::all_of(value.begin(), value.end(), [](char value) { return isStructuredCharacter(static_cast<unsigned char>(value)); })) {
169 throw std::runtime_error(std::string(context) + " contains a disallowed structured-value character");
170 }
171 } else if (kind == StringKind::Url) {
172 validateUrl(value, context);
173 }
174 }
175
176 void validate_file_size(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes) {
177 std::error_code error;
178 const std::uintmax_t size = std::filesystem::file_size(path, error);
179 if (error) {
180 throw std::runtime_error("unable to inspect " + std::string(context));
181 }
182 if (size > maximum_bytes) {
183 throw std::runtime_error(std::string(context) + " exceeds the allowed file size");
184 }
185 }
186
187 void validate_spirv_file(const std::filesystem::path &path, std::string_view context) {
188 constexpr std::uintmax_t MAX_SHADER_BYTES = 64U * 1024U * 1024U;
189 std::error_code error;
190 const std::uintmax_t size = std::filesystem::file_size(path, error);
191 if (error || size < 20U || size > MAX_SHADER_BYTES || size % 4U != 0U) {
192 throw std::runtime_error(std::string(context) + " has an invalid SPIR-V file size");
193 }
194
195 std::ifstream shader(path, std::ios::binary);
196 std::array<unsigned char, 4> magic{};
197 if (!shader.read(reinterpret_cast<char *>(magic.data()), static_cast<std::streamsize>(magic.size()))) {
198 throw std::runtime_error("unable to read " + std::string(context));
199 }
200 const std::uint32_t word = static_cast<std::uint32_t>(magic[0]) | (static_cast<std::uint32_t>(magic[1]) << 8U) | (static_cast<std::uint32_t>(magic[2]) << 16U) | (static_cast<std::uint32_t>(magic[3]) << 24U);
201 if (word != 0x07230203U) {
202 throw std::runtime_error(std::string(context) + " does not contain SPIR-V bytecode");
203 }
204 }
205
206 void validate_text_file(const std::filesystem::path &path, std::string_view context, std::uintmax_t maximum_bytes, std::size_t maximum_line_bytes) {
207 validate_file_size(path, context, maximum_bytes);
208 std::ifstream text_file(path, std::ios::binary);
209 if (!text_file) {
210 throw std::runtime_error("unable to open " + std::string(context));
211 }
212 std::string line;
213 std::size_t line_number = 1;
214 while (read_bounded_line(text_file, line, context, line_number++, maximum_line_bytes)) {
215 }
216 }
217
218 std::string truncate_utf8(std::string_view value, std::size_t maximum_bytes, std::string_view suffix) {
219 if (value.size() <= maximum_bytes) {
220 return std::string(value);
221 }
222 if (suffix.size() >= maximum_bytes) {
223 return std::string(suffix.substr(0, maximum_bytes));
224 }
225
226 std::size_t end = maximum_bytes - suffix.size();
227 while (end > 0 && end < value.size() && (static_cast<unsigned char>(value[end]) & 0xC0U) == 0x80U) {
228 --end;
229 }
230 std::string result(value.substr(0, end));
231 result.append(suffix);
232 return result;
233 }
234
235 bool read_bounded_line(std::istream &input, std::string &line, std::string_view context, std::size_t line_number, std::size_t maximum_bytes) {
236 line.clear();
237 char character = 0;
238 bool read_anything = false;
239 while (input.get(character)) {
240 read_anything = true;
241 if (character == '\n') {
242 break;
243 }
244 if (line.size() >= maximum_bytes) {
245 throw std::runtime_error(std::string(context) + " line " + std::to_string(line_number) + " exceeds the allowed length");
246 }
247 line.push_back(character);
248 }
249 if (!read_anything) {
250 return false;
251 }
252 if (!line.empty() && line.back() == '\r') {
253 line.pop_back();
254 }
255 if (line_number == 1 && line.size() >= 3 && static_cast<unsigned char>(line[0]) == 0xEFU && static_cast<unsigned char>(line[1]) == 0xBBU && static_cast<unsigned char>(line[2]) == 0xBFU) {
256 line.erase(0, 3);
257 }
259 return true;
260 }
261
262} // 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)