Skip to content

Commit 868c85c

Browse files
authored
dns: Add is_hostname() for RFC 1123 §2.1 Internet hostname validation (#2346)
Signed-off-by: Tushar Verma <tusharmyself06@gmail.com>
1 parent 29240a1 commit 868c85c

File tree

9 files changed

+397
-0
lines changed

9 files changed

+397
-0
lines changed

.github/workflows/website-build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ jobs:
2929
-DSOURCEMETA_CORE_CRYPTO:BOOL=OFF
3030
-DSOURCEMETA_CORE_REGEX:BOOL=OFF
3131
-DSOURCEMETA_CORE_IP:BOOL=OFF
32+
-DSOURCEMETA_CORE_DNS:BOOL=OFF
3233
-DSOURCEMETA_CORE_URI:BOOL=OFF
3334
-DSOURCEMETA_CORE_URITEMPLATE:BOOL=OFF
3435
-DSOURCEMETA_CORE_JSON:BOOL=OFF

.github/workflows/website-deploy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ jobs:
3939
-DSOURCEMETA_CORE_CRYPTO:BOOL=OFF
4040
-DSOURCEMETA_CORE_REGEX:BOOL=OFF
4141
-DSOURCEMETA_CORE_IP:BOOL=OFF
42+
-DSOURCEMETA_CORE_DNS:BOOL=OFF
4243
-DSOURCEMETA_CORE_URI:BOOL=OFF
4344
-DSOURCEMETA_CORE_URITEMPLATE:BOOL=OFF
4445
-DSOURCEMETA_CORE_JSON:BOOL=OFF

CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ option(SOURCEMETA_CORE_CRYPTO "Build the Sourcemeta Core Crypto library" ON)
1717
option(SOURCEMETA_CORE_CRYPTO_USE_SYSTEM_OPENSSL "Use system OpenSSL for the Sourcemeta Core Crypto library" OFF)
1818
option(SOURCEMETA_CORE_REGEX "Build the Sourcemeta Core Regex library" ON)
1919
option(SOURCEMETA_CORE_IP "Build the Sourcemeta Core IP library" ON)
20+
option(SOURCEMETA_CORE_DNS "Build the Sourcemeta Core DNS library" ON)
2021
option(SOURCEMETA_CORE_URI "Build the Sourcemeta Core URI library" ON)
2122
option(SOURCEMETA_CORE_URITEMPLATE "Build the Sourcemeta Core URI Template library" ON)
2223
option(SOURCEMETA_CORE_JSON "Build the Sourcemeta Core JSON library" ON)
@@ -118,6 +119,10 @@ if(SOURCEMETA_CORE_IP)
118119
add_subdirectory(src/core/ip)
119120
endif()
120121

122+
if(SOURCEMETA_CORE_DNS)
123+
add_subdirectory(src/core/dns)
124+
endif()
125+
121126
if(SOURCEMETA_CORE_URI)
122127
add_subdirectory(src/core/uri)
123128
endif()
@@ -237,6 +242,10 @@ if(SOURCEMETA_CORE_TESTS)
237242
add_subdirectory(test/ip)
238243
endif()
239244

245+
if(SOURCEMETA_CORE_DNS)
246+
add_subdirectory(test/dns)
247+
endif()
248+
240249
if(SOURCEMETA_CORE_URI)
241250
add_subdirectory(test/uri)
242251
endif()

config.cmake.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS)
1515
list(APPEND SOURCEMETA_CORE_COMPONENTS crypto)
1616
list(APPEND SOURCEMETA_CORE_COMPONENTS regex)
1717
list(APPEND SOURCEMETA_CORE_COMPONENTS ip)
18+
list(APPEND SOURCEMETA_CORE_COMPONENTS dns)
1819
list(APPEND SOURCEMETA_CORE_COMPONENTS uri)
1920
list(APPEND SOURCEMETA_CORE_COMPONENTS uritemplate)
2021
list(APPEND SOURCEMETA_CORE_COMPONENTS json)
@@ -61,6 +62,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS})
6162
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_regex.cmake")
6263
elseif(component STREQUAL "ip")
6364
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_ip.cmake")
65+
elseif(component STREQUAL "dns")
66+
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_dns.cmake")
6467
elseif(component STREQUAL "uri")
6568
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_ip.cmake")
6669
include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake")

src/core/dns/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME dns
2+
SOURCES hostname.cc)
3+
4+
if(SOURCEMETA_CORE_INSTALL)
5+
sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME dns)
6+
endif()

src/core/dns/hostname.cc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include <sourcemeta/core/dns.h>
2+
3+
namespace sourcemeta::core {
4+
5+
// RFC 952 §B: let-dig = ALPHA / DIGIT
6+
// RFC 1123 §2.1: first character of a label is letter or digit
7+
static constexpr auto is_let_dig(const char character) -> bool {
8+
return (character >= 'A' && character <= 'Z') ||
9+
(character >= 'a' && character <= 'z') ||
10+
(character >= '0' && character <= '9');
11+
}
12+
13+
// RFC 952 §B: let-dig-hyp = ALPHA / DIGIT / "-"
14+
static constexpr auto is_let_dig_hyp(const char character) -> bool {
15+
return is_let_dig(character) || character == '-';
16+
}
17+
18+
auto is_hostname(const std::string_view value) -> bool {
19+
// RFC 952 §B: <hname> requires at least one <name>
20+
if (value.empty()) {
21+
return false;
22+
}
23+
24+
// RFC 1123 §2.1: SHOULD handle host names of up to 255 characters
25+
if (value.size() > 255) {
26+
return false;
27+
}
28+
29+
std::string_view::size_type position{0};
30+
31+
while (position < value.size()) {
32+
const auto label_start{position};
33+
34+
// RFC 1123 §2.1: first character is letter or digit
35+
if (!is_let_dig(value[position])) {
36+
return false;
37+
}
38+
position += 1;
39+
40+
while (position < value.size() && value[position] != '.') {
41+
// RFC 952 §B: interior characters are let-dig-hyp
42+
if (!is_let_dig_hyp(value[position])) {
43+
return false;
44+
}
45+
position += 1;
46+
}
47+
48+
const auto label_length{position - label_start};
49+
50+
// RFC 1123 §2.1: MUST handle host names of up to 63 characters (per label)
51+
if (label_length > 63) {
52+
return false;
53+
}
54+
55+
// RFC 952 §B + ASSUMPTIONS: last character must not be a minus sign
56+
if (value[position - 1] == '-') {
57+
return false;
58+
}
59+
60+
// If we stopped on a dot, there must be another label following it
61+
if (position < value.size()) {
62+
// value[position] == '.'
63+
position += 1;
64+
// Trailing dot: JSON Schema test suite requires rejection (TS d7+ #15)
65+
if (position >= value.size()) {
66+
return false;
67+
}
68+
}
69+
}
70+
71+
return true;
72+
}
73+
74+
} // namespace sourcemeta::core
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#ifndef SOURCEMETA_CORE_DNS_H_
2+
#define SOURCEMETA_CORE_DNS_H_
3+
4+
#ifndef SOURCEMETA_CORE_DNS_EXPORT
5+
#include <sourcemeta/core/dns_export.h>
6+
#endif
7+
8+
#include <string_view> // std::string_view
9+
10+
/// @defgroup dns DNS
11+
/// @brief DNS and hostname validation utilities.
12+
///
13+
/// This functionality is included as follows:
14+
///
15+
/// ```cpp
16+
/// #include <sourcemeta/core/dns.h>
17+
/// ```
18+
19+
namespace sourcemeta::core {
20+
21+
/// @ingroup dns
22+
/// Check whether the given string is a valid Internet host name per
23+
/// RFC 1123 Section 2.1, which relaxes the first-character rule of
24+
/// RFC 952 to allow either a letter or a digit. This matches the
25+
/// definition used by the JSON Schema `hostname` format. For example:
26+
///
27+
/// ```cpp
28+
/// #include <sourcemeta/core/dns.h>
29+
///
30+
/// #include <cassert>
31+
///
32+
/// assert(sourcemeta::core::is_hostname("www.example.com"));
33+
/// assert(sourcemeta::core::is_hostname("1host"));
34+
/// assert(!sourcemeta::core::is_hostname("-bad"));
35+
/// assert(!sourcemeta::core::is_hostname("example."));
36+
/// ```
37+
///
38+
/// This function implements RFC 1123 §2.1 (ASCII only). It does not
39+
/// perform A-label or Punycode decoding. Those belong to the separate
40+
/// `idn-hostname` format.
41+
SOURCEMETA_CORE_DNS_EXPORT
42+
auto is_hostname(const std::string_view value) -> bool;
43+
44+
} // namespace sourcemeta::core
45+
46+
#endif

test/dns/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
sourcemeta_googletest(NAMESPACE sourcemeta PROJECT core NAME dns
2+
SOURCES hostname_test.cc)
3+
4+
target_link_libraries(sourcemeta_core_dns_unit
5+
PRIVATE sourcemeta::core::dns)

0 commit comments

Comments
 (0)