Commit a0a11676e3b for php.net
commit a0a11676e3b7987ba9d196e5525d734a3ff13b0e
Author: Máté Kocsis <kocsismate@woohoolabs.com>
Date: Sat Aug 15 13:02:11 2026 +0200
lexbor: Add new URL patches (#23274)
- URL: added public IPv6 parser.
(https://github.com/lexbor/lexbor/commit/a7dbebbe)
- URL: added public percent-encoder API. (https://github.com/lexbor/lexbor/commit/2b24b565)
Both of which commits are needed for https://wiki.php.net/rfc/uri_followup
diff --git a/ext/lexbor/lexbor/url/url.c b/ext/lexbor/lexbor/url/url.c
index 8099c12089b..69d91969a6a 100644
--- a/ext/lexbor/lexbor/url/url.c
+++ b/ext/lexbor/lexbor/url/url.c
@@ -27,20 +27,6 @@
#define LXB_URL_BUFFER_NUM_SIZE 128
-typedef enum {
- LXB_URL_MAP_UNDEF = 0x00,
- LXB_URL_MAP_C0 = 0x01,
- LXB_URL_MAP_FRAGMENT = 0x02,
- LXB_URL_MAP_QUERY = 0x04,
- LXB_URL_MAP_SPECIAL_QUERY = 0x08,
- LXB_URL_MAP_PATH = 0x10,
- LXB_URL_MAP_USERINFO = 0x20,
- LXB_URL_MAP_COMPONENT = 0x40,
- LXB_URL_MAP_X_WWW_FORM = 0x80,
- LXB_URL_MAP_ALL = 0xff
-}
-lxb_url_map_type_t;
-
typedef enum {
LXB_URL_HOST_OPT_UNDEF = 0 << 0,
LXB_URL_HOST_OPT_NOT_SPECIAL = 1 << 0,
@@ -563,7 +549,7 @@ lxb_url_path_fix_windows_drive(lxb_url_t *url, lxb_char_t *sbuf,
static lxb_status_t
lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
const lxb_char_t *end, lexbor_str_t *str,
- lexbor_mraw_t *mraw,
+ lexbor_mraw_t *mraw, const uint8_t *url_map,
const lxb_encoding_data_t *encoding,
lxb_url_map_type_t enmap,
bool space_as_plus);
@@ -571,7 +557,7 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
static lxb_status_t
lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
const lxb_char_t *end, lexbor_str_t *str,
- lexbor_mraw_t *mraw,
+ lexbor_mraw_t *mraw, const uint8_t *url_map,
lxb_url_map_type_t enmap,
bool space_as_plus);
@@ -1757,9 +1743,9 @@ lxb_url_parse_basic_h(lxb_url_parser_t *parser, lxb_url_t *url,
tmp = (pswd != NULL) ? pswd - 1 : p;
if (tmp > begin) {
- status = lxb_url_percent_encode_after_utf_8(begin, tmp,
- &url->username, url->mraw,
- LXB_URL_MAP_USERINFO, false);
+ status = lxb_url_percent_encode_after_utf_8(begin,
+ tmp, &url->username, url->mraw, lxb_url_map,
+ LXB_URL_MAP_USERINFO, false);
if (status != LXB_STATUS_OK) {
lxb_url_parse_return(orig_data, buf, status);
}
@@ -1768,8 +1754,8 @@ lxb_url_parse_basic_h(lxb_url_parser_t *parser, lxb_url_t *url,
if (pswd != NULL && p > pswd) {
status = lxb_url_percent_encode_after_utf_8(pswd, p,
- &url->password, url->mraw,
- LXB_URL_MAP_USERINFO, false);
+ &url->password, url->mraw, lxb_url_map,
+ LXB_URL_MAP_USERINFO, false);
if (status != LXB_STATUS_OK) {
lxb_url_parse_return(orig_data, buf, status);
}
@@ -2319,8 +2305,8 @@ lxb_url_parse_basic_h(lxb_url_parser_t *parser, lxb_url_t *url,
if (p >= end) {
tmp_str.data = NULL;
- status = lxb_url_percent_encode_after_utf_8(begin, p,
- &tmp_str, url->mraw,
+ status = lxb_url_percent_encode_after_utf_8(begin, p, &tmp_str,
+ url->mraw, lxb_url_map,
LXB_URL_MAP_C0, false);
if (status != LXB_STATUS_OK) {
lxb_url_parse_return(orig_data, buf, status);
@@ -2336,8 +2322,8 @@ lxb_url_parse_basic_h(lxb_url_parser_t *parser, lxb_url_t *url,
if (c == '#' || c == '?') {
tmp_str.data = NULL;
- status = lxb_url_percent_encode_after_utf_8(begin, p,
- &tmp_str, url->mraw,
+ status = lxb_url_percent_encode_after_utf_8(begin, p, &tmp_str,
+ url->mraw, lxb_url_map,
LXB_URL_MAP_C0, false);
if (status != LXB_STATUS_OK) {
lxb_url_parse_return(orig_data, buf, status);
@@ -2407,7 +2393,8 @@ lxb_url_parse_basic_h(lxb_url_parser_t *parser, lxb_url_t *url,
status = lxb_url_percent_encode_after_encoding(begin, p,
&url->query,
- url->mraw, enc,
+ url->mraw,
+ lxb_url_map, enc,
map_type, false);
if (status != LXB_STATUS_OK) {
lxb_url_parse_return(orig_data, buf, status);
@@ -2461,7 +2448,7 @@ lxb_url_parse_basic_h(lxb_url_parser_t *parser, lxb_url_t *url,
}
status = lxb_url_percent_encode_after_utf_8(begin, p, &url->fragment,
- url->mraw,
+ url->mraw, lxb_url_map,
LXB_URL_MAP_FRAGMENT, false);
lxb_url_parse_return(orig_data, buf, status);
@@ -3161,10 +3148,23 @@ lxb_url_scheme_find(const lxb_char_t *data, size_t length)
return &lxb_url_scheme_res[LXB_URL_SCHEMEL_TYPE__UNKNOWN];
}
+lxb_status_t
+lxb_url_percent_encode_encoding(const lxb_char_t *data, size_t length,
+ lexbor_str_t *str, lexbor_mraw_t *mraw,
+ const uint8_t *url_map,
+ const lxb_encoding_data_t *encoding,
+ lxb_url_map_type_t enmap,
+ bool space_as_plus)
+{
+ return lxb_url_percent_encode_after_encoding(data, data + length, str, mraw,
+ url_map, encoding, enmap,
+ space_as_plus);
+}
+
static lxb_status_t
lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
const lxb_char_t *end, lexbor_str_t *str,
- lexbor_mraw_t *mraw,
+ lexbor_mraw_t *mraw, const uint8_t *url_map,
const lxb_encoding_data_t *encoding,
lxb_url_map_type_t enmap,
bool space_as_plus)
@@ -3182,7 +3182,8 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
if (encoding->encoding == LXB_ENCODING_UTF_8) {
return lxb_url_percent_encode_after_utf_8(data, end, str, mraw,
- enmap, space_as_plus);
+ url_map, enmap,
+ space_as_plus);
}
lxb_url_encoding_init(encoding, &encode);
@@ -3193,7 +3194,7 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
/* Only valid for UTF-8. */
while (p < end) {
- if (lxb_url_map[*p++] & enmap) {
+ if (url_map[*p++] & enmap) {
length += 2;
}
}
@@ -3249,7 +3250,7 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
}
}
- else if (lxb_url_map[c] & enmap) {
+ else if (url_map[c] & enmap) {
percent[1] = lexbor_str_res_char_to_two_hex_value[c][0];
percent[2] = lexbor_str_res_char_to_two_hex_value[c][1];
@@ -3280,10 +3281,20 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
return LXB_STATUS_OK;
}
+lxb_status_t
+lxb_url_percent_encode_utf_8(const lxb_char_t *data, size_t length,
+ lexbor_str_t *str, lexbor_mraw_t *mraw,
+ const uint8_t *url_map, lxb_url_map_type_t enmap,
+ bool space_as_plus)
+{
+ return lxb_url_percent_encode_after_utf_8(data, data + length, str, mraw,
+ url_map, enmap, space_as_plus);
+}
+
static lxb_status_t
lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
const lxb_char_t *end, lexbor_str_t *str,
- lexbor_mraw_t *mraw,
+ lexbor_mraw_t *mraw, const uint8_t *url_map,
lxb_url_map_type_t enmap,
bool space_as_plus)
{
@@ -3298,7 +3309,7 @@ lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
/* Only valid for UTF-8. */
while (p < end) {
- if (lxb_url_map[*p++] & enmap) {
+ if (url_map[*p++] & enmap) {
length += 2;
}
}
@@ -3317,7 +3328,7 @@ lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
if (space_as_plus && c == ' ') {
*pd++ = '+';
}
- else if (lxb_url_map[c] & enmap) {
+ else if (url_map[c] & enmap) {
*pd++ = '%';
*pd++ = lexbor_str_res_char_to_two_hex_value[c][0];
*pd++ = lexbor_str_res_char_to_two_hex_value[c][1];
@@ -3335,6 +3346,12 @@ lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
return LXB_STATUS_OK;
}
+const uint8_t *
+lxb_url_get_percent_encoding_map(void)
+{
+ return lxb_url_map;
+}
+
static lxb_status_t
lxb_url_host_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
const lxb_char_t *end, lxb_url_host_t *host,
@@ -3752,6 +3769,46 @@ lxb_url_is_ipv4(lxb_url_parser_t *parser, const lxb_char_t *data,
return status != LXB_STATUS_ERROR;
}
+lxb_status_t
+lxb_url_parse_host_ipv6(lxb_url_parser_t *parser, const lxb_char_t *data,
+ size_t length, uint16_t *ipv6)
+{
+ lxb_status_t status;
+ lxb_url_parser_t self_parser;
+
+ if (parser == NULL) {
+ parser = &self_parser;
+
+ parser->log = NULL;
+ parser->idna = NULL;
+ parser->buffer = NULL;
+ }
+
+ if (data < data + length && *data == '[') {
+ if (data[length - 1] != ']') {
+ (void) lxb_url_log_append(parser, &data[length - 1],
+ LXB_URL_ERROR_TYPE_IPV6_UNCLOSED);
+
+ status = LXB_STATUS_ERROR_UNEXPECTED_DATA;
+
+ goto done;
+ }
+
+ data += 1;
+ length -= 2;
+ }
+
+ status = lxb_url_ipv6_parse(parser, data, data + length, ipv6);
+
+done:
+
+ if (parser == &self_parser) {
+ lxb_url_parser_destroy(parser, false);
+ }
+
+ return status;
+}
+
static lxb_status_t
lxb_url_ipv6_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
const lxb_char_t *end, uint16_t *ipv6)
@@ -3763,6 +3820,8 @@ lxb_url_ipv6_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
const lxb_char_t *p;
lxb_url_error_type_t err_type;
+ memset(ipv6, 0x00, sizeof(uint16_t) * 8);
+
piece = ipv6;
compress = NULL;
p = data;
@@ -4023,7 +4082,7 @@ lxb_url_opaque_host_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
host->type = LXB_URL_HOST_TYPE_OPAQUE;
return lxb_url_percent_encode_after_utf_8(data, end, &host->u.opaque, mraw,
- LXB_URL_MAP_C0, false);
+ lxb_url_map, LXB_URL_MAP_C0, false);
}
static lxb_status_t
@@ -4302,7 +4361,8 @@ lxb_url_api_username_set(lxb_url_t *url,
return lxb_url_percent_encode_after_utf_8(username, username + length,
&url->username, url->mraw,
- LXB_URL_MAP_USERINFO, false);
+ lxb_url_map, LXB_URL_MAP_USERINFO,
+ false);
}
lxb_status_t
@@ -4322,7 +4382,8 @@ lxb_url_api_password_set(lxb_url_t *url,
return lxb_url_percent_encode_after_utf_8(password, password + length,
&url->password, url->mraw,
- LXB_URL_MAP_USERINFO, false);
+ lxb_url_map, LXB_URL_MAP_USERINFO,
+ false);
}
lxb_status_t
diff --git a/ext/lexbor/lexbor/url/url.h b/ext/lexbor/lexbor/url/url.h
index 6cc6f1081c8..d2c93080c92 100644
--- a/ext/lexbor/lexbor/url/url.h
+++ b/ext/lexbor/lexbor/url/url.h
@@ -81,6 +81,20 @@ typedef enum {
}
lxb_url_state_t;
+typedef enum {
+ LXB_URL_MAP_UNDEF = 0x00,
+ LXB_URL_MAP_C0 = 0x01,
+ LXB_URL_MAP_FRAGMENT = 0x02,
+ LXB_URL_MAP_QUERY = 0x04,
+ LXB_URL_MAP_SPECIAL_QUERY = 0x08,
+ LXB_URL_MAP_PATH = 0x10,
+ LXB_URL_MAP_USERINFO = 0x20,
+ LXB_URL_MAP_COMPONENT = 0x40,
+ LXB_URL_MAP_X_WWW_FORM = 0x80,
+ LXB_URL_MAP_ALL = 0xff
+}
+lxb_url_map_type_t;
+
/*
* New values can only be added downwards.
* Before LXB_URL_SCHEMEL_TYPE__LAST_ENTRY.
@@ -321,6 +335,114 @@ lxb_url_parse_basic(lxb_url_parser_t *parser, lxb_url_t *url,
const lxb_char_t *data, size_t length,
lxb_url_state_t override_state, lxb_encoding_t encoding);
+/*
+ * IPv6 parser.
+ *
+ * This function is an implementation of IPv6 parsing according to the WHATWG
+ * specification.
+ * https://url.spec.whatwg.org/#concept-ipv6-parser
+ *
+ * The address can be passed both with and without the surrounding square
+ * brackets: "::1" and "[::1]" give the same result. If the opening bracket is
+ * present, the closing one is required.
+ *
+ * The output buffer is zeroed by the function, there is no need to prepare it.
+ * Use the lxb_url_serialize_host_ipv6() function to serialize the result.
+ *
+ * @param[in] lxb_url_parser_t *. Can be NULL.
+ * @param[in] Pointer to the beginning of the data. Not NULL.
+ * @param[in] Length of the data. Can be 0.
+ * @param[out] Buffer for eight (uint16_t[8]) IPv6 pieces. Not NULL. The value
+ * is meaningful only if LXB_STATUS_OK is returned.
+ *
+ * @return LXB_STATUS_OK if successful, otherwise an error status value.
+ */
+LXB_API lxb_status_t
+lxb_url_parse_host_ipv6(lxb_url_parser_t *parser, const lxb_char_t *data,
+ size_t length, uint16_t *ipv6);
+
+/*
+ * UTF-8 percent-encoder.
+ *
+ * Percent-encodes bytes from data according to url_map and appends the result
+ * to str. A byte is encoded as "%HH" when the result of
+ * (url_map[byte] & enmap) is non-zero. Uppercase hexadecimal digits are used.
+ * If space_as_plus is true, U+0020 SPACE is encoded as '+' before the map is
+ * checked.
+ *
+ * The input is expected to be valid UTF-8; the function does not validate it.
+ *
+ * @param[in] Pointer to UTF-8 data. Not NULL.
+ * @param[in] Length of data. Can be 0.
+ * @param[in, out] Output string. Can be uninitialized (data = NULL). Encoded
+ * data is appended to any existing content. Not NULL.
+ * @param[in] Memory object used to allocate or resize the output string. Not
+ * NULL.
+ * @param[in] Table of 256 entries indexed by input byte, each entry is a bit
+ * mask of lxb_url_map_type_t values. Not NULL.
+ * @param[in] Mask selecting the bytes to percent-encode.
+ * @param[in] Replace U+0020 SPACE with '+' if true.
+ *
+ * @return LXB_STATUS_OK if successful, otherwise an error status value.
+ */
+LXB_API lxb_status_t
+lxb_url_percent_encode_utf_8(const lxb_char_t *data, size_t length,
+ lexbor_str_t *str, lexbor_mraw_t *mraw,
+ const uint8_t *url_map, lxb_url_map_type_t enmap,
+ bool space_as_plus);
+
+/*
+ * Percent-encode after encoding.
+ *
+ * Converts valid UTF-8 data to the specified encoding and appends the
+ * percent-encoded result to str. Each encoded byte for which
+ * (url_map[byte] & enmap) is non-zero is written as "%HH" using uppercase
+ * hexadecimal digits. If a code point cannot be represented in the target
+ * encoding, its percent-encoded numeric character reference is appended.
+ *
+ * If encoding is UTF-8, no conversion is performed. If space_as_plus is true,
+ * an encoded U+0020 SPACE is replaced with '+' before the map is checked.
+ * The input is expected to be valid UTF-8; the function does not validate it.
+ *
+ * @param[in] Pointer to UTF-8 data. Not NULL.
+ * @param[in] Length of data. Can be 0.
+ * @param[in, out] Output string. Can be uninitialized (data = NULL). Encoded
+ * data is appended to any existing content. Not NULL.
+ * @param[in] Memory object used to allocate or resize the output string. Not
+ * NULL.
+ * @param[in] Table of 256 entries indexed by encoded byte, each entry is a bit
+ * mask of lxb_url_map_type_t values. Not NULL.
+ * @param[in] Target encoding. Not NULL.
+ * @param[in] Mask selecting the bytes to percent-encode.
+ * @param[in] Replace an encoded U+0020 SPACE with '+' if true.
+ *
+ * @return LXB_STATUS_OK if successful, otherwise an error status value.
+ */
+LXB_API lxb_status_t
+lxb_url_percent_encode_encoding(const lxb_char_t *data, size_t length,
+ lexbor_str_t *str, lexbor_mraw_t *mraw,
+ const uint8_t *url_map,
+ const lxb_encoding_data_t *encoding,
+ lxb_url_map_type_t enmap,
+ bool space_as_plus);
+
+/*
+ * Get the URL percent-encoding map.
+ *
+ * Returns the built-in lookup table for the percent-encode sets defined by the
+ * URL specification. The table contains 256 entries indexed by byte value.
+ * Each entry is a bit mask of the lxb_url_map_type_t sets in which the byte
+ * must be percent-encoded.
+ *
+ * The returned map can be passed to lxb_url_percent_encode_utf_8() or
+ * lxb_url_percent_encode_encoding(). It has static storage duration and must
+ * not be modified or freed.
+ *
+ * @return Pointer to a read-only table of 256 entries. Never NULL.
+ */
+LXB_API const uint8_t *
+lxb_url_get_percent_encoding_map(void);
+
/*
* Erase URL.
*
diff --git a/ext/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch b/ext/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
index f23ec0f5034..b9c4cda7a03 100644
--- a/ext/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
+++ b/ext/lexbor/patches/0001-Expose-line-and-column-information-for-use-in-PHP.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Sat, 26 Aug 2023 15:08:59 +0200
-Subject: [PATCH 01/11] Expose line and column information for use in PHP
+Subject: [PATCH 01/13] Expose line and column information for use in PHP
---
source/lexbor/dom/interfaces/node.h | 2 ++
diff --git a/ext/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch b/ext/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
index 8758c09a2e8..84868e209e7 100644
--- a/ext/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
+++ b/ext/lexbor/patches/0002-Track-implied-added-nodes-for-options-use-in-PHP.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Mon, 14 Aug 2023 20:18:51 +0200
-Subject: [PATCH 02/11] Track implied added nodes for options use in PHP
+Subject: [PATCH 02/13] Track implied added nodes for options use in PHP
---
source/lexbor/html/tree.h | 3 +++
diff --git a/ext/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch b/ext/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
index 56458a49dee..35750437667 100644
--- a/ext/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
+++ b/ext/lexbor/patches/0003-Patch-utilities-and-data-structure-to-be-able-to-gen.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Thu, 24 Aug 2023 22:57:48 +0200
-Subject: [PATCH 03/11] Patch utilities and data structure to be able to
+Subject: [PATCH 03/13] Patch utilities and data structure to be able to
generate smaller lookup tables
Changed the generation script to check if everything fits in 32-bits.
diff --git a/ext/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch b/ext/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
index f4fe2050998..029c0ff6933 100644
--- a/ext/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
+++ b/ext/lexbor/patches/0004-Remove-unused-upper-case-tag-static-data.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Wed, 29 Nov 2023 21:26:47 +0100
-Subject: [PATCH 04/11] Remove unused upper case tag static data
+Subject: [PATCH 04/13] Remove unused upper case tag static data
---
source/lexbor/tag/res.h | 2 ++
diff --git a/ext/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch b/ext/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
index f36a2d75818..a8771beea0f 100644
--- a/ext/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
+++ b/ext/lexbor/patches/0005-Shrink-size-of-static-binary-search-tree.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Wed, 29 Nov 2023 21:29:31 +0100
-Subject: [PATCH 05/11] Shrink size of static binary search tree
+Subject: [PATCH 05/13] Shrink size of static binary search tree
This also makes it more efficient on the data cache.
---
diff --git a/ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch b/ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
index 7c6e1beebf4..a81d614575c 100644
--- a/ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
+++ b/ext/lexbor/patches/0006-Patch-out-unused-CSS-style-code.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Niels Dossche <7771979+nielsdos@users.noreply.github.com>
Date: Sun, 7 Jan 2024 21:59:28 +0100
-Subject: [PATCH 06/11] Patch out unused CSS style code
+Subject: [PATCH 06/13] Patch out unused CSS style code
---
source/lexbor/css/rule.h | 2 ++
diff --git a/ext/lexbor/patches/0007-Add-lxb_url_is_special-to-the-public-API-362.patch b/ext/lexbor/patches/0007-Add-lxb_url_is_special-to-the-public-API-362.patch
index 50bdc2397ba..c49cf53628b 100644
--- a/ext/lexbor/patches/0007-Add-lxb_url_is_special-to-the-public-API-362.patch
+++ b/ext/lexbor/patches/0007-Add-lxb_url_is_special-to-the-public-API-362.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= <kocsismate@woohoolabs.com>
Date: Sun, 17 May 2026 22:17:14 +0200
-Subject: [PATCH 07/11] Add lxb_url_is_special() to the public API (#362)
+Subject: [PATCH 07/13] Add lxb_url_is_special() to the public API (#362)
As https://wiki.php.net/rfc/uri_followup#uri_type_detection relies on this information.
---
diff --git a/ext/lexbor/patches/0008-URL-fixed-setters-for-empty-hosts.patch b/ext/lexbor/patches/0008-URL-fixed-setters-for-empty-hosts.patch
index 4218461c3ba..c8c8e93686d 100644
--- a/ext/lexbor/patches/0008-URL-fixed-setters-for-empty-hosts.patch
+++ b/ext/lexbor/patches/0008-URL-fixed-setters-for-empty-hosts.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexander Borisov <lex.borisov@gmail.com>
Date: Fri, 26 Jun 2026 18:55:56 +0300
-Subject: [PATCH 08/11] URL: fixed setters for empty hosts.
+Subject: [PATCH 08/13] URL: fixed setters for empty hosts.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
diff --git a/ext/lexbor/patches/0009-URL-fixed-uninitialized-memory-in-the-path-buffer-gr.patch b/ext/lexbor/patches/0009-URL-fixed-uninitialized-memory-in-the-path-buffer-gr.patch
index 91e78a899f4..d95503378be 100644
--- a/ext/lexbor/patches/0009-URL-fixed-uninitialized-memory-in-the-path-buffer-gr.patch
+++ b/ext/lexbor/patches/0009-URL-fixed-uninitialized-memory-in-the-path-buffer-gr.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alexander Borisov <lex.borisov@gmail.com>
Date: Fri, 5 Jun 2026 22:13:32 +0300
-Subject: [PATCH 09/11] URL: fixed uninitialized memory in the path buffer
+Subject: [PATCH 09/13] URL: fixed uninitialized memory in the path buffer
growth.
When a path was long enough to outgrow the on-stack buffer, the first
diff --git a/ext/lexbor/patches/0010-Fix-parsing-for-URL-containing-empty-host-and-userin.patch b/ext/lexbor/patches/0010-Fix-parsing-for-URL-containing-empty-host-and-userin.patch
index 9cbf3e0094e..624943a4924 100644
--- a/ext/lexbor/patches/0010-Fix-parsing-for-URL-containing-empty-host-and-userin.patch
+++ b/ext/lexbor/patches/0010-Fix-parsing-for-URL-containing-empty-host-and-userin.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= <kocsismate@woohoolabs.com>
Date: Thu, 9 Jul 2026 21:51:05 +0200
-Subject: [PATCH 10/11] Fix parsing for URL containing empty host and userinfo
+Subject: [PATCH 10/13] Fix parsing for URL containing empty host and userinfo
The returned error code (LXB_URL_ERROR_TYPE_INVALID_CREDENTIALS) apparently contradicts the specification:
diff --git a/ext/lexbor/patches/0011-Percent-encode-the-caret-in-the-path.patch b/ext/lexbor/patches/0011-Percent-encode-the-caret-in-the-path.patch
index 2781fad5bcb..f971dc39cff 100644
--- a/ext/lexbor/patches/0011-Percent-encode-the-caret-in-the-path.patch
+++ b/ext/lexbor/patches/0011-Percent-encode-the-caret-in-the-path.patch
@@ -1,7 +1,7 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?M=C3=A1t=C3=A9=20Kocsis?= <kocsismate@woohoolabs.com>
Date: Fri, 10 Jul 2026 22:31:16 +0200
-Subject: [PATCH 11/11] Percent-encode the caret in the path
+Subject: [PATCH 11/13] Percent-encode the caret in the path
The caret (^) is part of the path percent-encode set:
diff --git a/ext/lexbor/patches/0012-URL-added-public-IPv6-parser.patch b/ext/lexbor/patches/0012-URL-added-public-IPv6-parser.patch
new file mode 100644
index 00000000000..a005dd13491
--- /dev/null
+++ b/ext/lexbor/patches/0012-URL-added-public-IPv6-parser.patch
@@ -0,0 +1,320 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Alexander Borisov <lex.borisov@gmail.com>
+Date: Wed, 12 Aug 2026 23:29:20 +0300
+Subject: [PATCH 12/13] URL: added public IPv6 parser.
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Added lxb_url_parse_host_ipv6() — a public entry point to the IPv6
+parser from the WHATWG specification:
+https://url.spec.whatwg.org/#concept-ipv6-parser
+
+The address is accepted both with and without the surrounding square
+brackets: "::1" and "[::1]" give the same result.
+
+https://github.com/lexbor/lexbor/pull/402
+
+The API was requested in #402 for use by php/php-src#22268.
+
+Suggested-by: Máté Kocsis (@kocsismate)
+---
+ source/lexbor/url/url.c | 42 +++++++
+ source/lexbor/url/url.h | 26 ++++
+ test/lexbor/url/parse_host_ipv6.c | 190 ++++++++++++++++++++++++++++++
+ 3 files changed, 258 insertions(+)
+ create mode 100644 test/lexbor/url/parse_host_ipv6.c
+
+diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
+index 8099c12..7487762 100644
+--- a/source/lexbor/url/url.c
++++ b/source/lexbor/url/url.c
+@@ -3752,6 +3752,46 @@ lxb_url_is_ipv4(lxb_url_parser_t *parser, const lxb_char_t *data,
+ return status != LXB_STATUS_ERROR;
+ }
+
++lxb_status_t
++lxb_url_parse_host_ipv6(lxb_url_parser_t *parser, const lxb_char_t *data,
++ size_t length, uint16_t *ipv6)
++{
++ lxb_status_t status;
++ lxb_url_parser_t self_parser;
++
++ if (parser == NULL) {
++ parser = &self_parser;
++
++ parser->log = NULL;
++ parser->idna = NULL;
++ parser->buffer = NULL;
++ }
++
++ if (data < data + length && *data == '[') {
++ if (data[length - 1] != ']') {
++ (void) lxb_url_log_append(parser, &data[length - 1],
++ LXB_URL_ERROR_TYPE_IPV6_UNCLOSED);
++
++ status = LXB_STATUS_ERROR_UNEXPECTED_DATA;
++
++ goto done;
++ }
++
++ data += 1;
++ length -= 2;
++ }
++
++ status = lxb_url_ipv6_parse(parser, data, data + length, ipv6);
++
++done:
++
++ if (parser == &self_parser) {
++ lxb_url_parser_destroy(parser, false);
++ }
++
++ return status;
++}
++
+ static lxb_status_t
+ lxb_url_ipv6_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
+ const lxb_char_t *end, uint16_t *ipv6)
+@@ -3763,6 +3803,8 @@ lxb_url_ipv6_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
+ const lxb_char_t *p;
+ lxb_url_error_type_t err_type;
+
++ memset(ipv6, 0x00, sizeof(uint16_t) * 8);
++
+ piece = ipv6;
+ compress = NULL;
+ p = data;
+diff --git a/source/lexbor/url/url.h b/source/lexbor/url/url.h
+index 6cc6f10..aa50485 100644
+--- a/source/lexbor/url/url.h
++++ b/source/lexbor/url/url.h
+@@ -321,6 +321,32 @@ lxb_url_parse_basic(lxb_url_parser_t *parser, lxb_url_t *url,
+ const lxb_char_t *data, size_t length,
+ lxb_url_state_t override_state, lxb_encoding_t encoding);
+
++/*
++ * IPv6 parser.
++ *
++ * This function is an implementation of IPv6 parsing according to the WHATWG
++ * specification.
++ * https://url.spec.whatwg.org/#concept-ipv6-parser
++ *
++ * The address can be passed both with and without the surrounding square
++ * brackets: "::1" and "[::1]" give the same result. If the opening bracket is
++ * present, the closing one is required.
++ *
++ * The output buffer is zeroed by the function, there is no need to prepare it.
++ * Use the lxb_url_serialize_host_ipv6() function to serialize the result.
++ *
++ * @param[in] lxb_url_parser_t *. Can be NULL.
++ * @param[in] Pointer to the beginning of the data. Not NULL.
++ * @param[in] Length of the data. Can be 0.
++ * @param[out] Buffer for eight (uint16_t[8]) IPv6 pieces. Not NULL. The value
++ * is meaningful only if LXB_STATUS_OK is returned.
++ *
++ * @return LXB_STATUS_OK if successful, otherwise an error status value.
++ */
++LXB_API lxb_status_t
++lxb_url_parse_host_ipv6(lxb_url_parser_t *parser, const lxb_char_t *data,
++ size_t length, uint16_t *ipv6);
++
+ /*
+ * Erase URL.
+ *
+diff --git a/test/lexbor/url/parse_host_ipv6.c b/test/lexbor/url/parse_host_ipv6.c
+new file mode 100644
+index 0000000..bbf5293
+--- /dev/null
++++ b/test/lexbor/url/parse_host_ipv6.c
+@@ -0,0 +1,190 @@
++/*
++ * Copyright (C) 2026 Alexander Borisov
++ *
++ * Author: Alexander Borisov <borisov@lexbor.com>
++ */
++
++#include <unit/test.h>
++#include <lexbor/url/url.h>
++
++
++typedef struct {
++ const lxb_char_t *input;
++ size_t length;
++ uint16_t ipv6[8];
++}
++ipv6_success_t;
++
++typedef struct {
++ const lxb_char_t *input;
++ lxb_url_error_type_t error;
++}
++ipv6_failure_t;
++
++
++static const ipv6_success_t success_entries[] = {
++ {
++ (const lxb_char_t *) "::",
++ sizeof("::") - 1,
++ {0, 0, 0, 0, 0, 0, 0, 0}
++ },
++ {
++ (const lxb_char_t *) "::1",
++ sizeof("::1") - 1,
++ {0, 0, 0, 0, 0, 0, 0, 1}
++ },
++ {
++ (const lxb_char_t *) "[::1]",
++ sizeof("[::1]") - 1,
++ {0, 0, 0, 0, 0, 0, 0, 1}
++ },
++ {
++ (const lxb_char_t *) "1:2:3:4:5:6:7:8",
++ sizeof("1:2:3:4:5:6:7:8") - 1,
++ {1, 2, 3, 4, 5, 6, 7, 8}
++ },
++ {
++ (const lxb_char_t *) "2001:db8::ff00:42:8329",
++ sizeof("2001:db8::ff00:42:8329") - 1,
++ {0x2001, 0x0db8, 0, 0, 0, 0xff00, 0x0042, 0x8329}
++ },
++ {
++ (const lxb_char_t *) "::ffff:192.0.2.1",
++ sizeof("::ffff:192.0.2.1") - 1,
++ {0, 0, 0, 0, 0, 0xffff, 0xc000, 0x0201}
++ },
++ {
++ (const lxb_char_t *) "[::1]ignored",
++ 5,
++ {0, 0, 0, 0, 0, 0, 0, 1}
++ }
++};
++
++static const ipv6_failure_t failure_entries[] = {
++ {
++ (const lxb_char_t *) "",
++ LXB_URL_ERROR_TYPE_IPV6_TOO_FEW_PIECES
++ },
++ {
++ (const lxb_char_t *) "[::1",
++ LXB_URL_ERROR_TYPE_IPV6_UNCLOSED
++ },
++ {
++ (const lxb_char_t *) ":",
++ LXB_URL_ERROR_TYPE_IPV6_INVALID_COMPRESSION
++ },
++ {
++ (const lxb_char_t *) "1::2::3",
++ LXB_URL_ERROR_TYPE_IPV6_MULTIPLE_COMPRESSION
++ },
++ {
++ (const lxb_char_t *) "1:2:3:4:5:6:7:8:9",
++ LXB_URL_ERROR_TYPE_IPV6_TOO_MANY_PIECES
++ },
++ {
++ (const lxb_char_t *) "1:2:3:4:5:6:7",
++ LXB_URL_ERROR_TYPE_IPV6_TOO_FEW_PIECES
++ },
++ {
++ (const lxb_char_t *) "1:2:3:4:5:6:7:g",
++ LXB_URL_ERROR_TYPE_IPV6_INVALID_CODE_POINT
++ },
++ {
++ (const lxb_char_t *) "1:2:3:4:5:6:7:1.2.3.4",
++ LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_TOO_MANY_PIECES
++ },
++ {
++ (const lxb_char_t *) "::ffff:.1.2.3",
++ LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_INVALID_CODE_POINT
++ },
++ {
++ (const lxb_char_t *) "::ffff:192.0.2.256",
++ LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_OUT_OF_RANGE_PART
++ },
++ {
++ (const lxb_char_t *) "::ffff:192.0.2",
++ LXB_URL_ERROR_TYPE_IPV4_IN_IPV6_TOO_FEW_PARTS
++ }
++};
++
++
++TEST_BEGIN(parse_success)
++{
++ size_t length;
++ lxb_status_t status;
++ uint16_t ipv6[8];
++
++ length = sizeof(success_entries) / sizeof(ipv6_success_t);
++
++ for (size_t i = 0; i < length; i++) {
++ memset(ipv6, 0xff, sizeof(ipv6));
++
++ status = lxb_url_parse_host_ipv6(NULL, success_entries[i].input,
++ success_entries[i].length, ipv6);
++ test_eq(status, LXB_STATUS_OK);
++
++ for (size_t j = 0; j < 8; j++) {
++ test_eq_u_short(ipv6[j], success_entries[i].ipv6[j]);
++ }
++ }
++}
++TEST_END
++
++TEST_BEGIN(parse_failure)
++{
++ size_t length;
++ lxb_status_t status;
++ lxb_url_parser_t parser;
++ lexbor_plog_entry_t *error;
++
++ status = lxb_url_parser_init(&parser, NULL);
++ test_eq(status, LXB_STATUS_OK);
++
++ length = sizeof(failure_entries) / sizeof(ipv6_failure_t);
++
++ for (size_t i = 0; i < length; i++) {
++ status = lxb_url_parse_host_ipv6(
++ &parser, failure_entries[i].input,
++ strlen((const char *) failure_entries[i].input),
++ (uint16_t[8]) {0});
++
++ test_eq(status, LXB_STATUS_ERROR_UNEXPECTED_DATA);
++ test_ne(parser.log, NULL);
++ test_eq_size(lexbor_plog_length(parser.log), 1UL);
++
++ error = lexbor_array_obj_get(&parser.log->list, 0);
++ test_ne(error, NULL);
++ test_eq(error->id, failure_entries[i].error);
++
++ lxb_url_parser_clean(&parser);
++ }
++
++ lxb_url_parser_memory_destroy(&parser);
++ lxb_url_parser_destroy(&parser, false);
++}
++TEST_END
++
++TEST_BEGIN(parse_failure_without_parser)
++{
++ lxb_status_t status;
++ uint16_t ipv6[8];
++
++ static const lexbor_str_t input = lexbor_str("::ffff:192.0.2.256");
++
++ status = lxb_url_parse_host_ipv6(NULL, input.data, input.length, ipv6);
++ test_eq(status, LXB_STATUS_ERROR_UNEXPECTED_DATA);
++}
++TEST_END
++
++int
++main(int argc, const char *argv[])
++{
++ TEST_INIT();
++
++ TEST_ADD(parse_success);
++ TEST_ADD(parse_failure);
++ TEST_ADD(parse_failure_without_parser);
++
++ TEST_RUN("lexbor/url/parse_host_ipv6");
++ TEST_RELEASE();
++}
diff --git a/ext/lexbor/patches/0013-URL-added-public-percent-encoder-API.patch b/ext/lexbor/patches/0013-URL-added-public-percent-encoder-API.patch
new file mode 100644
index 00000000000..c5d11c8f64f
--- /dev/null
+++ b/ext/lexbor/patches/0013-URL-added-public-percent-encoder-API.patch
@@ -0,0 +1,626 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Alexander Borisov <lex.borisov@gmail.com>
+Date: Thu, 13 Aug 2026 23:29:16 +0300
+Subject: [PATCH 13/13] URL: added public percent-encoder API.
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Added public entry points to the percent-encoder from the WHATWG
+specification:
+https://url.spec.whatwg.org/#percent-encoded-bytes
+
+ lxb_url_percent_encode_utf_8()
+ lxb_url_percent_encode_encoding()
+ lxb_url_get_percent_encoding_map()
+
+Both encoders take a caller-supplied table of 256 entries indexed by byte
+value, where each entry is a bit mask of lxb_url_map_type_t values, so the
+percent-encode sets can be adjusted without patching the library.
+lxb_url_get_percent_encoding_map() returns the built-in table for callers
+that need only the sets defined by the specification.
+
+https://github.com/lexbor/lexbor/pull/404
+
+The API was requested in #404 for use by PHP:
+https://wiki.php.net/rfc/uri_followup#percent-encoding_support
+
+Based-on-patch-by: Máté Kocsis (@kocsismate)
+---
+ source/lexbor/url/url.c | 93 ++++++++-----
+ source/lexbor/url/url.h | 96 +++++++++++++
+ test/lexbor/url/percent_encode.c | 228 +++++++++++++++++++++++++++++++
+ 3 files changed, 380 insertions(+), 37 deletions(-)
+ create mode 100644 test/lexbor/url/percent_encode.c
+
+diff --git a/source/lexbor/url/url.c b/source/lexbor/url/url.c
+index 7487762..69d9196 100644
+--- a/source/lexbor/url/url.c
++++ b/source/lexbor/url/url.c
+@@ -27,20 +27,6 @@
+ #define LXB_URL_BUFFER_NUM_SIZE 128
+
+
+-typedef enum {
+- LXB_URL_MAP_UNDEF = 0x00,
+- LXB_URL_MAP_C0 = 0x01,
+- LXB_URL_MAP_FRAGMENT = 0x02,
+- LXB_URL_MAP_QUERY = 0x04,
+- LXB_URL_MAP_SPECIAL_QUERY = 0x08,
+- LXB_URL_MAP_PATH = 0x10,
+- LXB_URL_MAP_USERINFO = 0x20,
+- LXB_URL_MAP_COMPONENT = 0x40,
+- LXB_URL_MAP_X_WWW_FORM = 0x80,
+- LXB_URL_MAP_ALL = 0xff
+-}
+-lxb_url_map_type_t;
+-
+ typedef enum {
+ LXB_URL_HOST_OPT_UNDEF = 0 << 0,
+ LXB_URL_HOST_OPT_NOT_SPECIAL = 1 << 0,
+@@ -563,7 +549,7 @@ lxb_url_path_fix_windows_drive(lxb_url_t *url, lxb_char_t *sbuf,
+ static lxb_status_t
+ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
+ const lxb_char_t *end, lexbor_str_t *str,
+- lexbor_mraw_t *mraw,
++ lexbor_mraw_t *mraw, const uint8_t *url_map,
+ const lxb_encoding_data_t *encoding,
+ lxb_url_map_type_t enmap,
+ bool space_as_plus);
+@@ -571,7 +557,7 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
+ static lxb_status_t
+ lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
+ const lxb_char_t *end, lexbor_str_t *str,
+- lexbor_mraw_t *mraw,
++ lexbor_mraw_t *mraw, const uint8_t *url_map,
+ lxb_url_map_type_t enmap,
+ bool space_as_plus);
+
+@@ -1757,9 +1743,9 @@ again:
+ tmp = (pswd != NULL) ? pswd - 1 : p;
+
+ if (tmp > begin) {
+- status = lxb_url_percent_encode_after_utf_8(begin, tmp,
+- &url->username, url->mraw,
+- LXB_URL_MAP_USERINFO, false);
++ status = lxb_url_percent_encode_after_utf_8(begin,
++ tmp, &url->username, url->mraw, lxb_url_map,
++ LXB_URL_MAP_USERINFO, false);
+ if (status != LXB_STATUS_OK) {
+ lxb_url_parse_return(orig_data, buf, status);
+ }
+@@ -1768,8 +1754,8 @@ again:
+
+ if (pswd != NULL && p > pswd) {
+ status = lxb_url_percent_encode_after_utf_8(pswd, p,
+- &url->password, url->mraw,
+- LXB_URL_MAP_USERINFO, false);
++ &url->password, url->mraw, lxb_url_map,
++ LXB_URL_MAP_USERINFO, false);
+ if (status != LXB_STATUS_OK) {
+ lxb_url_parse_return(orig_data, buf, status);
+ }
+@@ -2319,8 +2305,8 @@ again:
+ if (p >= end) {
+ tmp_str.data = NULL;
+
+- status = lxb_url_percent_encode_after_utf_8(begin, p,
+- &tmp_str, url->mraw,
++ status = lxb_url_percent_encode_after_utf_8(begin, p, &tmp_str,
++ url->mraw, lxb_url_map,
+ LXB_URL_MAP_C0, false);
+ if (status != LXB_STATUS_OK) {
+ lxb_url_parse_return(orig_data, buf, status);
+@@ -2336,8 +2322,8 @@ again:
+ if (c == '#' || c == '?') {
+ tmp_str.data = NULL;
+
+- status = lxb_url_percent_encode_after_utf_8(begin, p,
+- &tmp_str, url->mraw,
++ status = lxb_url_percent_encode_after_utf_8(begin, p, &tmp_str,
++ url->mraw, lxb_url_map,
+ LXB_URL_MAP_C0, false);
+ if (status != LXB_STATUS_OK) {
+ lxb_url_parse_return(orig_data, buf, status);
+@@ -2407,7 +2393,8 @@ again:
+
+ status = lxb_url_percent_encode_after_encoding(begin, p,
+ &url->query,
+- url->mraw, enc,
++ url->mraw,
++ lxb_url_map, enc,
+ map_type, false);
+ if (status != LXB_STATUS_OK) {
+ lxb_url_parse_return(orig_data, buf, status);
+@@ -2461,7 +2448,7 @@ again:
+ }
+
+ status = lxb_url_percent_encode_after_utf_8(begin, p, &url->fragment,
+- url->mraw,
++ url->mraw, lxb_url_map,
+ LXB_URL_MAP_FRAGMENT, false);
+ lxb_url_parse_return(orig_data, buf, status);
+
+@@ -3161,10 +3148,23 @@ lxb_url_scheme_find(const lxb_char_t *data, size_t length)
+ return &lxb_url_scheme_res[LXB_URL_SCHEMEL_TYPE__UNKNOWN];
+ }
+
++lxb_status_t
++lxb_url_percent_encode_encoding(const lxb_char_t *data, size_t length,
++ lexbor_str_t *str, lexbor_mraw_t *mraw,
++ const uint8_t *url_map,
++ const lxb_encoding_data_t *encoding,
++ lxb_url_map_type_t enmap,
++ bool space_as_plus)
++{
++ return lxb_url_percent_encode_after_encoding(data, data + length, str, mraw,
++ url_map, encoding, enmap,
++ space_as_plus);
++}
++
+ static lxb_status_t
+ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
+ const lxb_char_t *end, lexbor_str_t *str,
+- lexbor_mraw_t *mraw,
++ lexbor_mraw_t *mraw, const uint8_t *url_map,
+ const lxb_encoding_data_t *encoding,
+ lxb_url_map_type_t enmap,
+ bool space_as_plus)
+@@ -3182,7 +3182,8 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
+
+ if (encoding->encoding == LXB_ENCODING_UTF_8) {
+ return lxb_url_percent_encode_after_utf_8(data, end, str, mraw,
+- enmap, space_as_plus);
++ url_map, enmap,
++ space_as_plus);
+ }
+
+ lxb_url_encoding_init(encoding, &encode);
+@@ -3193,7 +3194,7 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
+ /* Only valid for UTF-8. */
+
+ while (p < end) {
+- if (lxb_url_map[*p++] & enmap) {
++ if (url_map[*p++] & enmap) {
+ length += 2;
+ }
+ }
+@@ -3249,7 +3250,7 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
+ return LXB_STATUS_ERROR_MEMORY_ALLOCATION;
+ }
+ }
+- else if (lxb_url_map[c] & enmap) {
++ else if (url_map[c] & enmap) {
+ percent[1] = lexbor_str_res_char_to_two_hex_value[c][0];
+ percent[2] = lexbor_str_res_char_to_two_hex_value[c][1];
+
+@@ -3280,10 +3281,20 @@ lxb_url_percent_encode_after_encoding(const lxb_char_t *data,
+ return LXB_STATUS_OK;
+ }
+
++lxb_status_t
++lxb_url_percent_encode_utf_8(const lxb_char_t *data, size_t length,
++ lexbor_str_t *str, lexbor_mraw_t *mraw,
++ const uint8_t *url_map, lxb_url_map_type_t enmap,
++ bool space_as_plus)
++{
++ return lxb_url_percent_encode_after_utf_8(data, data + length, str, mraw,
++ url_map, enmap, space_as_plus);
++}
++
+ static lxb_status_t
+ lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
+ const lxb_char_t *end, lexbor_str_t *str,
+- lexbor_mraw_t *mraw,
++ lexbor_mraw_t *mraw, const uint8_t *url_map,
+ lxb_url_map_type_t enmap,
+ bool space_as_plus)
+ {
+@@ -3298,7 +3309,7 @@ lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
+ /* Only valid for UTF-8. */
+
+ while (p < end) {
+- if (lxb_url_map[*p++] & enmap) {
++ if (url_map[*p++] & enmap) {
+ length += 2;
+ }
+ }
+@@ -3317,7 +3328,7 @@ lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
+ if (space_as_plus && c == ' ') {
+ *pd++ = '+';
+ }
+- else if (lxb_url_map[c] & enmap) {
++ else if (url_map[c] & enmap) {
+ *pd++ = '%';
+ *pd++ = lexbor_str_res_char_to_two_hex_value[c][0];
+ *pd++ = lexbor_str_res_char_to_two_hex_value[c][1];
+@@ -3335,6 +3346,12 @@ lxb_url_percent_encode_after_utf_8(const lxb_char_t *data,
+ return LXB_STATUS_OK;
+ }
+
++const uint8_t *
++lxb_url_get_percent_encoding_map(void)
++{
++ return lxb_url_map;
++}
++
+ static lxb_status_t
+ lxb_url_host_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
+ const lxb_char_t *end, lxb_url_host_t *host,
+@@ -4065,7 +4082,7 @@ lxb_url_opaque_host_parse(lxb_url_parser_t *parser, const lxb_char_t *data,
+ host->type = LXB_URL_HOST_TYPE_OPAQUE;
+
+ return lxb_url_percent_encode_after_utf_8(data, end, &host->u.opaque, mraw,
+- LXB_URL_MAP_C0, false);
++ lxb_url_map, LXB_URL_MAP_C0, false);
+ }
+
+ static lxb_status_t
+@@ -4344,7 +4361,8 @@ lxb_url_api_username_set(lxb_url_t *url,
+
+ return lxb_url_percent_encode_after_utf_8(username, username + length,
+ &url->username, url->mraw,
+- LXB_URL_MAP_USERINFO, false);
++ lxb_url_map, LXB_URL_MAP_USERINFO,
++ false);
+ }
+
+ lxb_status_t
+@@ -4364,7 +4382,8 @@ lxb_url_api_password_set(lxb_url_t *url,
+
+ return lxb_url_percent_encode_after_utf_8(password, password + length,
+ &url->password, url->mraw,
+- LXB_URL_MAP_USERINFO, false);
++ lxb_url_map, LXB_URL_MAP_USERINFO,
++ false);
+ }
+
+ lxb_status_t
+diff --git a/source/lexbor/url/url.h b/source/lexbor/url/url.h
+index aa50485..d2c9308 100644
+--- a/source/lexbor/url/url.h
++++ b/source/lexbor/url/url.h
+@@ -81,6 +81,20 @@ typedef enum {
+ }
+ lxb_url_state_t;
+
++typedef enum {
++ LXB_URL_MAP_UNDEF = 0x00,
++ LXB_URL_MAP_C0 = 0x01,
++ LXB_URL_MAP_FRAGMENT = 0x02,
++ LXB_URL_MAP_QUERY = 0x04,
++ LXB_URL_MAP_SPECIAL_QUERY = 0x08,
++ LXB_URL_MAP_PATH = 0x10,
++ LXB_URL_MAP_USERINFO = 0x20,
++ LXB_URL_MAP_COMPONENT = 0x40,
++ LXB_URL_MAP_X_WWW_FORM = 0x80,
++ LXB_URL_MAP_ALL = 0xff
++}
++lxb_url_map_type_t;
++
+ /*
+ * New values can only be added downwards.
+ * Before LXB_URL_SCHEMEL_TYPE__LAST_ENTRY.
+@@ -347,6 +361,88 @@ LXB_API lxb_status_t
+ lxb_url_parse_host_ipv6(lxb_url_parser_t *parser, const lxb_char_t *data,
+ size_t length, uint16_t *ipv6);
+
++/*
++ * UTF-8 percent-encoder.
++ *
++ * Percent-encodes bytes from data according to url_map and appends the result
++ * to str. A byte is encoded as "%HH" when the result of
++ * (url_map[byte] & enmap) is non-zero. Uppercase hexadecimal digits are used.
++ * If space_as_plus is true, U+0020 SPACE is encoded as '+' before the map is
++ * checked.
++ *
++ * The input is expected to be valid UTF-8; the function does not validate it.
++ *
++ * @param[in] Pointer to UTF-8 data. Not NULL.
++ * @param[in] Length of data. Can be 0.
++ * @param[in, out] Output string. Can be uninitialized (data = NULL). Encoded
++ * data is appended to any existing content. Not NULL.
++ * @param[in] Memory object used to allocate or resize the output string. Not
++ * NULL.
++ * @param[in] Table of 256 entries indexed by input byte, each entry is a bit
++ * mask of lxb_url_map_type_t values. Not NULL.
++ * @param[in] Mask selecting the bytes to percent-encode.
++ * @param[in] Replace U+0020 SPACE with '+' if true.
++ *
++ * @return LXB_STATUS_OK if successful, otherwise an error status value.
++ */
++LXB_API lxb_status_t
++lxb_url_percent_encode_utf_8(const lxb_char_t *data, size_t length,
++ lexbor_str_t *str, lexbor_mraw_t *mraw,
++ const uint8_t *url_map, lxb_url_map_type_t enmap,
++ bool space_as_plus);
++
++/*
++ * Percent-encode after encoding.
++ *
++ * Converts valid UTF-8 data to the specified encoding and appends the
++ * percent-encoded result to str. Each encoded byte for which
++ * (url_map[byte] & enmap) is non-zero is written as "%HH" using uppercase
++ * hexadecimal digits. If a code point cannot be represented in the target
++ * encoding, its percent-encoded numeric character reference is appended.
++ *
++ * If encoding is UTF-8, no conversion is performed. If space_as_plus is true,
++ * an encoded U+0020 SPACE is replaced with '+' before the map is checked.
++ * The input is expected to be valid UTF-8; the function does not validate it.
++ *
++ * @param[in] Pointer to UTF-8 data. Not NULL.
++ * @param[in] Length of data. Can be 0.
++ * @param[in, out] Output string. Can be uninitialized (data = NULL). Encoded
++ * data is appended to any existing content. Not NULL.
++ * @param[in] Memory object used to allocate or resize the output string. Not
++ * NULL.
++ * @param[in] Table of 256 entries indexed by encoded byte, each entry is a bit
++ * mask of lxb_url_map_type_t values. Not NULL.
++ * @param[in] Target encoding. Not NULL.
++ * @param[in] Mask selecting the bytes to percent-encode.
++ * @param[in] Replace an encoded U+0020 SPACE with '+' if true.
++ *
++ * @return LXB_STATUS_OK if successful, otherwise an error status value.
++ */
++LXB_API lxb_status_t
++lxb_url_percent_encode_encoding(const lxb_char_t *data, size_t length,
++ lexbor_str_t *str, lexbor_mraw_t *mraw,
++ const uint8_t *url_map,
++ const lxb_encoding_data_t *encoding,
++ lxb_url_map_type_t enmap,
++ bool space_as_plus);
++
++/*
++ * Get the URL percent-encoding map.
++ *
++ * Returns the built-in lookup table for the percent-encode sets defined by the
++ * URL specification. The table contains 256 entries indexed by byte value.
++ * Each entry is a bit mask of the lxb_url_map_type_t sets in which the byte
++ * must be percent-encoded.
++ *
++ * The returned map can be passed to lxb_url_percent_encode_utf_8() or
++ * lxb_url_percent_encode_encoding(). It has static storage duration and must
++ * not be modified or freed.
++ *
++ * @return Pointer to a read-only table of 256 entries. Never NULL.
++ */
++LXB_API const uint8_t *
++lxb_url_get_percent_encoding_map(void);
++
+ /*
+ * Erase URL.
+ *
+diff --git a/test/lexbor/url/percent_encode.c b/test/lexbor/url/percent_encode.c
+new file mode 100644
+index 0000000..361e22e
+--- /dev/null
++++ b/test/lexbor/url/percent_encode.c
+@@ -0,0 +1,228 @@
++/*
++ * Copyright (C) 2026 Alexander Borisov
++ *
++ * Author: Alexander Borisov <borisov@lexbor.com>
++ */
++
++#include <unit/test.h>
++#include <lexbor/url/url.h>
++
++
++typedef struct {
++ const lexbor_str_t input;
++ const lexbor_str_t output;
++ lxb_encoding_t encoding;
++ lxb_url_map_type_t enmap;
++ bool space_as_plus;
++ const uint8_t *url_map;
++ const lexbor_str_t initial;
++}
++percent_encode_entry_t;
++
++
++static const uint8_t custom_url_map[256] = {
++ ['a'] = LXB_URL_MAP_QUERY,
++ ['b'] = LXB_URL_MAP_PATH
++};
++
++static const percent_encode_entry_t entries[] = {
++ {
++ lexbor_str(""),
++ lexbor_str(""),
++ LXB_ENCODING_UTF_8,
++ LXB_URL_MAP_COMPONENT,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("AZaz09-._~!*'()"),
++ lexbor_str("AZaz09-._~!*'()"),
++ LXB_ENCODING_UTF_8,
++ LXB_URL_MAP_COMPONENT,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("\x00" "A /?\xC3\xA9"),
++ lexbor_str("%00A%20%2F%3F%C3%A9"),
++ LXB_ENCODING_UTF_8,
++ LXB_URL_MAP_COMPONENT,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("A b"),
++ lexbor_str("prefix:A+b"),
++ LXB_ENCODING_UTF_8,
++ LXB_URL_MAP_COMPONENT,
++ true,
++ NULL,
++ lexbor_str("prefix:")
++ },
++ {
++ lexbor_str("\xE2\x89\xA1\xE2\x80\xBD"),
++ lexbor_str("%E2%89%A1%E2%80%BD"),
++ LXB_ENCODING_UTF_8,
++ LXB_URL_MAP_SPECIAL_QUERY,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("\xE2\x89\xA1"),
++ lexbor_str("%81%DF"),
++ LXB_ENCODING_SHIFT_JIS,
++ LXB_URL_MAP_SPECIAL_QUERY,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("\xE2\x80\xBD"),
++ lexbor_str("%26%238253%3B"),
++ LXB_ENCODING_SHIFT_JIS,
++ LXB_URL_MAP_SPECIAL_QUERY,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("1+1 \xE2\x89\xA1 2%20\xE2\x80\xBD"),
++ lexbor_str("1+1%20%81%DF%202%20%26%238253%3B"),
++ LXB_ENCODING_SHIFT_JIS,
++ LXB_URL_MAP_SPECIAL_QUERY,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("\xC2\xA5"),
++ lexbor_str("%1B(J\\%1B(B"),
++ LXB_ENCODING_ISO_2022_JP,
++ LXB_URL_MAP_SPECIAL_QUERY,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("caf\xC3\xA9"),
++ lexbor_str("caf%E9"),
++ LXB_ENCODING_WINDOWS_1252,
++ LXB_URL_MAP_COMPONENT,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("\xD0\xAF"),
++ lexbor_str("%DF"),
++ LXB_ENCODING_WINDOWS_1251,
++ LXB_URL_MAP_COMPONENT,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("\xE4\xB8\xAD\xE6\x96\x87"),
++ lexbor_str("%A4%A4%A4%E5"),
++ LXB_ENCODING_BIG5,
++ LXB_URL_MAP_COMPONENT,
++ false,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("a b+c~"),
++ lexbor_str("a+b%2Bc%7E"),
++ LXB_ENCODING_UTF_8,
++ LXB_URL_MAP_X_WWW_FORM,
++ true,
++ NULL,
++ lexbor_str("")
++ },
++ {
++ lexbor_str("abc"),
++ lexbor_str("%61bc"),
++ LXB_ENCODING_UTF_8,
++ LXB_URL_MAP_QUERY,
++ false,
++ custom_url_map,
++ lexbor_str("")
++ }
++};
++
++
++TEST_BEGIN(percent_encode)
++{
++ size_t length;
++ lxb_char_t *data;
++ lxb_status_t status;
++ lexbor_mraw_t mraw;
++ lexbor_str_t str;
++ const uint8_t *url_map, *default_url_map;
++ const lxb_encoding_data_t *encoding;
++ const percent_encode_entry_t *entry;
++
++ status = lexbor_mraw_init(&mraw, 1024);
++ test_eq(status, LXB_STATUS_OK);
++
++ default_url_map = lxb_url_get_percent_encoding_map();
++ test_ne(default_url_map, NULL);
++
++ length = sizeof(entries) / sizeof(percent_encode_entry_t);
++
++ for (size_t i = 0; i < length; i++) {
++ entry = &entries[i];
++ encoding = lxb_encoding_data(entry->encoding);
++ test_ne(encoding, NULL);
++
++ str = (lexbor_str_t) {0};
++
++ if (entry->initial.length != 0) {
++ data = lexbor_str_init_append(&str, &mraw, entry->initial.data,
++ entry->initial.length);
++ test_ne(data, NULL);
++ }
++
++ url_map = entry->url_map;
++ if (url_map == NULL) {
++ url_map = default_url_map;
++ }
++
++ status = lxb_url_percent_encode_encoding(entry->input.data,
++ entry->input.length,
++ &str, &mraw, url_map, encoding,
++ entry->enmap,
++ entry->space_as_plus);
++ test_eq(status, LXB_STATUS_OK);
++
++ if (str.length != entry->output.length
++ || memcmp(str.data, entry->output.data, str.length) != 0)
++ {
++ TEST_PRINTLN("Percent-encode entry %zu (%s)", i + 1,
++ encoding->name);
++ }
++
++ test_eq_str_n(str.data, str.length, entry->output.data,
++ entry->output.length);
++
++ lexbor_str_destroy(&str, &mraw, false);
++ }
++
++ lexbor_mraw_destroy(&mraw, false);
++}
++TEST_END
++
++int
++main(int argc, const char *argv[])
++{
++ TEST_INIT();
++
++ TEST_ADD(percent_encode);
++
++ TEST_RUN("lexbor/url/percent_encode");
++ TEST_RELEASE();
++}