 
            For React Native.
 
            From: Zhiyi Zhang zzhang@codeweavers.com
All URI properties other than the domain are supported. Domain needs to be retrieved from the host name and processed according to the public suffix list[1].
The parser is roughly based on the GLib URI parser.
[1]: https://publicsuffix.org/list/ --- dlls/iertutil/Makefile.in | 2 +- dlls/iertutil/main.c | 804 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 767 insertions(+), 39 deletions(-)
diff --git a/dlls/iertutil/Makefile.in b/dlls/iertutil/Makefile.in index ba633d5ee72..d7c90ebd503 100644 --- a/dlls/iertutil/Makefile.in +++ b/dlls/iertutil/Makefile.in @@ -1,5 +1,5 @@ MODULE = iertutil.dll -IMPORTS = combase +IMPORTS = combase kernelbase
SOURCES = \ classes.idl \ diff --git a/dlls/iertutil/main.c b/dlls/iertutil/main.c index 70f8d4dcb99..c98031f4e4d 100644 --- a/dlls/iertutil/main.c +++ b/dlls/iertutil/main.c @@ -1,5 +1,5 @@ /* - * Copyright 2024 Zhiyi Zhang for CodeWeavers + * Copyright 2024-2025 Zhiyi Zhang for CodeWeavers * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -17,16 +17,695 @@ */
#include "private.h" +#include <winternl.h> +#include <inaddr.h> +#include <in6addr.h> +#include <ip2string.h> +#include <wchar.h>
WINE_DEFAULT_DEBUG_CHANNEL(iertutil);
+#define RESERVED_SUB_DELIMITER_CHARS L"!$&'()*+,;=" +#define ALLOWED_USER_RESERVED_CHARS RESERVED_SUB_DELIMITER_CHARS L":" +#define ALLOWED_PASSWORD_RESERVED_CHARS RESERVED_SUB_DELIMITER_CHARS L":" +#define ALLOWED_HOST_RESERVED_CHARS RESERVED_SUB_DELIMITER_CHARS +#define ALLOWED_PATH_RESERVED_CHARS RESERVED_SUB_DELIMITER_CHARS L"/:@" + struct uri { IUriRuntimeClass IUriRuntimeClass_iface; + HSTRING absolute_uri; + HSTRING display_uri; + HSTRING extension; + HSTRING fragment; + HSTRING host; + HSTRING password; + HSTRING path; + HSTRING query; + HSTRING scheme; HSTRING raw_uri; + HSTRING user; + INT32 port; + BOOL is_host_ipv6; + BOOL has_authority; + BOOL has_user_semicolon; LONG ref; };
+static const struct known_scheme +{ + const WCHAR *scheme; + int default_port; +} +known_schemes[] = +{ + {L"file", 0}, + {L"ftp", 21}, + {L"telnet", 23}, + {L"http", 80}, + {L"https", 443}, +}; + +static WCHAR *strdupwn(const WCHAR *str, DWORD length) +{ + WCHAR *ret; + + if (!str) + return NULL; + + if ((ret = malloc((length + 1) * sizeof(WCHAR)))) + { + memcpy(ret, str, length * sizeof(WCHAR)); + ret[length] = '\0'; + } + return ret; +} + +static BOOL is_char_valid(WCHAR ch, const WCHAR *allowed_reserved_chars) +{ + if (iswalnum(ch) || ch == '-' || ch == '.' || ch == '_' || ch == '~') + return TRUE; + if (allowed_reserved_chars && wcschr(allowed_reserved_chars, ch)) + return TRUE; + return FALSE; +} + +static BOOL is_known_scheme(HSTRING scheme) +{ + const WCHAR *buffer; + unsigned int i; + + buffer = WindowsGetStringRawBuffer(scheme, NULL); + for (i = 0; i < ARRAY_SIZE(known_schemes); i++) + { + if (!wcsicmp(buffer, known_schemes[i].scheme)) + return TRUE; + } + return FALSE; +} + +static INT32 get_known_scheme_default_port(HSTRING scheme) +{ + const WCHAR *buffer; + unsigned int i; + + buffer = WindowsGetStringRawBuffer(scheme, NULL); + for (i = 0; i < ARRAY_SIZE(known_schemes); i++) + { + if (!wcsicmp(buffer, known_schemes[i].scheme)) + return known_schemes[i].default_port; + } + return 0; +} + +static HRESULT percent_encode(HSTRING scheme, const WCHAR *start, unsigned length, + const WCHAR *allowed_reserved_chars, WCHAR **result) +{ + static const WCHAR *hex = L"0123456789ABCDEF"; + const WCHAR *p = start, *end = start + length; + unsigned int buffer_length = 0; + WCHAR *buffer; + + /* If not known scheme, don't percent encode. Simply make a NUL-terminated string */ + if (!is_known_scheme(scheme)) + { + buffer = strdupwn(start, length); + if (!buffer) + return E_OUTOFMEMORY; + + *result = buffer; + return S_OK; + } + + /* Percent encode */ + buffer = malloc((length * 3 + 1) * sizeof(WCHAR)); + if (!buffer) + return E_OUTOFMEMORY; + + while (p < end) + { + /* Unicode characters or allowed characters */ + if (*p >= 0x80 || is_char_valid(*p, allowed_reserved_chars)) + { + buffer[buffer_length++] = *p; + p++; + } + /* Already percent encoded. Copy them over */ + else if (*p == '%') + { + if (p + 2 >= end || !iswxdigit(p[1]) || !iswxdigit(p[2])) + { + free(buffer); + return E_INVALIDARG; + } + + memcpy(&buffer[buffer_length], p, 3 * sizeof(WCHAR)); + buffer_length += 3; + p += 3; + } + /* Percent encode */ + else + { + buffer[buffer_length++] = '%'; + buffer[buffer_length++] = hex[*p >> 4]; + buffer[buffer_length++] = hex[*p & 0xf]; + p++; + } + } + + buffer[buffer_length] = '\0'; + *result = buffer; + return S_OK; +} + +static HRESULT parse_user_info(struct uri *uri, const WCHAR *start, unsigned int length) +{ + const WCHAR *user_end = NULL, *password_end = NULL; + WCHAR *user, *password; + HRESULT hr; + + password_end = start + length; + user_end = wmemchr(start, ':', password_end - start); + if (!user_end) + user_end = password_end; + + if (FAILED(hr = percent_encode(uri->scheme, start, user_end - start, + ALLOWED_USER_RESERVED_CHARS, &user))) + return hr; + WindowsCreateString(user, wcslen(user), &uri->user); + free(user); + + if (*user_end == ':') + { + uri->has_user_semicolon = TRUE; + start = user_end + 1; + + if (FAILED(hr = percent_encode(uri->scheme, start, password_end - start, + ALLOWED_PASSWORD_RESERVED_CHARS, &password))) + return hr; + WindowsCreateString(password, wcslen(password), &uri->password); + free(password); + } + return S_OK; +} + +static HRESULT parse_ipv6_host(struct uri *uri, const WCHAR *start, unsigned int length) +{ + HRESULT hr = E_INVALIDARG; + const WCHAR *terminator; + WCHAR *addr = NULL; + IN6_ADDR in6_addr; + + if (start[length - 1] != ']') + goto failed; + + /* Zone ID is not allowed */ + if (wcschr(start, '%')) + goto failed; + + /* Strip [] */ + addr = strdupwn(start + 1, length - 2); + if (!addr) + return E_OUTOFMEMORY; + + if (RtlIpv6StringToAddressW(addr, &terminator, &in6_addr)) + goto failed; + + uri->is_host_ipv6 = TRUE; + WindowsCreateString(addr, wcslen(addr), &uri->host); + hr = S_OK; + +failed: + free(addr); + return hr; +} + +static HRESULT parse_ipv4_host(struct uri *uri, const WCHAR *start, unsigned int length) +{ + WCHAR *addr = NULL, ipv4_string[16]; + const WCHAR *terminator; + IN_ADDR in_addr; + + addr = strdupwn(start, length); + if (!addr) + return E_OUTOFMEMORY; + + if (!RtlIpv4StringToAddressW(addr, FALSE, &terminator, &in_addr)) + { + free(addr); + RtlIpv4AddressToStringW(&in_addr, ipv4_string); + WindowsCreateString(ipv4_string, wcslen(ipv4_string), &uri->host); + return S_OK; + } + + free(addr); + return E_INVALIDARG; +} + +static HRESULT parse_host(struct uri *uri, const WCHAR *start, unsigned int length) +{ + WCHAR *host = NULL, *unicode_host = NULL; + int unicode_host_length, ret; + HRESULT hr; + + if (*start == '[') + { + if (FAILED(hr = parse_ipv6_host(uri, start, length))) + { + WARN("Failed to parse ipv6 host %s.\n", wine_dbgstr_wn(start, length)); + return hr; + } + return S_OK; + } + + if (iswdigit(*start)) + { + if (FAILED(hr = parse_ipv4_host(uri, start, length))) + { + WARN("Failed to parse ipv4 host %s.\n", wine_dbgstr_wn(start, length)); + return hr; + } + return S_OK; + } + + if (FAILED(hr = percent_encode(uri->scheme, start, length, ALLOWED_HOST_RESERVED_CHARS, &host))) + { + WARN("Failed to encode host %s.\n", wine_dbgstr_wn(start, length)); + return E_FAIL; + } + + CharLowerBuffW(host, wcslen(host)); + + unicode_host_length = IdnToUnicode(0, host, wcslen(host), NULL, 0); + unicode_host = malloc(unicode_host_length * sizeof(WCHAR)); + ret = IdnToUnicode(0, host, wcslen(host), unicode_host, unicode_host_length); + if (!ret) + { + WARN("Failed to convert host %s from IDN to Unicode.\n", wine_dbgstr_w(host)); + free(unicode_host); + free(host); + return E_FAIL; + } + + WindowsCreateString(unicode_host, unicode_host_length, &uri->host); + free(unicode_host); + free(host); + return S_OK; +} + +static HRESULT parse_port(struct uri *uri, const WCHAR *start, unsigned int length) +{ + WCHAR *end; + int port; + + /* wcstol() allows + and - */ + if (!iswdigit(*start)) + { + WARN("Could not parse port %s.\n", wine_dbgstr_wn(start, length)); + return E_INVALIDARG; + } + + port = wcstol(start, &end, 10); + if (end != start + length) + { + WARN("Could not parse port %s.\n", wine_dbgstr_wn(start, length)); + return E_INVALIDARG; + } + else if (port > 65535) + { + WARN("Port %s is out of range.\n", wine_dbgstr_wn(start, length)); + return E_INVALIDARG; + } + + uri->port = port; + return S_OK; +} + +static void parse_extension(struct uri *uri, const WCHAR *start, unsigned int length) +{ + const WCHAR *last_point = NULL, *p = start; + + while (*p) + { + if (*p == '\' || *p == ' ') + last_point = NULL; + else if (*p == '.') + last_point = p; + p++; + } + + if (last_point) + WindowsCreateString(last_point, length - (last_point - start), &uri->extension); +} + +static void remove_dot_segments(WCHAR *path) +{ + WCHAR *src = path, *dst = path; + + if (!*path) + return; + + while (*src) + { + if (!wcsncmp(src, L"../", 3)) + { + src += 3; + } + else if (!wcsncmp(src, L"./", 2)) + { + src += 2; + } + else if (!wcsncmp(src, L"/./", 3)) + { + src += 2; + } + else if (!wcscmp(src, L"/.")) + { + src[1] = '\0'; + } + else if (!wcsncmp(src, L"/../", 4)) + { + src += 3; + if (dst > path) + { + do + { + dst--; + } while (*dst != '/' && dst > path); + } + } + else if (!wcscmp(src, L"/..")) + { + src[1] = '\0'; + if (dst > path) + { + do + { + dst--; + } while (*dst != '/' && dst > path); + } + } + else if (!wcscmp(src, L"..") || !wcscmp(src, L".")) + { + src[0] = '\0'; + } + else + { + *dst++ = *src++; + while (*src && *src != '/') + *dst++ = *src++; + } + } + *dst = '\0'; +} + +static void normalize_backslashes(WCHAR *start, unsigned int length) +{ + unsigned int i = 0; + + for (i = 0; i < length; i++) + { + if (start[i] == '?' || start[i] == '#') + break; + + if (start[i] == '\') + start[i] = '/'; + } +} + +static void construct_uri_string(struct uri *uri, BOOL check_authority, BOOL include_user_info, HSTRING *result) +{ + unsigned int scheme_length, user_length, password_length, host_length, path_length, query_length, fragment_length, length = 0; + const WCHAR *scheme, *user, *password, *host, *path, *query, *fragment; + WCHAR *buffer, port[6]; + + /* Display URI doesn't check authority */ + if (check_authority && is_known_scheme(uri->scheme) && !uri->has_authority) + return; + + scheme = WindowsGetStringRawBuffer(uri->scheme, &scheme_length); + user = WindowsGetStringRawBuffer(uri->user, &user_length); + password = WindowsGetStringRawBuffer(uri->password, &password_length); + host = WindowsGetStringRawBuffer(uri->host, &host_length); + path = WindowsGetStringRawBuffer(uri->path, &path_length); + query = WindowsGetStringRawBuffer(uri->query, &query_length); + fragment = WindowsGetStringRawBuffer(uri->fragment, &fragment_length); + + length = scheme_length + 3 /* :// */ + user_length + 1 /* : */ + password_length + + 1 /* @ */ + host_length + 2 /* [] for IPv6 */ + 6 /* :port(max 65535) */ + + path_length + query_length + fragment_length; + buffer = malloc((length + 1) * sizeof(WCHAR)); + if (!buffer) + return; + + wcscpy(buffer, scheme); + + if (uri->has_authority) + wcscat(buffer, L"://"); + else + wcscat(buffer, L":"); + + if (include_user_info) + { + wcscat(buffer, user); + + if (uri->has_user_semicolon && (user_length || password_length)) + wcscat(buffer, L":"); + + wcscat(buffer, password); + + if (user_length || password_length) + wcscat(buffer, L"@"); + } + + if (uri->is_host_ipv6) + wcscat(buffer, L"["); + wcscat(buffer, host); + if (uri->is_host_ipv6) + wcscat(buffer, L"]"); + + if (uri->port && uri->port != get_known_scheme_default_port(uri->scheme)) + { + wcscat(buffer, L":"); + swprintf_s(port, ARRAY_SIZE(port) - 1, L"%d", uri->port); + wcscat(buffer, port); + } + + wcscat(buffer, path); + wcscat(buffer, query); + wcscat(buffer, fragment); + + WindowsCreateString(buffer, wcslen(buffer), result); + free(buffer); +} + +static WCHAR *get_raw_uri(const WCHAR *start, unsigned int length) +{ + WCHAR *buffer, *p; + unsigned int i; + + buffer = malloc((length + 1) * sizeof(WCHAR)); + if (!buffer) + return NULL; + + /* White spaces other than ' ' are not allowed in the raw URI */ + for (i = 0, p = buffer; i < length; i++) + { + if (start[i] == ' ' || !iswspace(start[i])) + { + *p = start[i]; + p++; + } + } + *p = '\0'; + return buffer; +} + +static WCHAR *get_full_uri(WCHAR *uri, unsigned int length) +{ + static const WCHAR file_scheme[] = L"file:///"; + WCHAR *buffer; + + /* add the implicit file scheme if it matches X: pattern*/ + if (length >= 2 && iswalpha(uri[0]) && uri[1] == ':') + { + buffer = malloc(sizeof(file_scheme) + length * sizeof(WCHAR)); + if (!buffer) + return NULL; + + wcscpy(buffer, file_scheme); + wcscat(buffer, uri); + return buffer; + } + else + { + return strdupwn(uri, length); + } +} + +static HRESULT parse_uri_string(HSTRING string, struct uri *uri) +{ + const WCHAR *end, *colon, *at, *path_start, *query, *bracket, *host_end, *p, *raw_buffer; + WCHAR *uri_string, *buffer, *path, *raw_uri; + UINT32 length; + HRESULT hr; + + raw_buffer = WindowsGetStringRawBuffer(string, &length); + raw_uri = get_raw_uri(raw_buffer, length); + if (!raw_uri) + return E_OUTOFMEMORY; + + uri_string = get_full_uri(raw_uri, wcslen(raw_uri)); + if (!uri_string) + return E_OUTOFMEMORY; + + p = uri_string; + length = wcslen(uri_string); + + /* Parse scheme */ + while (*p && (iswalpha(*p) || (iswdigit(*p) || *p == '.' || *p == '+' || *p == '-'))) + p++; + + if (p > uri_string && *p == ':') + { + length = p - uri_string; + buffer = strdupwn(uri_string, length); + if (!buffer) + { + hr = E_OUTOFMEMORY; + goto done; + } + + CharLowerBuffW(buffer, length); + WindowsCreateString(buffer, length, &uri->scheme); + free(buffer); + p++; + } + else + { + WARN("No scheme is found in %s.\n", wine_dbgstr_hstring(string)); + return E_INVALIDARG; + } + + if (is_known_scheme(uri->scheme)) + normalize_backslashes(uri_string, wcslen(uri_string)); + + /* Parse authority */ + if (!wcsncmp(p, L"//", 2)) + { + uri->has_authority = TRUE; + + p += 2; + path_start = p + wcscspn(p, L"/?#"); + at = wmemchr(p, L'@', path_start - p); + if (at) + { + if (FAILED(hr = parse_user_info(uri, p, at - p))) + { + WARN("Failed to parse userinfo %s.\n", wine_dbgstr_wn(p, at - p)); + goto done; + } + + p = at + 1; + } + + /* Parse host */ + if (*p == '[') + { + bracket = wmemchr(p, ']', path_start - p); + if (bracket && *(bracket + 1) == ':') + colon = bracket + 1; + else + colon = NULL; + } + else + { + colon = wmemchr(p, ':', path_start - p); + } + + host_end = colon ? colon : path_start; + if (*p && host_end > p && FAILED(hr = parse_host(uri, p, host_end - p))) + goto done; + + /* Parse port */ + if (colon && colon != path_start - 1) + { + p = colon + 1; + if (FAILED(hr = parse_port(uri, p, path_start - p))) + goto done; + } + + p = path_start; + } + + /* Parse fragment */ + end = p + wcscspn(p, L"#"); + if (*end == '#') + WindowsCreateString(end, wcslen(end), &uri->fragment); + + /* Parse query */ + query = wmemchr(p, '?', end - p); + if (query) + { + WindowsCreateString(query, end - query, &uri->query); + end = query; + } + + /* Parse path */ + if (FAILED(hr = percent_encode(uri->scheme, p, end - p, ALLOWED_PATH_RESERVED_CHARS, &path))) + { + WARN("Failed to percent encode path %s.\n", wine_dbgstr_w(query + 1)); + goto done; + } + + remove_dot_segments(path); + + if (uri->has_authority && path && path[0] == '\0') + { + free(path); + path = strdupwn(L"/", 1); + if (!path) + { + hr = E_OUTOFMEMORY; + goto done; + } + } + + WindowsCreateString(path, wcslen(path), &uri->path); + + /* Parse extension */ + parse_extension(uri, path, wcslen(path)); + free(path); + + /* Get the default port */ + if (!uri->port) + uri->port = get_known_scheme_default_port(uri->scheme); + + construct_uri_string(uri, TRUE, TRUE, &uri->absolute_uri); + construct_uri_string(uri, FALSE, !is_known_scheme(uri->scheme), &uri->display_uri); + WindowsCreateString(raw_uri, wcslen(raw_uri), &uri->raw_uri); + hr = S_OK; + +done: + if (FAILED(hr)) + { + WindowsDeleteString(uri->absolute_uri); + WindowsDeleteString(uri->display_uri); + WindowsDeleteString(uri->extension); + WindowsDeleteString(uri->fragment); + WindowsDeleteString(uri->host); + WindowsDeleteString(uri->password); + WindowsDeleteString(uri->path); + WindowsDeleteString(uri->query); + WindowsDeleteString(uri->scheme); + WindowsDeleteString(uri->raw_uri); + WindowsDeleteString(uri->user); + free(uri_string); + free(raw_uri); + } + return hr; +} + static inline struct uri *impl_from_IUriRuntimeClass(IUriRuntimeClass *iface) { return CONTAINING_RECORD(iface, struct uri, IUriRuntimeClass_iface); @@ -67,7 +746,17 @@ static ULONG STDMETHODCALLTYPE uri_Release(IUriRuntimeClass *iface)
if (!ref) { + WindowsDeleteString(impl->absolute_uri); + WindowsDeleteString(impl->display_uri); + WindowsDeleteString(impl->extension); + WindowsDeleteString(impl->fragment); + WindowsDeleteString(impl->host); + WindowsDeleteString(impl->password); + WindowsDeleteString(impl->path); + WindowsDeleteString(impl->query); + WindowsDeleteString(impl->scheme); WindowsDeleteString(impl->raw_uri); + WindowsDeleteString(impl->user); free(impl); }
@@ -96,58 +785,83 @@ static HRESULT STDMETHODCALLTYPE uri_GetTrustLevel(IUriRuntimeClass *iface, Trus
static HRESULT STDMETHODCALLTYPE uri_AbsoluteUri(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p semi-stub!\n", iface, value); + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value);
- /* TODO: Parse the raw URI and reconstruct it from parts according to RFC 3986 or RFC 3987 */ - return IUriRuntimeClass_get_RawUri(iface, value); + return WindowsDuplicateString(impl->absolute_uri, value); }
static HRESULT STDMETHODCALLTYPE uri_DisplayUri(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->display_uri, value); }
static HRESULT STDMETHODCALLTYPE uri_Domain(IUriRuntimeClass *iface, HSTRING *value) { FIXME("iface %p, value %p stub!\n", iface, value); + + /* TODO: Needs to use things like psl_registrable_domain() in libpsl to get the domain from the host name */ + return E_NOTIMPL; }
static HRESULT STDMETHODCALLTYPE uri_Extension(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->extension, value); }
static HRESULT STDMETHODCALLTYPE uri_Fragment(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->fragment, value); }
static HRESULT STDMETHODCALLTYPE uri_Host(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->host, value); }
static HRESULT STDMETHODCALLTYPE uri_Password(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->password, value); }
static HRESULT STDMETHODCALLTYPE uri_Path(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->path, value); }
static HRESULT STDMETHODCALLTYPE uri_Query(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->query, value); }
static HRESULT STDMETHODCALLTYPE uri_QueryParsed(IUriRuntimeClass *iface, @@ -168,20 +882,33 @@ static HRESULT STDMETHODCALLTYPE uri_RawUri(IUriRuntimeClass *iface, HSTRING *va
static HRESULT STDMETHODCALLTYPE uri_SchemeName(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->scheme, value); }
static HRESULT STDMETHODCALLTYPE uri_UserName(IUriRuntimeClass *iface, HSTRING *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + return WindowsDuplicateString(impl->user, value); }
static HRESULT STDMETHODCALLTYPE uri_Port(IUriRuntimeClass *iface, INT32 *value) { - FIXME("iface %p, value %p stub!\n", iface, value); - return E_NOTIMPL; + struct uri *impl = impl_from_IUriRuntimeClass(iface); + + TRACE("iface %p, value %p.\n", iface, value); + + if (!impl->port) + return S_FALSE; + + *value = impl->port; + return S_OK; }
static HRESULT STDMETHODCALLTYPE uri_Suspicious(IUriRuntimeClass *iface, boolean *value) @@ -332,30 +1059,31 @@ static const struct IActivationFactoryVtbl activation_factory_vtbl =
DEFINE_IINSPECTABLE(uri_factory, IUriRuntimeClassFactory, struct iertutil, IActivationFactory_iface)
-static HRESULT STDMETHODCALLTYPE uri_factory_CreateUri(IUriRuntimeClassFactory *iface, HSTRING uri, +static HRESULT STDMETHODCALLTYPE uri_factory_CreateUri(IUriRuntimeClassFactory *iface, HSTRING string, IUriRuntimeClass **instance) { - const WCHAR *raw_buffer; - struct uri *uri_impl; + struct uri *uri; + HRESULT hr;
- FIXME("iface %p, uri %s, instance %p semi-stub!\n", iface, debugstr_hstring(uri), instance); + TRACE("iface %p, uri %s, instance %p.\n", iface, debugstr_hstring(string), instance);
- if (!uri) + if (!string) return E_POINTER;
- uri_impl = calloc(1, sizeof(*uri_impl)); - if (!uri_impl) + uri = calloc(1, sizeof(*uri)); + if (!uri) return E_OUTOFMEMORY;
- uri_impl->IUriRuntimeClass_iface.lpVtbl = &uri_vtbl; - uri_impl->ref = 1; - - raw_buffer = WindowsGetStringRawBuffer(uri, NULL); - WindowsCreateString(raw_buffer, wcslen(raw_buffer), &uri_impl->raw_uri); + if (FAILED(hr = parse_uri_string(string, uri))) + { + free(uri); + return hr; + }
- /* TODO: Parse the URI according to RFC 3986 and RFC 3987 */ + uri->IUriRuntimeClass_iface.lpVtbl = &uri_vtbl; + uri->ref = 1;
- *instance = &uri_impl->IUriRuntimeClass_iface; + *instance = &uri->IUriRuntimeClass_iface; return S_OK; }
 
            From: Zhiyi Zhang zzhang@codeweavers.com
--- dlls/iertutil/tests/iertutil.c | 265 +++++++++++++++++++++++++++++---- 1 file changed, 233 insertions(+), 32 deletions(-)
diff --git a/dlls/iertutil/tests/iertutil.c b/dlls/iertutil/tests/iertutil.c index 0f77b8a7f28..c7e18f5af18 100644 --- a/dlls/iertutil/tests/iertutil.c +++ b/dlls/iertutil/tests/iertutil.c @@ -1,5 +1,5 @@ /* - * Copyright 2024 Zhiyi Zhang for CodeWeavers + * Copyright 2024-2025 Zhiyi Zhang for CodeWeavers * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -96,15 +96,160 @@ static void test_IUriRuntimeClassFactory(void)
static void test_IUriRuntimeClass(void) { + static const struct + { + const WCHAR *src; + const WCHAR *RawUri; + const WCHAR *AbsoluteUri; + const WCHAR *DisplayUri; + const WCHAR *Domain; + const WCHAR *Extension; + const WCHAR *Fragment; + const WCHAR *Host; + const WCHAR *Password; + const WCHAR *Path; + const WCHAR *Query; + const WCHAR *SchemeName; + const WCHAR *UserName; + INT32 Port; + BOOL invalid; + } + tests[] = + { + /* Invalid URIs */ + {L"://example.com", .invalid = TRUE}, + {L"//", .invalid = TRUE}, + {L"//a", .invalid = TRUE}, + {L"a", .invalid = TRUE}, + {L"///a", .invalid = TRUE}, + {L"http://www.host.com:port", .invalid = TRUE}, + {L"http://example.com:+80/", .invalid = TRUE}, + {L"http://example.com:-80/", .invalid = TRUE}, + {L"http_s://example.com", .invalid = TRUE}, + {L"/a/b/c/./../../g", .invalid = TRUE}, + {L"mid/content=5/../6", .invalid = TRUE}, + {L"#frag", .invalid = TRUE}, + {L"http://example.com/a%2z", .invalid = TRUE}, + {L"http://example.com/a%z", .invalid = TRUE}, + {L"http://u:p?a@", .invalid = TRUE}, + {L"http://%5B1%5D/", .invalid = TRUE}, + {L"http://%5Bfe80::1ff:fe23:4567:890a%eth0%5D/", .invalid = TRUE}, + {L"http://%5Bfe80::1ff:fe23:4567:890a%25eth0%5D/", .invalid = TRUE}, + /* Valid URIs */ + /* URI syntax tests */ + {L"http:", L"http:", NULL, L"http:", NULL, NULL, NULL, NULL, NULL, NULL, NULL, L"http", NULL, 80}, + {L"http:/", L"http:/", NULL, L"http:/", NULL, NULL, NULL, NULL, NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://u:p@", L"http://u:p@", L"http://u:p@/", L"http:///", NULL, NULL, NULL, NULL, L"p", L"/", NULL, L"http", L"u", 80}, + {L"http://u:p;a@", L"http://u:p;a@", L"http://u:p;a@/", L"http:///", NULL, NULL, NULL, NULL, L"p;a", L"/", NULL, L"http", L"u", 80}, + {L"http://u;a:p@", L"http://u;a:p@", L"http://u;a:p@/", L"http:///", NULL, NULL, NULL, NULL, L"p", L"/", NULL, L"http", L"u;a", 80}, + {L"http://u:p:a@", L"http://u:p:a@", L"http://u:p:a@/", L"http:///", NULL, NULL, NULL, NULL, L"p:a", L"/", NULL, L"http", L"u", 80}, + {L"http://example.com", L"http://example.com", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://example.com/", L"http://example.com/", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://example.com:/", L"http://example.com:/", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://example.com:80/", L"http://example.com:80/", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"https://example.com:443/", L"https://example.com:443/", L"https://example.com/", L"https://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"https", NULL, 443}, + {L"http://example.com/data", L"http://example.com/data", L"http://example.com/data", L"http://example.com/data", L"example.com", NULL, NULL, L"example.com", NULL, L"/data", NULL, L"http", NULL, 80}, + {L"http://example.com/data/", L"http://example.com/data/", L"http://example.com/data/", L"http://example.com/data/", L"example.com", NULL, NULL, L"example.com", NULL, L"/data/", NULL, L"http", NULL, 80}, + {L"http://u:p@example.com/", L"http://u:p@example.com/", L"http://u:p@example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", L"p", L"/", NULL, L"http", L"u", 80}, + {L"http://u@example.com/", L"http://u@example.com/", L"http://u@example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", L"u", 80}, + {L"http://user:@example.com", L"http://user:@example.com", L"http://user:@example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", L"user", 80}, + {L"http://:pass@example.com", L"http://:pass@example.com", L"http://:pass@example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", L"pass", L"/", NULL, L"http", NULL, 80}, + {L"http://:@example.com", L"http://:@example.com", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"1http://example.com", L"1http://example.com", L"1http://example.com/", L"1http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"1http", NULL, -1}, + {L"ahttp://example.com", L"ahttp://example.com", L"ahttp://example.com/", L"ahttp://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"ahttp", NULL, -1}, + {L"http://example.com?q=a?b", L"http://example.com?q=a?b", L"http://example.com/?q=a?b", L"http://example.com/?q=a?b", L"example.com", NULL, NULL, L"example.com", NULL, L"/", L"?q=a?b", L"http", NULL, 80}, + {L"http://example.com?q=a b", L"http://example.com?q=a b", L"http://example.com/?q=a b", L"http://example.com/?q=a b", L"example.com", NULL, NULL, L"example.com", NULL, L"/", L"?q=a b", L"http", NULL, 80}, + {L"http://example.com#a b", L"http://example.com#a b", L"http://example.com/#a b", L"http://example.com/#a b", L"example.com", NULL, L"#a b", L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://a/b/c?d#e", L"http://a/b/c?d#e", L"http://a/b/c?d#e", L"http://a/b/c?d#e", NULL, NULL, L"#e", L"a", NULL, L"/b/c", L"?d", L"http", NULL, 80}, + {L"mailto:John.Doe@example.com", L"mailto:John.Doe@example.com", L"mailto:John.Doe@example.com", L"mailto:John.Doe@example.com", NULL, L".com", NULL, NULL, NULL, L"John.Doe@example.com", NULL, L"mailto", NULL, -1}, + {L"news:comp.infosystems.www.servers.unix", L"news:comp.infosystems.www.servers.unix", L"news:comp.infosystems.www.servers.unix", L"news:comp.infosystems.www.servers.unix", NULL, L".unix", NULL, NULL, NULL, L"comp.infosystems.www.servers.unix", NULL, L"news", NULL, -1}, + {L"tel:+1-816-555-1212", L"tel:+1-816-555-1212", L"tel:+1-816-555-1212", L"tel:+1-816-555-1212", NULL, NULL, NULL, NULL, NULL, L"+1-816-555-1212", NULL, L"tel", NULL, -1}, + {L"telnet://192.0.2.16:80/", L"telnet://192.0.2.16:80/", L"telnet://192.0.2.16:80/", L"telnet://192.0.2.16:80/", NULL, NULL, NULL, L"192.0.2.16", NULL, L"/", NULL, L"telnet", NULL, 80}, + {L"urn:oasis:names:specification:docbook:dtd:xml:4.1.2", L"urn:oasis:names:specification:docbook:dtd:xml:4.1.2", L"urn:oasis:names:specification:docbook:dtd:xml:4.1.2", L"urn:oasis:names:specification:docbook:dtd:xml:4.1.2", NULL, L".2", NULL, NULL, NULL, L"oasis:names:specification:docbook:dtd:xml:4.1.2", NULL, L"urn", NULL, -1}, + {L"urn:ietf:rfc:2648", L"urn:ietf:rfc:2648", L"urn:ietf:rfc:2648", L"urn:ietf:rfc:2648", NULL, NULL, NULL, NULL, NULL, L"ietf:rfc:2648", NULL, L"urn", NULL, -1}, + /* IPv4 hosts */ + {L"http://1/", L"http://1/", L"http://0.0.0.1/", L"http://0.0.0.1/", NULL, NULL, NULL, L"0.0.0.1", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://1.2/", L"http://1.2/", L"http://1.0.0.2/", L"http://1.0.0.2/", NULL, NULL, NULL, L"1.0.0.2", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://1.2.3/", L"http://1.2.3/", L"http://1.2.0.3/", L"http://1.2.0.3/", NULL, NULL, NULL, L"1.2.0.3", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://1.2.3.4/", L"http://1.2.3.4/", L"http://1.2.3.4/", L"http://1.2.3.4/", NULL, NULL, NULL, L"1.2.3.4", NULL, L"/", NULL, L"http", NULL, 80}, + /* IPv6 hosts */ + {L"http://%5B::1%5D/", L"http://%5B::1%5D/", L"http://%5B::1%5D/", L"http://%5B::1%5D/", NULL, NULL, NULL, L"::1", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://%5B::1%5D:8080/", L"http://%5B::1%5D:8080/", L"http://%5B::1%5D:8080/", L"http://%5B::1%5D:8080/", NULL, NULL, NULL, L"::1", NULL, L"/", NULL, L"http", NULL, 8080}, + {L"http://%5B::ffff:127.0.0.1%5D/", L"http://%5B::ffff:127.0.0.1%5D/", L"http://%5B::ffff:127.0.0.1%5D/", L"http://%5B::ffff:127.0.0.1%5D/", NULL, NULL, NULL, L"::ffff:127.0.0.1", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://%5B2001:db8::7%5D/", L"http://%5B2001:db8::7%5D/", L"http://%5B2001:db8::7%5D/", L"http://%5B2001:db8::7%5D/", NULL, NULL, NULL, L"2001:db8::7", NULL, L"/", NULL, L"http", NULL, 80}, + /* Domain. Domain is the registrable domain name, which is the effective top-level domain plus one */ + {L"http://www.example.com", L"http://www.example.com", L"http://www.example.com/", L"http://www.example.com/", L"example.com", NULL, NULL, L"www.example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://www.github.io", L"http://www.github.io", L"http://www.github.io/", L"http://www.github.io/", L"www.github.io", NULL, NULL, L"www.github.io", NULL, L"/", NULL, L"http", NULL, 80}, + /* Default ports */ + {L"ftp://", L"ftp://", L"ftp:///", L"ftp:///", NULL, NULL, NULL, NULL, NULL, L"/", NULL, L"ftp", NULL, 21}, + {L"telnet://", L"telnet://", L"telnet:///", L"telnet:///", NULL, NULL, NULL, NULL, NULL, L"/", NULL, L"telnet", NULL, 23}, + {L"http://", L"http://", L"http:///", L"http:///", NULL, NULL, NULL, NULL, NULL, L"/", NULL, L"http", NULL, 80}, + {L"https://", L"https://", L"https:///", L"https:///", NULL, NULL, NULL, NULL, NULL, L"/", NULL, L"https", NULL, 443}, + /* Extension */ + {L"http://example.com/.txt", L"http://example.com/.txt", L"http://example.com/.txt", L"http://example.com/.txt", L"example.com", L".txt", NULL, L"example.com", NULL, L"/.txt", NULL, L"http", NULL, 80}, + {L"http://example.com/1.txt", L"http://example.com/1.txt", L"http://example.com/1.txt", L"http://example.com/1.txt", L"example.com", L".txt", NULL, L"example.com", NULL, L"/1.txt", NULL, L"http", NULL, 80}, + {L"http://example.com/1..txt", L"http://example.com/1..txt", L"http://example.com/1..txt", L"http://example.com/1..txt", L"example.com", L".txt", NULL, L"example.com", NULL, L"/1..txt", NULL, L"http", NULL, 80}, + {L"http://example.com/1.2.txt", L"http://example.com/1.2.txt", L"http://example.com/1.2.txt", L"http://example.com/1.2.txt", L"example.com", L".txt", NULL, L"example.com", NULL, L"/1.2.txt", NULL, L"http", NULL, 80}, + /* Backslash normalization */ + {L"dummy://a\b//c\d?e\f#g\h", L"dummy://a\b//c\d?e\f#g\h", L"dummy://a\b//c\d?e\f#g\h", L"dummy://a\b//c\d?e\f#g\h", NULL, NULL, L"#g\h", L"a\b", NULL, L"//c\d", L"?e\f", L"dummy", NULL, -1}, + {L"http://a%5C%5Cb//c%5C%5Cd?e%5C%5Cf#g%5C%5Ch", L"http://a%5C%5Cb//c%5C%5Cd?e%5C%5Cf#g%5C%5Ch", L"http://a/b//c/d?e%5C%5Cf#g%5C%5Ch", L"http://a/b//c/d?e%5C%5Cf#g%5C%5Ch", NULL, NULL, L"#g\h", L"a", NULL, L"/b//c/d", L"?e\f", L"http", NULL, 80}, + {L"https://a%5C%5Cb//c%5C%5Cd?e%5C%5Cf#g%5C%5Ch", L"https://a%5C%5Cb//c%5C%5Cd?e%5C%5Cf#g%5C%5Ch", L"https://a/b//c/d?e%5C%5Cf#g%5C%5Ch", L"https://a/b//c/d?e%5C%5Cf#g%5C%5Ch", NULL, NULL, L"#g\h", L"a", NULL, L"/b//c/d", L"?e\f", L"https", NULL, 443}, + {L"ftp://a\b//c\d?e\f#g\h", L"ftp://a\b//c\d?e\f#g\h", L"ftp://a/b//c/d?e\f#g\h", L"ftp://a/b//c/d?e\f#g\h", NULL, NULL, L"#g\h", L"a", NULL, L"/b//c/d", L"?e\f", L"ftp", NULL, 21}, + {L"file://\sample\sample.bundle", L"file://\sample\sample.bundle", L"file:///sample/sample.bundle", L"file:///sample/sample.bundle", NULL, L".bundle", NULL, NULL, NULL, L"/sample/sample.bundle", NULL, L"file", NULL, -1}, + /* Dot segments removal */ + {L"http://example.com/.", L"http://example.com/.", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://example.com/..", L"http://example.com/..", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://example.com/...", L"http://example.com/...", L"http://example.com/...", L"http://example.com/...", L"example.com", L".", NULL, L"example.com", NULL, L"/...", NULL, L"http", NULL, 80}, + {L"http://example.com/a/.", L"http://example.com/a/.", L"http://example.com/a/", L"http://example.com/a/", L"example.com", NULL, NULL, L"example.com", NULL, L"/a/", NULL, L"http", NULL, 80}, + {L"http://example.com/a/..", L"http://example.com/a/..", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://example.com/a/b/../../c", L"http://example.com/a/b/../../c", L"http://example.com/c", L"http://example.com/c", L"example.com", NULL, NULL, L"example.com", NULL, L"/c", NULL, L"http", NULL, 80}, + {L"http://example.com/a/b/.././c", L"http://example.com/a/b/.././c", L"http://example.com/a/c", L"http://example.com/a/c", L"example.com", NULL, NULL, L"example.com", NULL, L"/a/c", NULL, L"http", NULL, 80}, + {L"http://example.com/a/b/..", L"http://example.com/a/b/..", L"http://example.com/a/", L"http://example.com/a/", L"example.com", NULL, NULL, L"example.com", NULL, L"/a/", NULL, L"http", NULL, 80}, + {L"http://example.com/a/b/.", L"http://example.com/a/b/.", L"http://example.com/a/b/", L"http://example.com/a/b/", L"example.com", NULL, NULL, L"example.com", NULL, L"/a/b/", NULL, L"http", NULL, 80}, + /* Implicit file scheme */ + {L"a:", L"a:", L"file:///a:", L"file:///a:", NULL, NULL, NULL, NULL, NULL, L"/a:", NULL, L"file", NULL, -1}, + {L"A:", L"A:", L"file:///A:", L"file:///A:", NULL, NULL, NULL, NULL, NULL, L"/A:", NULL, L"file", NULL, -1}, + {L"a:/", L"a:/", L"file:///a:/", L"file:///a:/", NULL, NULL, NULL, NULL, NULL, L"/a:/", NULL, L"file", NULL, -1}, + {L"z:/", L"z:/", L"file:///z:/", L"file:///z:/", NULL, NULL, NULL, NULL, NULL, L"/z:/", NULL, L"file", NULL, -1}, + {L"a://", L"a://", L"file:///a://", L"file:///a://", NULL, NULL, NULL, NULL, NULL, L"/a://", NULL, L"file", NULL, -1}, + {L"a:b/c", L"a:b/c", L"file:///a:b/c", L"file:///a:b/c", NULL, NULL, NULL, NULL, NULL, L"/a:b/c", NULL, L"file", NULL, -1}, + {L"ab:c/d", L"ab:c/d", L"ab:c/d", L"ab:c/d", NULL, NULL, NULL, NULL, NULL, L"c/d", NULL, L"ab", NULL, -1}, + /* White spaces */ + {L"http:// example.com/", L"http:// example.com/", L"http://%20example.com/", L"http://%20example.com/", L"%20example.com", NULL, NULL, L"%20example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://%5Ctexample.com/", L"http://example.com/", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://%5Crexample.com/", L"http://example.com/", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"http://%5Cnexample.com/", L"http://example.com/", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + /* Percent encoding */ + {L"http://example.com/a%20b", L"http://example.com/a%20b", L"http://example.com/a%20b", L"http://example.com/a%20b", L"example.com", NULL, NULL, L"example.com", NULL, L"/a%20b", NULL, L"http", NULL, 80}, + {L"http://example.com/a%2f/b", L"http://example.com/a%2f/b", L"http://example.com/a%2f/b", L"http://example.com/a%2f/b", L"example.com", NULL, NULL, L"example.com", NULL, L"/a%2f/b", NULL, L"http", NULL, 80}, + {L"http://example.com/a%2F/b", L"http://example.com/a%2F/b", L"http://example.com/a%2F/b", L"http://example.com/a%2F/b", L"example.com", NULL, NULL, L"example.com", NULL, L"/a%2F/b", NULL, L"http", NULL, 80}, + {L"http://%FF%FF%FF.com", L"http://%FF%FF%FF.com", L"http://%ff%ff%ff.com/", L"http://%ff%ff%ff.com/", L"%ff%ff%ff.com", NULL, NULL, L"%ff%ff%ff.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"dummy://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"dummy://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"dummy://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"dummy://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"exam ple.com", NULL, L"#frag ment", L"exam ple.com", L"pass word", L"/pa th", L"?qu ery=val ue", L"dummy", L"us er", -1}, + {L"http://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"http://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"http://us%20er:pass%20word@exam%20ple.com/pa%20th?qu ery=val ue#frag ment", L"http://exam%20ple.com/pa%20th?qu ery=val ue#frag ment", L"exam%20ple.com", NULL, L"#frag ment", L"exam%20ple.com", L"pass%20word", L"/pa%20th", L"?qu ery=val ue", L"http", L"us%20er", 80}, + {L"https://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"https://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"https://us%20er:pass%20word@exam%20ple.com/pa%20th?qu ery=val ue#frag ment", L"https://exam%20ple.com/pa%20th?qu ery=val ue#frag ment", L"exam%20ple.com", NULL, L"#frag ment", L"exam%20ple.com", L"pass%20word", L"/pa%20th", L"?qu ery=val ue", L"https", L"us%20er", 443}, + {L"ftp://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"ftp://us er:pass word@exam ple.com/pa th?qu ery=val ue#frag ment", L"ftp://us%20er:pass%20word@exam%20ple.com/pa%20th?qu ery=val ue#frag ment", L"ftp://exam%20ple.com/pa%20th?qu ery=val ue#frag ment", L"exam%20ple.com", NULL, L"#frag ment", L"exam%20ple.com", L"pass%20word", L"/pa%20th", L"?qu ery=val ue", L"ftp", L"us%20er", 21}, + {L"file://a b/c d", L"file://a b/c d", L"file://a%20b/c%20d", L"file://a%20b/c%20d", NULL, NULL, NULL, L"a%20b", NULL, L"/c%20d", NULL, L"file", NULL, -1}, + {L"http://example.com/path/to /resource", L"http://example.com/path/to /resource", L"http://example.com/path/to%20/resource", L"http://example.com/path/to%20/resource", L"example.com", NULL, NULL, L"example.com", NULL, L"/path/to%20/resource", NULL, L"http", NULL, 80}, + /* Punycode */ + {L"http://xn--0zwm56d.example.com/", L"http://xn--0zwm56d.example.com/", L"http://%5Cu6d4b%5Cu8bd5.example.com/", L"http://%5Cu6d4b%5Cu8bd5.example.com/", L"example.com", NULL, NULL, L"\u6d4b\u8bd5.example.com", NULL, L"/", NULL, L"http", NULL, 80}, + /* Unicode */ + {L"http://example.com/%5Cu00e4%5Cu00f6%5Cu00fc", L"http://example.com/%5Cu00e4%5Cu00f6%5Cu00fc", L"http://example.com/%5Cu00e4%5Cu00f6%5Cu00fc", L"http://example.com/%5Cu00e4%5Cu00f6%5Cu00fc", L"example.com", NULL, NULL, L"example.com", NULL, L"/\u00e4\u00f6\u00fc", NULL, L"http", NULL, 80}, + /* Miscellaneous tests */ + {L"HTTP://EXAMPLE.COM/", L"HTTP://EXAMPLE.COM/", L"http://example.com/", L"http://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"http", NULL, 80}, + {L"dummy:", L"dummy:", L"dummy:", L"dummy:", NULL, NULL, NULL, NULL, NULL, NULL, NULL, L"dummy", NULL, -1}, + {L"dummy://example.com", L"dummy://example.com", L"dummy://example.com/", L"dummy://example.com/", L"example.com", NULL, NULL, L"example.com", NULL, L"/", NULL, L"dummy", NULL, -1}, + }; static const WCHAR *class_name = L"Windows.Foundation.Uri"; IActivationFactory *activation_factory = NULL; IUriRuntimeClassFactory *uri_factory = NULL; IUriRuntimeClass *uri_class = NULL; IInspectable *inspectable = NULL; - IPropertyValue *value; + IPropertyValue *prop_value; + const WCHAR *buffer; + INT32 int32_value; HSTRING str, uri; + unsigned int i; HRESULT hr; - INT32 res;
hr = RoInitialize(RO_INIT_MULTITHREADED); ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); @@ -124,40 +269,96 @@ static void test_IUriRuntimeClass(void) hr = IActivationFactory_QueryInterface(activation_factory, &IID_IUriRuntimeClassFactory, (void **)&uri_factory); ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr);
- hr = WindowsCreateString(L"https://www.winehq.org/", wcslen(L"https://www.winehq.org/"), &uri); - ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); - hr = IUriRuntimeClassFactory_CreateUri(uri_factory, uri, &uri_class); - ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); + for (i = 0; i < ARRAY_SIZE(tests); i++) + { + winetest_push_context("%s", wine_dbgstr_w(tests[i].src));
- hr = IUriRuntimeClass_QueryInterface(uri_class, &IID_IInspectable, (void **)&inspectable); - ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); - ok(uri_class == (void *)inspectable, "QueryInterface IID_IInspectable returned %p, expected %p.\n", - inspectable, uri_factory); - IInspectable_Release(inspectable); + hr = WindowsCreateString(tests[i].src, wcslen(tests[i].src), &uri); + ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); + hr = IUriRuntimeClassFactory_CreateUri(uri_factory, uri, &uri_class); + if (tests[i].invalid) + { + ok(hr == E_INVALIDARG, "Got unexpected hr %#lx.\n", hr); + WindowsDeleteString(uri); + winetest_pop_context(); + continue; + } + else + { + ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); + }
- hr = IUriRuntimeClass_QueryInterface(uri_class, &IID_IPropertyValue, (void **)&value); - ok(hr == E_NOINTERFACE, "Got unexpected hr %#lx.\n", hr); + if (i == 0) + { + hr = IUriRuntimeClass_QueryInterface(uri_class, &IID_IInspectable, (void **)&inspectable); + ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); + ok(uri_class == (void *)inspectable, "QueryInterface IID_IInspectable returned %p, expected %p.\n", + inspectable, uri_factory); + IInspectable_Release(inspectable);
- /* Test IUriRuntimeClass_get_RawUri() */ - hr = IUriRuntimeClass_get_RawUri(uri_class, &str); - ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); - ok(str != uri, "Expected a different pointer.\n"); - hr = WindowsCompareStringOrdinal(uri, str, &res); - ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); - ok(res == 0, "Expected %s, got %s.\n", debugstr_hstring(uri), debugstr_hstring(str)); - WindowsDeleteString(str); + hr = IUriRuntimeClass_QueryInterface(uri_class, &IID_IPropertyValue, (void **)&prop_value); + ok(hr == E_NOINTERFACE, "Got unexpected hr %#lx.\n", hr); + }
- /* Test IUriRuntimeClass_get_AbsoluteUri() */ - hr = IUriRuntimeClass_get_AbsoluteUri(uri_class, &str); - ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); - ok(str != uri, "Expected a different pointer.\n"); - hr = WindowsCompareStringOrdinal(uri, str, &res); - ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); - ok(res == 0, "Expected %s, got %s.\n", debugstr_hstring(uri), debugstr_hstring(str)); - WindowsDeleteString(str); +#define TEST_HSTRING(prop) \ + winetest_push_context(#prop); \ + str = NULL; \ + hr = IUriRuntimeClass_get_##prop(uri_class, &str); \ + ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); \ + ok(str != uri, "Expected a different pointer.\n"); \ + if (tests[i].prop) \ + { \ + ok(!!str, "Expected a valid pointer.\n"); \ + buffer = WindowsGetStringRawBuffer(str, NULL); \ + ok(!wcscmp(buffer, tests[i].prop), "Expected %s, got %s.\n", wine_dbgstr_w(tests[i].prop), \ + debugstr_hstring(str)); \ + WindowsDeleteString(str); \ + } \ + else \ + { \ + ok(!str, "Expected a NULL pointer.\n"); \ + } \ + winetest_pop_context(); + +#define TEST_INT32(prop) \ + winetest_push_context(#prop); \ + hr = IUriRuntimeClass_get_##prop(uri_class, &int32_value); \ + if (tests[i].prop == -1) \ + { \ + ok(hr == S_FALSE, "Got unexpected hr %#lx.\n", hr); \ + } \ + else \ + { \ + ok(hr == S_OK, "Got unexpected hr %#lx.\n", hr); \ + ok(int32_value == tests[i].prop, "Expected " #prop " %d, got %d.\n", tests[i].prop, \ + int32_value); \ + } \ + winetest_pop_context(); + + TEST_HSTRING(AbsoluteUri) + TEST_HSTRING(DisplayUri) + + /* TODO: TEST_HSTRING(Domain) */ + + TEST_HSTRING(Extension) + TEST_HSTRING(Fragment) + TEST_HSTRING(Host) + TEST_HSTRING(Password) + TEST_HSTRING(Path) + TEST_HSTRING(Query) + TEST_HSTRING(RawUri) + TEST_HSTRING(SchemeName) + TEST_HSTRING(UserName) + TEST_INT32(Port) + +#undef TEST_HSTRING +#undef TEST_HSTRING + + IUriRuntimeClass_Release(uri_class); + WindowsDeleteString(uri); + winetest_pop_context(); + }
- WindowsDeleteString(uri); - IUriRuntimeClass_Release(uri_class); IUriRuntimeClassFactory_Release(uri_factory); IActivationFactory_Release(activation_factory); RoUninitialize();
 
            Is it possible to reuse CreateUri() for this?
 
            On Wed Oct 8 13:52:49 2025 +0000, Nikolay Sivov wrote:
