Ориентация главы
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 в слоях
102.3示例
Первая полная программа: разбор, выписывание target и разрешение ссылок
Программа ниже из 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; }
Что только что произошло. (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 — валидация ссылки без временных строк:
#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; }
Что только что произошло. (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 ведает только структурой.
Типичные ошибки
Симптом: пустой 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 есть (может быть пустым) - при выписывании вопросик сохраняется */ }
Симптом: у трёх входов :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; /* дефолт только при невыразимом порте */ }
Симптом: вписанный в 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 сохраняется лексически.
Реализуйте 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки