Ориентация главы

xurl — URL-слой xhttp, но он не только про HTTP URL: xrtUrlParse разбирает полный URI-reference RFC 3986 — mailto:user@example.com, urn:isbn:..., сетевой путь //host/path, относительную ссылку ../items?page=2 поддерживаются целиком; URL не сводится к scheme://host/path. Дизайн в один дух со всей библиотекой: ноль копирования (все представления заимствуют ввод), биты присутствия (Path присутствует всегда; есть ли Query/Fragment/UserInfo/порт — смотреть Flags, не на длину представления), лексическая сохранность (PortText хранит :00080 как есть, port — десятичный текст произвольной длины: :65536 не отвергается разбором за превышение uint16), выписывание без выделения (семейство Write отвергает перекрытие, короткий буфер — ноль записанного). Плюс разрешение ссылок RFC 3986 (xrtUrlResolve — разворот относительных адресов) и выписывание target (форма строки запроса HTTP) — вот полный словарь URL.

102.1引入

«Разобрать URL» звучит как нарезка строки; где же профессиональные ловушки?Первая: присутствие против пустого значения — у https://host нет query, у https://host? есть пустой query: длины представлений одинаково нулевые, семантика различна (при выписывании у последнего вопросик сохраняется). xurl решает битами присутствия в Flags: XURL_HAS_QUERY различает «нет» и «пусто».Вторая: лексика и значение порта — порт в RFC есть десятичный текст произвольной длины: :65536 — легальный URL (разбор не вправе отвергнуть), просто без значения uint16; бит XURL_PORT_VALUE означает «член Port содержит выразимое значение» — :00080 сохраняется лексически, значение 80.Третья: относительные ссылки — Location: ../health?full=1 HTTP-перенаправления разворачивается against текущего URL (глава 100); разрешение ссылок — деталь протокольной корректности.

Все три вопроса указывают на одно: «URL — структурированный текст, а не строка» — парсер доделывает синтаксическое суждение, вызывающий получает однозначную структуру.

102.2概念

Структура и биты присутствия

Поля xurl — все представления: Scheme/Authority/UserInfo/Host/PortText/Path/Query/Fragment + Flags (биты присутствия) + Port (значение uint16). Ключевая дисциплина: Path присутствует всегда, но может быть пуст; наличие Query/Fragment/UserInfo/порта смотрится по битам вроде XURL_HAS_QUERY, не по длине представления или значению — явный пустой query (?) и отсутствие query суть две структуры. Симметричное правило ручного построения: численный порт — установить XURL_HAS_PORT | XURL_PORT_VALUE, заполнить Port, оставить пустым PortText; лексический порт — заполнить PortText, не ставя PORT_VALUE.

Границы разбора и нелегальный ввод

  • ASCII URI: не-ASCII текст сперва проходит слой IRI/percent-кодирования — незакодированные байты UTF-8 не легальный URI (китайский домен сперва punycode, пробел сперва %20).
  • IPv6 ZoneID: RFC 9844 отменил расширения вида [fe80::1%25eth0] — парсер отвергает (локальное выражение scope — слой сетевых адресов главы 71, две вещи раздельно).
  • Представления заимствуют ввод (по недействительному вводу больше не читать); вывод разбора не перекрывает ввод; заворот конца записываемого диапазона адресов отвергается как ошибка параметра.

Семейство Write: без выделения, атомарно, с отказом от перекрытия

xrtUrlWrite/UrlAuthorityWrite/UrlHostWrite/UrlTargetWrite: структура → текст, без \0, NULL/0-ёмкость запрашивает точную длину, короткий буфер — ноль записанного, перекрытие вывода с объектом/представлением/длиной ввода отвергается всегда. Target выписывает origin-form для строки запроса HTTP (/api/items?page=2) — прямой транзит для построения запроса. PathNormalize поддерживает на месте и ограниченное перекрытие; Resolve отвергает вывод, перекрывающий Base/Reference.Семейство Build (UrlResolveBuild и другие) возвращает строку с нулевым завершителем, освобождаемую xrtFree, — эта единственная плата за выделение берётся, только когда нужна «независимая строка».