Is it possible to reuse CreateUri() for this?
We could. But the behavior is slightly different. For example, uri_factory_CreateUri() has a similar behavior as CreateUri(..., Uri_CREATE_NO_DECODE_EXTRA_INFO | Uri_CREATE_ALLOW_IMPLICIT_FILE_SCHEME | Uri_CREATE_NO_CANONICALIZE, ...). The Uri_CREATE_NO_CANONICALIZE will cause the absolute URI of "http://example.com" to be "http://example.com" while IUriRuntimeClass adds an "/" at the end and becomes "http://example.com/". So if we want to reuse CreateUri(), we will need to reconstruct the absolute URI from each component. However, to reconstruct the absolute URI, there is a bit of information missing. For example, in "http://user:@example.com", we need to know if there is a ":" after "user". So we still need to parse the scheme and authority to know that. Thus, we could end up having some URI parsing logic in IUriRuntimeClass() with the main parsing happening in CreateUri(). I am afraid that there might be more inconsistencies, and there is no evidence of that IUriRuntimeClass actua lly uses IUri, so I decided to have a dedicated parser for IUriRuntimeClass.
 
            On Wed Oct 8 13:52:49 2025 +0000, Zhiyi Zhang wrote:
We could. But the behavior is slightly different. For example, uri_factory_CreateUri() has a similar behavior as CreateUri(..., Uri_CREATE_NO_DECODE_EXTRA_INFO | Uri_CREATE_ALLOW_IMPLICIT_FILE_SCHEME | Uri_CREATE_NO_CANONICALIZE, ...). The Uri_CREATE_NO_CANONICALIZE will cause the absolute URI of "http://example.com" to be "http://example.com" while IUriRuntimeClass adds an "/" at the end and becomes "http://example.com/". So if we want to reuse CreateUri(), we will need to reconstruct the absolute URI from each component. However, to reconstruct the absolute URI, there is a bit of information missing. For example, in "http://user:@example.com", we need to know if there is a ":" after "user". So we still need to parse the scheme and authority to know that. Thus, we could end up having some URI parsing logic in IUriRuntimeClass() with the main parsing happening in CreateUri(). I am afraid that there might be more inconsistencies, and there is no evidence of that IUriRuntimeClass actually uses IUri, so I decided to have a dedicated parser for IUriRuntimeClass.
Looking at ierturil exports, there are things like `CreateIUriBuilder`, so it seems likely that `IUri` implementation should be moved from urlmon to iertutil. We could then use the same parser with different frontends.
 
            On Wed Oct 8 14:49:58 2025 +0000, Jacek Caban wrote:
Looking at ierturil exports, there are things like `CreateIUriBuilder`, so it seems likely that `IUri` implementation should be moved from urlmon to iertutil. We could then use the same parser with different frontends.
I tried moving the IUri implementation to iertutil today. However, a lot of the functions in urlmon depend on each other so moving CreateUri() turned out to be more complicated than I thought. Windows seems to work around this by introducing various private or unnamed functions in iertutil, and then urlmon forwards or calls into them. I am not sure if we want to do that. It will be quite messy.
 
            On Thu Oct 9 13:32:46 2025 +0000, Zhiyi Zhang wrote:
I tried moving the IUri implementation to iertutil today. However, a lot of the functions in urlmon depend on each other so moving CreateUri() turned out to be more complicated than I thought. Windows seems to work around this by introducing various private or unnamed functions in iertutil, and then urlmon forwards or calls into them. I am not sure if we want to do that. It will be quite messy.
How bad is it? How many of these private exports would we need? The details of those private calls don't seem important, but if it’s only a few functions, maybe we should just invent our own. Given the size of the URI parser, it would be nice to avoid duplication.
 
            On Mon Oct 13 14:17:30 2025 +0000, Jacek Caban wrote:
How bad is it? How many of these private exports would we need? The details of those private calls don't seem important, but if it’s only a few functions, maybe we should just invent our own. Given the size of the URI parser, it would be nice to avoid duplication.
For example, after moving CreateUri(), CreateUriWithFragment(), and CreateIUriBuilder() to iertutil, we would need to move CoInternetParseIUri() and CoInternetCombineIUri() (to PrivateCoInternetParseIUri(), etc, in iertutil) because they also use the parser. To move CoInternetParseIUri(), we would need to move get_protocol_info() to iertutil as well. get_protocol_info() uses the name_space_list, which is initialized inside urlmon, and it's also used by find_name_space() -> get_protocol_handler() -> BindProtocol_StartEx(). So I am not sure how to move get_protocol_info() to iertutil in a clean way. Dynamically loading get_protocol_info() from urlmon in iertutil may work. But it seems too hacky.
 
            On Tue Oct 14 03:12:51 2025 +0000, Zhiyi Zhang wrote:
For example, after moving CreateUri(), CreateUriWithFragment(), and CreateIUriBuilder() to iertutil, we would need to move CoInternetParseIUri() and CoInternetCombineIUri() (to PrivateCoInternetParseIUri(), etc, in iertutil) because they also use the parser. To move CoInternetParseIUri(), we would need to move get_protocol_info() to iertutil as well. get_protocol_info() uses the name_space_list, which is initialized inside urlmon, and it's also used by find_name_space() -> get_protocol_handler() -> BindProtocol_StartEx(). So I am not sure how to move get_protocol_info() to iertutil in a clean way. Dynamically loading get_protocol_info() from urlmon in iertutil may work. But it seems too hacky.
Maybe we could partially move CoInternetCombineIUri(). And keep the following in urlmon. ``` info = get_protocol_info(base->canon_uri); if(info) { WCHAR result[INTERNET_MAX_URL_LENGTH+1]; DWORD result_len = 0;
hr = IInternetProtocolInfo_CombineUrl(info, base->canon_uri, relative->canon_uri, dwCombineFlags, result, INTERNET_MAX_URL_LENGTH+1, &result_len, 0); IInternetProtocolInfo_Release(info); if(SUCCEEDED(hr)) { hr = CreateUri(result, Uri_CREATE_ALLOW_RELATIVE, 0, ppCombinedUri); if(SUCCEEDED(hr)) return hr; } } ```
And then do something like ``` info = get_protocol_info(base->canon_uri); /* get the canon_uri via a function? */ if(info) { ... }
return PrivateCoInternetCombineIUri(); ```
Anyway, it still feels a bit weird to take such an approach.



