1 /* $NetBSD: url_test.c,v 1.1.1.1 2026/08/29 14:32:15 christos Exp $ */ 2 3 /* 4 * Copyright (C) Internet Systems Consortium, Inc. ("ISC") 5 * 6 * SPDX-License-Identifier: MPL-2.0 7 * 8 * This Source Code Form is subject to the terms of the Mozilla Public 9 * License, v. 2.0. If a copy of the MPL was not distributed with this 10 * file, you can obtain one at https://mozilla.org/MPL/2.0/. 11 * 12 * See the COPYRIGHT file distributed with this work for additional 13 * information regarding copyright ownership. 14 */ 15 16 /*! \file */ 17 18 #include <inttypes.h> 19 #include <sched.h> /* IWYU pragma: keep */ 20 #include <setjmp.h> 21 #include <stdarg.h> 22 #include <stddef.h> 23 #include <stdlib.h> 24 #include <string.h> 25 #include <unistd.h> 26 27 #define UNIT_TESTING 28 #include <cmocka.h> 29 30 #include <isc/mem.h> 31 #include <isc/url.h> 32 #include <isc/util.h> 33 34 #include <tests/isc.h> 35 36 typedef struct { 37 const char *uri; 38 isc_result_t result; 39 /* Expected components (checked only when result is ISC_R_SUCCESS). */ 40 const char *scheme; 41 const char *userinfo; 42 const char *host; 43 const char *port; 44 const char *path; 45 const char *query; 46 const char *fragment; 47 } url_testcase_t; 48 49 /* 50 * Extract the substring of component 'uf' from 'buf' and compare it with 51 * 'expected'. A field that is not set is treated as the empty string. 52 */ 53 static void 54 check_field(const isc_url_parser_t *up, const char *buf, isc_url_field_t uf, 55 const char *expected) { 56 char got[256] = { 0 }; 57 58 if ((up->field_set & (1 << uf)) != 0) { 59 uint16_t off = up->field_data[uf].off; 60 uint16_t len = up->field_data[uf].len; 61 62 INSIST(len < sizeof(got)); 63 memmove(got, buf + off, len); 64 } 65 66 assert_string_equal(got, expected); 67 } 68 69 static void 70 run_url_testcases(const url_testcase_t *cases, size_t ncases) { 71 for (size_t i = 0; i < ncases; i++) { 72 const url_testcase_t *tc = &cases[i]; 73 isc_url_parser_t up = { 0 }; 74 isc_result_t result = isc_url_parse(tc->uri, strlen(tc->uri), 75 false, &up); 76 77 assert_int_equal(result, tc->result); 78 if (result != ISC_R_SUCCESS) { 79 continue; 80 } 81 82 check_field(&up, tc->uri, ISC_UF_SCHEMA, tc->scheme); 83 check_field(&up, tc->uri, ISC_UF_USERINFO, tc->userinfo); 84 check_field(&up, tc->uri, ISC_UF_HOST, tc->host); 85 check_field(&up, tc->uri, ISC_UF_PORT, tc->port); 86 check_field(&up, tc->uri, ISC_UF_PATH, tc->path); 87 check_field(&up, tc->uri, ISC_UF_QUERY, tc->query); 88 check_field(&up, tc->uri, ISC_UF_FRAGMENT, tc->fragment); 89 } 90 } 91 92 ISC_RUN_TEST_IMPL(parse) { 93 isc_result_t result; 94 isc_url_parser_t up = { 0 }; 95 const char *url = NULL; 96 97 /* Test an empty buffer. */ 98 url = ""; 99 result = isc_url_parse(url, strlen(url), false, &up); 100 assert_int_equal(ISC_R_RANGE, result); 101 102 /* Test a buffer with a valid URI. */ 103 url = "http://user:pass@example.com:8080/test?a=b&c=d"; 104 result = isc_url_parse(url, strlen(url), false, &up); 105 assert_int_equal(ISC_R_SUCCESS, result); 106 assert_int_equal(0, up.field_data[ISC_UF_SCHEMA].off); 107 assert_int_equal(4, up.field_data[ISC_UF_SCHEMA].len); 108 assert_int_equal(17, up.field_data[ISC_UF_HOST].off); 109 assert_int_equal(11, up.field_data[ISC_UF_HOST].len); 110 assert_int_equal(29, up.field_data[ISC_UF_PORT].off); 111 assert_int_equal(4, up.field_data[ISC_UF_PORT].len); 112 assert_int_equal(33, up.field_data[ISC_UF_PATH].off); 113 assert_int_equal(5, up.field_data[ISC_UF_PATH].len); 114 assert_int_equal(39, up.field_data[ISC_UF_QUERY].off); 115 assert_int_equal(7, up.field_data[ISC_UF_QUERY].len); 116 assert_int_equal(0, up.field_data[ISC_UF_FRAGMENT].off); 117 assert_int_equal(0, up.field_data[ISC_UF_FRAGMENT].len); 118 assert_int_equal(7, up.field_data[ISC_UF_USERINFO].off); 119 assert_int_equal(9, up.field_data[ISC_UF_USERINFO].len); 120 121 /* 122 * Test the maximum accepted buffer length (URL_MAX_LENGTH). A path of 123 * exactly URL_MAX_LENGTH bytes also drives ISC_UF_PATH.len to its 124 * maximum without overflowing. 125 */ 126 size_t max_len = URL_MAX_LENGTH; 127 char *max_buf = isc_mem_get(mctx, max_len); 128 memset(max_buf, 'a', max_len); 129 max_buf[0] = '/'; 130 result = isc_url_parse(max_buf, max_len, false, &up); 131 isc_mem_put(mctx, max_buf, max_len); 132 assert_int_equal(ISC_R_SUCCESS, result); 133 assert_int_equal(0, up.field_data[ISC_UF_PATH].off); 134 assert_int_equal(URL_MAX_LENGTH, up.field_data[ISC_UF_PATH].len); 135 136 /* Test a too big buffer (URL_MAX_LENGTH + 1). */ 137 size_t buf_len = URL_MAX_LENGTH + 2; 138 char *buf = isc_mem_get(mctx, buf_len); 139 memset(buf, 'a', buf_len); 140 buf[0] = '/'; 141 result = isc_url_parse(buf, URL_MAX_LENGTH + 1, false, &up); 142 isc_mem_put(mctx, buf, buf_len); 143 assert_int_equal(ISC_R_RANGE, result); 144 } 145 146 /* 147 * isc_url_parse() splits an absolute URI / request target into components; 148 * it does NOT resolve relative references or remove dot-segments 149 * (RFC 3986 section 5). The inputs below are the base URI and a 150 * representative subset of the resolved targets from RFC 3986 section 5.4, 151 * parsed as standalone absolute URIs. Note in particular that '.'/'..' and 152 * embedded slashes inside the query and fragment are preserved verbatim 153 * rather than normalized. 154 */ 155 ISC_RUN_TEST_IMPL(parse_rfc3986) { 156 static const url_testcase_t testcases[] = { 157 /* The base URI used throughout RFC 3986 section 5.4. */ 158 { "http://a/b/c/d;p?q", ISC_R_SUCCESS, "http", "", "a", "", 159 "/b/c/d;p", "q", "" }, 160 161 /* Normal examples (section 5.4.1). */ 162 { "http://a/b/c/g", ISC_R_SUCCESS, "http", "", "a", "", 163 "/b/c/g", "", "" }, 164 { "http://a/g", ISC_R_SUCCESS, "http", "", "a", "", "/g", "", 165 "" }, 166 { "http://g", ISC_R_SUCCESS, "http", "", "g", "", "", "", "" }, 167 { "http://a/", ISC_R_SUCCESS, "http", "", "a", "", "/", "", 168 "" }, 169 { "http://a/b/c/;x", ISC_R_SUCCESS, "http", "", "a", "", 170 "/b/c/;x", "", "" }, 171 { "http://a/b/c/g;x?y#s", ISC_R_SUCCESS, "http", "", "a", "", 172 "/b/c/g;x", "y", "s" }, 173 { "http://a/b/c/d;p?q#s", ISC_R_SUCCESS, "http", "", "a", "", 174 "/b/c/d;p", "q", "s" }, 175 176 /* 177 * Abnormal examples (section 5.4.2): dot-segments and slashes 178 * embedded in the query and fragment are not normalized. 179 */ 180 { "http://a/b/c/..g", ISC_R_SUCCESS, "http", "", "a", "", 181 "/b/c/..g", "", "" }, 182 { "http://a/b/c/g..", ISC_R_SUCCESS, "http", "", "a", "", 183 "/b/c/g..", "", "" }, 184 { "http://a/b/c/g?y/./x", ISC_R_SUCCESS, "http", "", "a", "", 185 "/b/c/g", "y/./x", "" }, 186 { "http://a/b/c/g#s/../x", ISC_R_SUCCESS, "http", "", "a", "", 187 "/b/c/g", "", "s/../x" }, 188 189 /* The full generic-syntax example from RFC 3986 section 3. */ 190 { "foo://example.com:8042/over/there?name=ferret#nose", 191 ISC_R_SUCCESS, "foo", "", "example.com", "8042", 192 "/over/there", "name=ferret", "nose" }, 193 194 /* 195 * Relative references cannot be parsed as request targets: 196 * a scheme requires an authority, and a bare reference has no 197 * host. 198 */ 199 { "g:h", ISC_R_FAILURE, "", "", "", "", "", "", "" }, 200 { "g", ISC_R_FAILURE, "", "", "", "", "", "", "" }, 201 { "http:g", ISC_R_FAILURE, "", "", "", "", "", "", "" }, 202 }; 203 204 run_url_testcases(testcases, ARRAY_SIZE(testcases)); 205 } 206 207 /* 208 * Authority and IPv6 edge cases, drawn from the Addressable URI test suite. 209 * isc_url_parse() is stricter than a generic RFC 3986 parser: it requires a 210 * scheme followed by "://" and an authority, the host of an IPv6 literal 211 * excludes the surrounding brackets, and it neither percent-decodes nor 212 * case-normalizes any component. 213 */ 214 ISC_RUN_TEST_IMPL(parse_addressable) { 215 static const url_testcase_t testcases[] = { 216 /* IPv6 literal hosts: the brackets are stripped from the host. 217 */ 218 { "http://[::1]/", ISC_R_SUCCESS, "http", "", "[::1]", "", "/", 219 "", "" }, 220 { "http://[fe80::200:f8ff:fe21:67cf]/", ISC_R_SUCCESS, "http", 221 "", "[fe80::200:f8ff:fe21:67cf]", "", "/", "", "" }, 222 { "http://[2001:db8::7]:8080/path", ISC_R_SUCCESS, "http", "", 223 "[2001:db8::7]", "8080", "/path", "", "" }, 224 { "ldap://[2001:db8::7]/c=GB?objectClass?one", ISC_R_SUCCESS, 225 "ldap", "", "[2001:db8::7]", "", "/c=GB", "objectClass?one", 226 "" }, 227 /* An IPv6 zone identifier is kept verbatim (not decoded). */ 228 { "http://[fe80::1%25en0]/", ISC_R_SUCCESS, "http", "", 229 "[fe80::1%25en0]", "", "/", "", "" }, 230 231 /* IPv4 host with an explicit port. */ 232 { "telnet://192.0.2.16:80/", ISC_R_SUCCESS, "telnet", "", 233 "192.0.2.16", "80", "/", "", "" }, 234 235 /* Userinfo, with and without a password; case is preserved. */ 236 { "http://user@example.com/", ISC_R_SUCCESS, "http", "user", 237 "example.com", "", "/", "", "" }, 238 { "http://:@example.com/", ISC_R_SUCCESS, "http", ":", 239 "example.com", "", "/", "", "" }, 240 { "HTTP://EXAMPLE.COM/", ISC_R_SUCCESS, "HTTP", "", 241 "EXAMPLE.COM", "", "/", "", "" }, 242 243 /* A trailing dot in the host is part of the host. */ 244 { "http://example.com./", ISC_R_SUCCESS, "http", "", 245 "example.com.", "", "/", "", "" }, 246 247 /* No path component at all. */ 248 { "http://example.com", ISC_R_SUCCESS, "http", "", 249 "example.com", "", "", "", "" }, 250 251 /* Path parameters stay in the path. */ 252 { "http://example.com/file.txt;x=y", ISC_R_SUCCESS, "http", "", 253 "example.com", "", "/file.txt;x=y", "", "" }, 254 255 /* 256 * Rejected where a generic RFC 3986 parser would succeed: a '+' 257 * in the scheme, an IPvFuture literal, and a percent-encoded 258 * port. 259 */ 260 { "svn+ssh://developername (at) rubyforge.org/var/svn/project", 261 ISC_R_FAILURE, "", "", "", "", "", "", "" }, 262 { "http://[v9.3ffe:1900:4545:3:200:f8ff:fe21:67cf]/", 263 ISC_R_FAILURE, "", "", "", "", "", "", "" }, 264 { "http://example.com:%38%30/", ISC_R_FAILURE, "", "", "", "", 265 "", "", "" }, 266 }; 267 268 run_url_testcases(testcases, ARRAY_SIZE(testcases)); 269 } 270 271 ISC_TEST_LIST_START 272 273 ISC_TEST_ENTRY(parse) 274 ISC_TEST_ENTRY(parse_rfc3986) 275 ISC_TEST_ENTRY(parse_addressable) 276 277 ISC_TEST_LIST_END 278 279 ISC_TEST_MAIN 280