unilink  0.4.3
A simple C++ library for unified async communication
input_validator.cc
Go to the documentation of this file.
1 /*
2  * Copyright 2025 Jinwoo Sung
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
18 
19 #include <algorithm>
20 #include <boost/asio/ip/address.hpp>
21 #include <boost/system/error_code.hpp>
22 #include <string_view>
23 
24 namespace unilink {
25 namespace util {
26 
27 void InputValidator::validate_host(const std::string& host) {
28  validate_non_empty_string(host, "host");
30 
31  if (is_valid_host(host)) {
32  return;
33  }
34 
35  throw diagnostics::ValidationException("invalid host format", "host", "valid IPv4, IPv6, or hostname");
36 }
37 
38 void InputValidator::validate_ipv4_address(const std::string& address) {
39  validate_non_empty_string(address, "ipv4_address");
40 
41  if (!is_valid_ipv4(address)) {
42  throw diagnostics::ValidationException("invalid IPv4 address format", "ipv4_address", "valid IPv4 address");
43  }
44 }
45 
46 void InputValidator::validate_ipv6_address(const std::string& address) {
47  validate_non_empty_string(address, "ipv6_address");
48 
49  if (!is_valid_ipv6(address)) {
50  throw diagnostics::ValidationException("invalid IPv6 address format", "ipv6_address", "valid IPv6 address");
51  }
52 }
53 
54 void InputValidator::validate_device_path(const std::string& device) {
55  validate_non_empty_string(device, "device_path");
57 
58  if (!is_valid_device_path(device)) {
59  throw diagnostics::ValidationException("invalid device path format", "device_path", "valid device path");
60  }
61 }
62 
63 void InputValidator::validate_parity(const std::string& parity) {
64  validate_non_empty_string(parity, "parity");
65 
66  // Convert to lowercase for case-insensitive comparison
67  std::string lower_parity = parity;
68  std::transform(lower_parity.begin(), lower_parity.end(), lower_parity.begin(),
69  [](unsigned char c) { return std::tolower(c); });
70 
71  if (lower_parity != "none" && lower_parity != "odd" && lower_parity != "even") {
72  throw diagnostics::ValidationException("invalid parity value", "parity", "none, odd, or even");
73  }
74 }
75 
76 bool InputValidator::is_valid_host(const std::string& host) {
77  // Check if it's an IPv4 address
78  if (is_valid_ipv4(host)) {
79  return true;
80  }
81 
82  // Check if it's an IPv6 address
83  if (is_valid_ipv6(host)) {
84  return true;
85  }
86 
87  // Check if it's a valid hostname
88  if (is_valid_hostname(host)) {
89  return true;
90  }
91 
92  return false;
93 }
94 
95 bool InputValidator::is_valid_ipv4(std::string_view address) {
96  if (address.empty()) return false;
97 
98  // Use Boost.Asio for parsing to ensure standard compliance
99  // and robust validation, but enforce strict canonical form
100  // to reject ambiguous formats (e.g., octal, hex, whitespace).
101  boost::system::error_code ec;
102 
103  // Create string copy for compatibility with older Boost versions
104  std::string addr_str(address);
105  auto ip = boost::asio::ip::make_address_v4(addr_str, ec);
106 
107  if (ec) {
108  return false;
109  }
110 
111  // Canonicalization check: The string representation of the parsed IP
112  // must match the input exactly. This rejects:
113  // - Octal (0127.0.0.1 -> 87.0.0.1 != 0127.0.0.1)
114  // - Hex (0x7F000001 -> 127.0.0.1 != 0x7F000001)
115  // - Leading/trailing whitespace
116  // - Leading zeros in octets (01.1.1.1 -> 1.1.1.1 != 01.1.1.1)
117  if (ip.to_string() != addr_str) {
118  return false;
119  }
120 
121  return true;
122 }
123 
124 bool InputValidator::is_valid_ipv6(const std::string& address) {
125  // Reject addresses containing brackets (e.g. [::1]:80) or port numbers
126  // boost::asio::ip::make_address_v6 behavior on Windows regarding this might be permissive
127  // or platform-dependent, so we explicitly reject them for consistency.
128  if (address.find('[') != std::string::npos || address.find(']') != std::string::npos) {
129  return false;
130  }
131 
132  boost::system::error_code ec;
133  boost::asio::ip::make_address_v6(address, ec);
134  return !ec;
135 }
136 
137 bool InputValidator::is_valid_hostname(std::string_view hostname) {
138  // Hostname validation according to RFC 1123
139  // - Must not be empty
140  // - Must not start or end with hyphen
141  // - Must contain only alphanumeric characters and hyphens
142  // - Each label must be 1-63 characters
143  // - Total length must not exceed 253 characters
144 
145  if (hostname.empty() || hostname.length() > base::constants::MAX_HOSTNAME_LENGTH) {
146  return false;
147  }
148 
149  if (hostname.front() == '-' || hostname.back() == '-') {
150  return false;
151  }
152 
153  // Check each label (separated by dots)
154  size_t start = 0;
155  size_t end = 0;
156 
157  while ((end = hostname.find('.', start)) != std::string_view::npos) {
158  std::string_view label = hostname.substr(start, end - start);
159 
160  if (label.empty() || label.length() > 63) {
161  return false;
162  }
163 
164  if (label.front() == '-' || label.back() == '-') {
165  return false;
166  }
167 
168  // Check if label contains only valid characters
169  for (char c : label) {
170  if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-') {
171  return false;
172  }
173  }
174  start = end + 1;
175  }
176 
177  // Check last label
178  std::string_view label = hostname.substr(start);
179  if (label.empty() || label.length() > 63) {
180  return false;
181  }
182 
183  if (label.front() == '-' || label.back() == '-') {
184  return false;
185  }
186 
187  for (char c : label) {
188  if (!std::isalnum(static_cast<unsigned char>(c)) && c != '-') {
189  return false;
190  }
191  }
192 
193  return true;
194 }
195 
196 bool InputValidator::is_valid_device_path(const std::string& device) {
197  // Basic device path validation
198  // - Must not be empty
199  // - Must start with '/' (Unix-style) or be a COM port (Windows-style)
200  // - Must not contain invalid characters
201 
202  if (device.empty()) {
203  return false;
204  }
205 
206  // Unix-style device path (e.g., /dev/ttyUSB0, /dev/ttyACM0)
207  // Must start with "/dev/" for security
208  if (device.length() >= 5 && device.substr(0, 5) == "/dev/") {
209  // Check for valid Unix device path characters
210  for (char c : device) {
211  if (!std::isalnum(static_cast<unsigned char>(c)) && c != '/' && c != '_' && c != '-') {
212  return false;
213  }
214  }
215  return true;
216  }
217 
218  // Windows-style COM port (e.g., COM1, COM2, etc.)
219  if (device.length() >= 4 && device.substr(0, 3) == "COM") {
220  std::string port_num = device.substr(3);
221 
222  // Check if port_num contains only digits
223  if (port_num.empty() ||
224  !std::all_of(port_num.begin(), port_num.end(), [](unsigned char c) { return std::isdigit(c); })) {
225  return false;
226  }
227 
228  try {
229  int port = std::stoi(port_num);
230  return port >= 1 && port <= 255;
231  } catch (const std::exception&) {
232  return false;
233  }
234  }
235 
236  // Windows special device names
237  if (device == "NUL" || device == "CON" || device == "PRN" || device == "AUX" || device == "LPT1" ||
238  device == "LPT2" || device == "LPT3") {
239  return true;
240  }
241 
242  return false;
243 }
244 
245 } // namespace util
246 } // namespace unilink