Home | History | Annotate | Line # | Download | only in isc
      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