Разрешение ссылок: разворот относительных адресов

xrtUrlResolve(Base, Reference, 输出) (вывод) разворачивает относительную ссылку по RFC 3986 §5: ../health?full=1 против https://example.test/api/items?page=2 даёт https://example.test/api/health?full=1 — путь сворачивается сегмент за сегментом (.. поднимается), query перекрывает, без scheme наследуются scheme/authority базы. Семантика fragment — у вызывающего (правила наследования/блокирования HTTP-перенаправлений — глава 100; там протокольная политика, здесь только синтаксический разворот). Запрос длины и прямое выписывание без выделения; отказ не меняет вывод.

Разделение труда: глава 89 Host, глава 103 query

Кому нужно представление Host из xurl? Разбору поля Host на HTTP-стороне (главы 89, xhttpauthority) — authority-форма заголовка Host:; host в URL — компонент URI; они разделяют синтаксис «скобочный IPv6/порт», но входы разные. Обход ключ-значение query-строки (xrtQueryNext) действует на представлении Url.Query — разворачивается в главе 102.Чего слой не делает: кодирование/декодирование (слой percent), punycode (слой charset) — xurl ведает только структурой.

Место xurl в слоях

1xurl (эта глава)структурный разбор/выписывание/разворот ссылок RFC 3986 - ноль копирования, биты присутствия, лексическая сохранность
2Два слоя query (глава 103)обход без выделения + владеющий контейнер (домен &)
3HTTP-специализация (главы 89/92)authority заголовка Host, точка с запятой-параметры значения поля (домен ;)
4Кодеки (глава 28)percent/punycode - слой символов над xurl

102.3示例

Первая полная программа: разбор, выписывание target и разрешение ссылок

Программа ниже из examples/url/main.c — три шага через ядро грани:

extlibs/xhttp/examples/url/main.c源码 ↗
#include <stdio.h>

#include <xhttp.h>



/* 展示零拷贝解析、HTTP target 写出和 RFC 3986 引用解析。 */
int main(void)
{
	xurl Url;
	char Target[128];
	str sResolved;
	size_t iSize;

	if ( !xrtUrlParse(
		XRT_STR_LITERAL("https://example.test/api/items?page=2#result"),
		&Url
	) || !xrtUrlTargetWrite(
		&Url, Target, sizeof(Target), &iSize
	) ) {
		return 1;
	}
	printf("host: %.*s\ntarget: %.*s\n",
		(int)Url.Host.Size, Url.Host.Data,
		(int)iSize, Target);

	sResolved = xrtUrlResolveBuild(
		&Url, XRT_STR_LITERAL("../health?full=1"), &iSize
	);
	if ( sResolved == NULL ) {
		return 1;
	}
	printf("resolved: %s\n", sResolved);
	xrtFree(sResolved);
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h impl.c extlibs/xhttp/examples/url/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapihost: example.testtarget: /api/items?page=2resolved: https://example.test/api/health?full=1

Что только что произошло. (1) UrlParse одним разом разбирает полный URL — представление Host (example.test) и Query (page=2) целиком заимствуют ввод, ноль выделений. (2) UrlTargetWrite выписывает target в origin-form: /api/items?page=2 — прямо годится для строки запроса HTTP (fragment #result не входит в target — это локальная семантика клиента). (3) UrlResolveBuild разворачивает относительную ссылку ../health?full=1 против текущего URL: путь api/items поднялся .. к api/, прицеплен health, query сменён на full=1 — одно выделение даёт только итоговую строку (без промежуточных объектов слияния). Эти три строки и есть полный фундамент обработки Location перенаправлений (глава 100).

Вторая полная программа: валидация параметров URI-Reference

Вторая программа из examples/url_param/main.c — валидация ссылки без временных строк:

extlibs/xhttp/examples/url_param/main.c源码 ↗
#include <stdio.h>
#include <string.h>

#include <xhttp.h>



/* 验证 HTTP 参数解码后的 URI-reference,不创建临时字符串。 */
int main(void)
{
	xhttpparam Param;

	memset(&Param, 0, sizeof(Param));
	Param.Value = XRT_STR_LITERAL("../items?page=2");
	Param.Flags = XHTTP_PARAM_HAS_VALUE | XHTTP_PARAM_QUOTED;
	printf("valid: %s\n", xrtUrlParamValid(&Param) ? "yes" : "no");
	return 0;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXHTTP_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xhttp.h impl.c extlibs/xhttp/examples/url_param/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapivalid: yes

Что только что произошло. (1) Ввод — URI-reference в форме значения HTTP-параметра (../items?page=2 внутри quoted-string — частый носитель заголовка Link); xrtUrlParamValid валидирует легальность прямо на представлении после декодирования параметра — без копирования и временных строк. (2) Такие входы «валидация без материализации» — частый путь middleware/прокси: URL в заголовке каждого запроса нужно проверить, выделение было бы расходом. (3) Сопутствующая инспекция — examples/url/query* (глава 103) и examples/http/origin/forward/link (вычисление origin, обработка Forwarded, разбор заголовка Link — HTTP-специализация той же синтаксической семьи).

102.4契约

  • Диапазон разбора: полный URI-reference RFC 3986 (не только http-форма); ввод ASCII; ZoneID IPv6 отвергается (RFC 9844).
  • Биты присутствия: Path присутствует всегда, может быть пуст; наличие Query/Fragment/UserInfo/Port — по битам вроде XURL_HAS_QUERY; явное пустое и отсутствующее — разные структуры.
  • Семантика порта: PortText сохраняет лексику (:00080 как есть); XURL_PORT_VALUE означает наличие значения uint16 в Port; без порта/ноль/пусто/вне диапазона — все могут дать Port==0 — судите вместе с битами присутствия; два симметричных способа ручного заполнения.
  • Семейство Write: без \0; пустая ёмкость запрашивает длину; короткий буфер — ноль записанного; перекрытие с вводом отвергается; Target даёт origin-form (без fragment).
  • PathNormalize/Resolve: без выделения; отказ не меняет вывод; контракт перекрытия (на месте/ограниченно/отказ перекрытия Base).
  • Семейство Build: освобождение xrtFree; ResolveBuild выделяет только итог.
  • Заимствование представлений: действительны, пока жив ввод; вывод не перекрывает объект ввода.
  • Безопасность невыравненности: структуры и вывод size_t допускают невыравненное хранение; заворот записываемого диапазона отвергается.
  • Границы разделения: percent-кодеки, punycode, разбор заголовка Host, ключ-значение query — отдельные слои; xurl ведает только структурой.

Типичные ошибки

Ловушка 1: судить о присутствии компонента по длине представления:Ловушка 1: судить о присутствии компонента по длине представления

Симптом: пустой query у https://host? принят за «нет query» — при перезаписи URL вопросик потерялся; или наоборот: отсутствие fragment выписано как #.

Причина: пустое и отсутствующее различает Flags; по длине представления они неотличимы. Это ключ к верности URL-цикла (parse→изменить→write).

错误写法
if ( Url.Query.Size != 0 ) {   /* пустой query (с ?) ложно принят за отсутствие */
	write_url_with_query(...);
}
正确写法
if ( (Url.Flags & XURL_HAS_QUERY) != 0 ) {
	/* query есть (может быть пустым) - при выписывании вопросик сохраняется */
}
Ловушка 2: значение Port напрямую как «порт есть»:Ловушка 2: значение Port напрямую как «порт есть»

Симптом: у трёх входов :0, пустого порта :, :99999 Port==0 — обработаны как «без порта», суждение о порте по умолчанию ошибочно.

Причина: значение Port осмысленно только при выставленном XURL_PORT_VALUE; три входа с Port==0 семантически различны (явный ноль/пусто/вне диапазона с лексической сохранностью). Два шага: бит присутствия + бит значения.

错误写法
uint16 Port = Url.Port ? Url.Port : 443;   /* все три Port==0 идут в дефолт */
正确写法
uint16 Port;
if ( !xrtUrlPort(&Url, &Port) ) {   /* доступность значения за один шаг (с восполнением дефолта) */
	Port = 443;                      /* дефолт только при невыразимом порте */
}
Ловушка 3: незакодированный UTF-8/китайский напрямую в URL:Ловушка 3: незакодированный UTF-8/китайский напрямую в URL

Симптом: вписанный в URL китайский путь не разбирается или на проде кракозябры — противоположная сторона, читая ASCII URI, понимает другое.

Причина: URI — ASCII-синтаксис; не-ASCII обязан сперва пройти percent-кодирование (путь/запрос) или punycode (host). xurl не гадает и не чинит — нелегальный ввод явно отвергается; исправление — ответственность верхнего слоя (charset/codec).

错误写法
snprintf(Url, "%s/search?q=%s", Base, "中文关键词");  /* голый UTF-8 в URL */
正确写法
/* сперва percent-кодирование, потом в URL (обязанность слоя codec главы 28) */
percent_encode("中文关键词", Encoded, sizeof(Encoded));
snprintf(Url, "%s/search?q=%s", Base, Encoded);

Практические задания

Базовое: матрица разбора пяти форм

Разберите пять входов: полный https URL, mailto:user@example.com, urn:isbn:0451450523, //host/path (сетевой путь), ../rel?x=1 (относительная ссылка) — печатайте Flags и непустые компоненты каждой группы. Критерии приёмки: структурные различия пяти групп (scheme/authority/относительность) согласуются с RFC 3986.

Продвинутое: тест верности цикла

Выберите десять «каверзных» URL (с пустым query, пустым fragment, портом :00080, scheme в разных регистрах, IPv6 host) — parse→write→повторный parse, сравните две структуры поле за полем. Критерии приёмки: все десять проходят цикл с сохранением (включая биты присутствия пустых компонентов); :00080 сохраняется лексически.

Вызов: разворачиватель Location перенаправлений

Реализуйте next_url(当前URL, Location头值) (текущий URL, значение заголовка Location): покройте пять форм Location (абсолютная, сетевой путь, абсолютный путь, относительный путь, наследование пустого fragment), разворачивайте UrlResolve — с правилами наследования/блокирования # из главы 100. Проиграйте цепочку перенаправлений реального сайта (записанную curl -L). Критерии приёмки: результат разворота совпадает с итоговым URL, по которому шёл curl; правила наследования fragment согласованы с RFC/главой 99; весь путь без промежуточных выделений (форма прямого выписывания Resolve).

Итоги

ТемаКратко
Диапазон разбораполный URI-reference RFC 3986 (mailto/urn/сетевой путь/относительная); ASCII; ZoneID отвергается
Биты присутствияPath присутствует всегда, может быть пуст; остальные компоненты — по битам вроде XURL_HAS_QUERY; пусто ≠ отсутствует
ПортPortText сохраняет лексику; PORT_VALUE даёт значение; три случая Port==0 семантически различны; UrlPort — один шаг
Семейство Writeбез \0; пустая ёмкость запрашивает длину; короткий буфер — ноль записанного; перекрытие отвергается; Target = origin-form без fragment
Разрешение ссылокUrlResolve разворачивает формы ../rel; без выделения, отказ не меняет; семейство Build одним выделением даёт итог
Безопасность невыравненностиструктуры/вывод длин допускают невыравненность; заворот отвергается
Дисциплина представленийвсё заимствует ввод; вывод не перекрывает ввод
Границы разделенияpercent/punycode/заголовок Host/ключ-значение query — отдельные слои; xurl ведает структурой
HTTP-специализацияразбор заголовков origin/Forwarded/Link — самостоятельные модули той же семьи

📖 Справочники API в этой главе:xhttp-url (17 записей)xhttp-query (8 записей)net (247 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки