ddn.net.uri

ddn.net.uri — RFC 3986 (URI) implementation in D.

This module provides a complete, practical implementation of RFC 3986 (Uniform Resource Identifier: Generic Syntax). It includes:

  • Parsing of absolute URIs and relative references
  • Component access (scheme, authority → userinfo/host/port, path, query, fragment)
  • Percent-encoding/decoding utilities per component
  • Normalization (case folding, dot-segment removal, percent-encoding normalization,

default port elision)

  • Reference resolution (RFC 3986 §5.2)
  • Serialization to string
  • Convenience wrappers URL and URN (via composition over URI)
  • RFC 6570 URI Templates support (Levels 1–4)

RFC 6570 (URI Templates) ----------------------- This module includes an RFC 6570 URI Template processor (Levels 1–4) so users can keep a single import (ddn.net.uri) for RFC 3986 + RFC 6570.

Public entry points:

  • UriTemplate.parse(string) returning a parsed template object
  • UriTemplate.expand(UriTemplateVars) expanding with scalar/list/map values

Notes -----

  • Internationalized domain names (IDNA, RFC 5890+) are supported for the host

component via Punycode (RFC 3492) conversion. Use URI.normalizeIDNA() to get a normalized ASCII form of Unicode hostnames, and idnaToUnicode/idnaToASCII helpers for explicit conversion.

  • Scope & caveats: This module implements the core Punycode algorithm and basic IDNA

ToASCII/ToUnicode processing (label-wise, dot mapping, case folding). It does not implement the full IDNA2008 contextual rules or UTS #46 compatibility mappings.

  • IPv6 validation is sufficient for typical cases but this module does not attempt full

RFC 5952 canonicalization.

Performance -----------

  • URI template expansion uses Appender internally to avoid O(n²) string

concatenation in loops.

  • Query parameter building functions use Appender for efficient string assembly.

Examples --------

import ddn.net.uri;

auto u = URI.parse("http://user:pass@example.com:80/a/./b/../c?x=1#frag");
auto n = u.normalize(); // => http://example.com/a/c?x=1#frag

auto base = URI.parse("http://a/b/c/d;p?q");
auto r = base.resolve(URI.parse("g?y#s")); // => http://a/b/c/g?y#s

auto url = URL.parse("https://user@example.com:443/path");
assert(url.inner.normalize().toString() == "https://user@example.com/path");

auto urn = URN.parse("urn:example:animal:ferret:nose");
assert(urn.nss() == "example:animal:ferret:nose");

assert(pctDecode("%41%42%43") == "ABC");
assert(pctNormalize("%7e", Component.Path) == "~");
assert(pctEncode("~", Component.Path) == "~");

@safe: The implementation aims to be @safe where practical and uses nothrow/@nogc selectively for hot paths and predicates.

Types 21

classUriException : Exception

Base exception type for URI-related errors.

Constructors
this(string msg, string file = __FILE__, size_t line = __LINE__)Construct a `UriException`.

Exception type for URI parsing/validation errors.

Constructors
this(string msg, string file = __FILE__, size_t line = __LINE__)Construct a `UriParseException`.

URI component selector used to apply component-specific rules (e.g. percent-encoding).

Values correspond to RFC 3986 components.

SchemeThe `scheme` component
UserInfoThe `userinfo` subcomponent of authority
HostThe `host` subcomponent of authority (reg-name, IPv4, IPv6, IPvFuture)
PathThe `path` component
QueryThe `query` component (without leading '?')
FragmentThe `fragment` component (without leading '#')
structAuthority

Authority component of a URI per RFC 3986 (userinfo@host:port).

The host is stored without square brackets even for IP-literals; brackets are added when formatting with toString.

Fields
string userinfoOptional `userinfo` (e.g. `user` or `user:pass`). Empty if absent.
string hostHostname or IP-literal value. IPv6/IPvFuture are stored without brackets.
int portOptional port number; `-1` means absent.
Methods
bool hasUserInfo() const @nogc nothrow @safeTrue if `userinfo` is non-empty.
bool hasPort() const @nogc nothrow @safeTrue if `port` is present.
string toString() const @safeSerialize the authority as `userinfo@host:port`, adding brackets for IP-literals.
bool isIPv6Literal(const string h) @nogc nothrow @safeReturns true if `h` looks like an IPv6 literal.
bool isIPvFuture(const string h) @nogc nothrow @safeReturns true if `h` looks like an RFC 3986 IPvFuture literal (e.g. `v1.fe80::1`).
structURI

Generic Uniform Resource Identifier per RFC 3986.

Represents either an absolute URI (has scheme) or a relative reference. Use parse to construct from a string and toString to serialize.

Fields
string schemeScheme name (lower/upper case as parsed). Empty if relative reference.
bool hasAuthorityFlagTrue when the parsed reference contained an authority ("//...").
Authority authorityThe authority part; valid only when `hasAuthority` is true.
string pathPath component (may be empty). Left as-is; use `normalize` to remove dot-segments.
bool hasQueryFlagTrue when the parsed reference contained a query delimiter (`?`).
string queryQuery component without the leading '?'. Empty if absent.
bool hasFragmentFlagTrue when the parsed reference contained a fragment delimiter (`#`).
string fragmentFragment component without the leading '#'. Empty if absent.
Methods
URI parse(string s, UriOptions opt) @safeParse an absolute URI or relative reference from `s`.
URI parse(string s) @safeParse an absolute URI or relative reference from `s` with default options.
URI parseStrict(string s) @safeParse `s` as a URI applying additional strict validations.
bool isAbsolute() const @nogc nothrow @safeTrue if this reference is an absolute URI (has a non-empty `scheme`).
bool isRelativeReference() const @nogc nothrow @safeTrue if this reference is a relative reference (i.e. has no `scheme`).
bool hasQuery() const @nogc nothrow @safeTrue if the `query` component is present and non-empty.
bool hasQueryComponent() const @nogc nothrow @safeTrue if the `query` component is present (even if empty).
bool hasFragment() const @nogc nothrow @safeTrue if the `fragment` component is present and non-empty.
bool hasFragmentComponent() const @nogc nothrow @safeTrue if the `fragment` component is present (even if empty).
bool hasAuthority() const @nogc nothrow @safeTrue if an authority component is present (i.e. the reference used the `//` form).
string toString() const @safeSerialize this URI to a string per RFC 3986 generic syntax.
URI normalizeIDNA() const @safeNormalize the URI applying RFC 3986 normalization and IDNA ToASCII for hostnames.
URI normalizeIDNA(UriOptions opt) const @safeNormalize the URI applying RFC 3986 normalization and IDNA ToASCII for hostnames using options.
string hostUnicode() const @safeReturn the Unicode form of the host using IDNA ToUnicode (if applicable). If the host is an IP-literal, it is returned as-is.
URI normalize(UriOptions /*opt*/ ) const @safeReturn a new URI normalized per RFC 3986 Section 6 with options.
URI normalize() const @safeReturn a new URI normalized per RFC 3986 Section 6.
bool equalsNormalized(const URI other) const @safeCompare for equivalence per RFC 3986 by normalizing both URIs and comparing their serialized forms.
URI resolve(const URI reference) const @safeResolve `reference` against this URI used as the base, per RFC 3986 §5.2.
string mergePaths(const URI base, const string refPath) @safeMerge `base` path with a reference path per RFC 3986 §5.2.3.
structURL

Convenience wrapper for hierarchical absolute URIs that have an authority.

URL composes a URI and provides aliases and helpers commonly used for network locations (scheme, authority, path, query, fragment).

Fields
URI inner
Methods
URL parse(string s) @safeParse `s` as a `URL`. Enforces that the parsed `URI` is absolute and has an authority.
string scheme() @property const @safeProperty to the inner `URI.scheme`.
void scheme(string value) @property @safeProperty to the inner `URI.scheme`.
Authority authority() @property ref return @safeProperty to the inner `URI.authority`.
void authority(Authority value) @property @safeProperty to the inner `URI.authority`.
string path() @property const @safeProperty to the inner `URI.path`.
void path(string value) @property @safeProperty to the inner `URI.path`.
string query() @property const @safeProperty to the inner `URI.query`.
void query(string value) @property @safeProperty to the inner `URI.query`.
string fragment() @property const @safeProperty to the inner `URI.fragment`.
void fragment(string value) @property @safeProperty to the inner `URI.fragment`.
string toString() const @safeSerialize the URL to string.
string host() @property const @safeReturn the host from the authority.
void host(string value) @property @safeSet the host in the authority.
int port() @property const @safeReturn the port from the authority (`-1` if absent).
void port(int value) @property @safeSet the port in the authority (`-1` means absent).
string userinfo() @property const @safeReturn the userinfo from the authority (empty if absent).
void userinfo(string value) @property @safeSet the userinfo in the authority.
string hostUnicode() @property const @safeReturn the Unicode form of the host using IDNA ToUnicode.
structURN

Uniform Resource Name wrapper (scheme == "urn").

Provides convenient access to the Namespace Specific String (NSS) while delegating serialization and parsing to the underlying URI.

Fields
URI innerUnderlying `URI` instance; guaranteed to have scheme `"urn"`.
Methods
URN parse(string s) @safeParse `s` as a `URN`.
string nss() @property const @safeReturn the Namespace Specific String (NSS): the part after `"urn:"`. In generic URI syntax this corresponds to the path component.
void nss(string value) @property @safeSet the Namespace Specific String (NSS).
string toString() const @safeSerialize the URN to string.

Exception type for RFC 6570 URI Template parsing/expansion errors.

Constructors
this(string msg, string file = __FILE__, size_t line = __LINE__)Construct a `UriTemplateException`.

RFC 6570 expression operator.

Each operator defines how an expression is expanded (prefix, separator, whether variable names are emitted, and the encoding mode).

NONE = 0No operator (simple string expansion).
RESERVED = '+'Reserved expansion (`+`).
FRAGMENT = '#'Fragment expansion (`#`).
LABEL = '.'Label expansion (`.`).
PATH_SEGMENT = '/'Path segment expansion (`/`).
PATH_PARAMETER = ';'Path-style parameter expansion (`;`).
FORM_QUERY = '?'Form-style query expansion (`?`).
QUERY_CONTINUATION = '&'Query continuation expansion (`&`).

One variable specification inside an RFC 6570 expression.

Example varspecs:

  • var
  • var:3 (prefix modifier)
  • list* (explode modifier)
Fields
string nameVariable name (may be dotted, e.g. `user.name`).
Nullable!uint prefixLengthOptional prefix length modifier (`:n`), scalar-only.
bool explodeExplode modifier (`*`).

One RFC 6570 expression: `{` [operator] varspec *( "," varspec ) `}`.

Fields
UriTemplateOperator opOperator that controls rendering and encoding.
UriTemplateVarSpec[] varSpecsVariable specifications in order.

Kind of a template part.

LITERALLiteral text outside of expressions.
EXPRESSIONAn expression part (`{...}`).

One part of a template: either a literal substring or an expression.

Fields
UriTemplatePartKind kindWhether this part is a literal or expression.
string literalLiteral substring (valid when `kind == LITERAL`).
UriTemplateExpression expressionExpression value (valid when `kind == EXPRESSION`).
Methods
UriTemplatePart makeLiteral(string s) @safeConstruct a literal part.
UriTemplatePart makeExpression(UriTemplateExpression e) @safeConstruct an expression part.

RFC 6570 URI Template.

A template is a sequence of literal and expression parts.

Notes: Parsing and expansion are implemented incrementally in tasks T4+.

Fields
UriTemplatePart[] partsTemplate parts in order.
Methods
UriTemplate parse(string templateString) @safeParse an RFC 6570 URI Template string into an AST.
string expand(const UriTemplateVars vars) const @safeExpand this URI Template with the provided variables.
private structUriTemplateOpInfo

Operator properties needed for expansion.

Fields
string prefix
string separator
bool named
bool ifEmptyNameOnly
bool allowReserved

RFC 6570 variable value kind.

SCALARA single scalar string value.
LISTA list of string values.
MAPAn associative (map) value represented as ordered key/value pairs.

RFC 6570 variable value.

A value may be a scalar, list, or associative array (map). For maps, ordering is significant for deterministic expansions and tests.

Fields
UriTemplateValueKind kindWhich kind of value this instance holds.
string scalarScalar payload (valid when `kind == SCALAR`).
string[] listList payload (valid when `kind == LIST`).
Tuple!(string, string)[] mapMap payload as ordered pairs (valid when `kind == MAP`).
Methods
UriTemplateValue fromScalar(string s) @safeConstruct a scalar value.
UriTemplateValue fromList(string[] items) @safeConstruct a list value.
UriTemplateValue fromMap(Tuple!(string, string)[] pairs) @safeConstruct a map value from ordered key/value pairs.
UriTemplateValue fromAA(string[string] aa) @safeConstruct a map value from a D associative array.

Expansion scope for URI Templates.

Maps variable names to their values.

Fields
private UriTemplateValue[string] values
Methods
void set(string name, UriTemplateValue value) @safeSet variable `name` to `value`.
bool contains(string name) const @safeReturn whether `name` is present.
Nullable!(const(UriTemplateValue)) get(string name) const @safeGet variable `name`.
private structUriParser
Fields
string s
size_t i
bool strict
bool allowNonAsciiRegName
Methods
bool eof() const @nogc nothrow @safeReturns true if the parser cursor is at the end of input.
char peek() const @nogc nothrow @safeReturn the current character at the parser cursor.
URI parseURI() @safeParse the entire input string as a URI or relative reference.
ptrdiff_t indexOfFirst(char c) const @safeReturn the index of the first occurrence of `c` in the input, or `-1`.
ptrdiff_t indexOfAny(string chars) const @safeReturn the lowest index of any character from `chars` in the input, or `-1`.
bool match(string lit) @safeIf the remaining input starts with `lit`, advance and return true.
Authority parseAuthority() @safeParse an authority component (`userinfo@host:port`).
string parsePathAbempty() @safeParse a `path-abempty` as defined by RFC 3986.
string parsePathAbsolute() @safeParse a `path-absolute` as defined by RFC 3986.
string parsePathRootlessOrNoScheme(bool hasScheme) @safeParse a rootless path in the appropriate mode for the URI context.
string parseQuery() @safeParse the query component (without the leading `?`).
string parseFragment() @safeParse the fragment component (without the leading `#`).
int parsePort(string p) @safeParse a decimal port number.
bool isValidIPLiteral(string h) @safeValidate an IP-literal host string (without brackets).
bool isValidIPv6(string h) @safeValidate an IPv6 address literal.
bool isValidHextet(string p) @safeValidate a single IPv6 hextet (1..4 hex digits).
bool isValidRegNameOrIPv4(string h) @safeValidate a reg-name host or IPv4 address.
bool isValidIPv4(string h) @safeValidate an IPv4 address in dotted-decimal form.
Constructors
this(string s)Construct a parser for the given input string.

Options controlling IDNA processing.

Fields
bool enableMappingsApply basic UTS #46-like compatibility mappings (e.g., ß -> "ss", fullwidth ASCII → ASCII).
bool strictEnable stricter validation (reject labels starting/ending with '-' and prohibited code points).

Options controlling per-call parsing/normalization strictness.

  • strictParsing: When true, enables stricter RFC 3986 validation that

rejects URIs which would otherwise be accepted leniently (e.g., unencoded characters in certain positions).

  • idnaStrict: When true, applies stricter IDNA label validation during

IDNA conversion (rejects labels starting/ending with '-', prohibited code points, etc.).

  • allowNonAsciiHost: When true, allows non-ASCII bytes in reg-name

hosts during parsing (pre-IDNA conversion). Set to false to require Punycode-encoded hosts in input.

  • maxLength: Maximum allowed URI length in bytes. When non-zero, URIs

exceeding this length will be rejected during parsing. RFC 7230 recommends rejecting URIs longer than 8000 bytes.

Fields
bool strictParsingEnable stricter parsing validations akin to parseStrict.
bool idnaStrictApply stricter IDNA label validation during IDNA conversion (e.g., via `URI.normalizeIDNA(UriOptions)`).
bool allowNonAsciiHostAllow non-ASCII bytes in reg-name (Unicode host pre-IDNA) during parsing.
size_t maxLengthMaximum allowed URI length. 0 means unlimited.

Functions 50

private fnvoid enforceEx(E)(bool cond, lazy string msg) if (is(E : Exception))Helper that throws the given exception type when `cond` is false.
private fnUriTemplateOpInfo uriTemplateOpInfo(UriTemplateOperator op) @safe nothrowGet expansion settings for an RFC 6570 operator.
private fnstring uriTemplateExpandExpression(const UriTemplateExpression expr, const UriTemplateVars vars) @safeExpand a single expression using the given variable scope.
private fnUriTemplateExpression parseUriTemplateExpression(string exprStr, size_t basePos) @safeParse a single expression body (without braces) into an AST expression.
private fnUriTemplateVarSpec parseUriTemplateVarSpec(string exprStr, ref size_t i, size_t basePos) @safeParse a varspec starting at `i` inside an expression.
private fnUriTemplateOperator parseUriTemplateOperator(char c) @safeParse an RFC 6570 operator character.
private fnbool isUriTemplateAllowed(dchar c, bool allowReserved) @nogc nothrow @safeReturns true if `c` is allowed unescaped by URI Template encoding.
private fnvoid uriTemplateEncodeTo(R)(string s, bool allowReserved, ref R dst) @safeWrite URI Template-encoded representation of `s` to `dst`.
private fnstring uriTemplateEncodeSimple(string s) @safeURI Template encoding for simple operators (allow only unreserved).
private fnstring uriTemplateEncodeReserved(string s) @safeURI Template encoding for reserved operators (allow unreserved + reserved).
private fnstring uriTemplateApplyPrefix(string s, uint maxLen) @safeApply the RFC 6570 prefix modifier to `s`.
private fnstring[] uriTemplateValueToTokens(const UriTemplateVarSpec spec, const UriTemplateValue value, bool allowReserved) @safeConvert a value to expansion tokens for a given varspec.
private fnubyte[256] initHexToVal() @safeInitialize a lookup table that maps ASCII hex digits to their numeric values.
fnsize_t utf8Encode(dchar d, ref ubyte[4] outBuf) @safe nothrow @nogcEncode a single Unicode code point `d` to UTF-8 into `outBuf`.
private fnbool[256] initUnreserved() @safeInitialize the ASCII unreserved character set lookup table (RFC 3986 §2.3).
private fnbool[256] initSubDelims() @safeInitialize the ASCII sub-delims character set lookup table.
private fnbool isUnreserved(dchar c) @nogc nothrow @safeReturns true if `c` is an unreserved ASCII character (RFC 3986 §2.3).
private fnbool isSubDelim(dchar c) @nogc nothrow @safeReturns true if `c` is a sub-delimiter ASCII character (RFC 3986).
private fnbool isGenDelim(dchar c) @safe nothrow @nogcReturns true if `c` is a gen-delimiter ASCII character (RFC 3986).
fnvoid pctEncodeTo(R)(string s, Component comp, ref R dst) @safeWrite percent-encoded representation of `s` for component `comp` to `dst`.
fnstring pctEncode(string s, Component comp) @safePercent-encode `s` according to RFC 3986 rules for a specific component.
fnstring pctDecode(string s) @safeDecode percent-encoded triplets in `s`.
fnvoid pctNormalizeTo(R)(string s, Component comp, ref R dst) @safeWrite normalized percent-encoding of `s` for component `comp` to `dst`.
fnstring pctNormalize(string s, Component comp) @safeNormalize percent-encoding for a component string.
private fnubyte hexVal(char c) @nogc nothrow @safeConvert an ASCII hex digit to its numeric value.
fnbool isPctAllowed(dchar c, Component comp) @nogc nothrow @safeReturns true if character `c` is allowed to appear unescaped (literal ASCII byte) in `comp`.
fnstring removeDotSegments(string input) @safeRemove dot-segments from a path per RFC 3986 §5.2.4.
private fnvoid popLastSegment(ref string path) @safeRemove the last path segment from `path`.
fnint defaultPortForScheme(string scheme) @safeReturn the default TCP port for a known URI scheme, or `-1` if unknown.
private fnvoid validatePctEscapes(string s, string what) @safeValidate that any percent signs in `s` form valid `%HH` escapes.
private fnvoid validateComponentChars(string s, Component comp, bool allowNonAscii, string what) @safeValidate that `s` contains only RFC 3986-allowed characters for `comp`.
fnTuple!(string, string)[] parseQueryParams(string q) @safeParse a query string into key/value pairs without decoding.
fnTuple!(string, string)[] parseQueryParamsDecoded(string q, bool plusAsSpace = false) @safeParse a query string into key/value pairs with optional form-style decoding.
fnstring buildQueryParams(T)(T pairs) if (is(T : Tuple!(string, string)[])) @safeBuild a query string from key/value pairs without adding percent-encoding.
fnstring buildQueryParamsEncoded(T)(T pairs, bool spaceAsPlus = false) if (is(T : Tuple!(string, string)[])) @safeBuild a percent-encoded query string from key/value pairs.
private fnvoid encodeQueryTo(R)(string s, bool spaceAsPlus, ref R dst) @safeWrite a single query key or value applying percent-encoding and optional `+` mapping.
private fnchar basicDigitToCodePoint(int d) @safe nothrow @nogcConvert a digit value to a basic code point (Punycode).
private fnint basicCodePointToDigit(dchar c) @safe nothrow @nogcConvert a basic code point to its digit value (Punycode), or `-1`.
private fnint adaptBias(int delta, int numPoints, bool firstTime) @safe nothrow @nogcAdapt the Punycode bias value.
fnstring punycodeEncodeLabel(string label) @safeEncode a single DNS label to Punycode (without adding the `xn--` prefix).
fnstring punycodeDecodeLabel(string ascii) @safeDecode a single Punycode label (without the `xn--` prefix) to Unicode.
private fndchar mapDot(dchar c) @safe nothrow @nogcMap various dot-like separators to a literal `'.'` per IDNA.
private fnstring[] splitDomainLabels(string domain) @safeSplit a domain name into labels using mapped dots.
private fnstring uts46BasicMap(string s) @safeApply a small subset of UTS #46-like mappings.
private fnbool isProhibitedIdna(dchar c) @nogc nothrow @safeReturn true if code point is prohibited under strict IDNA options.
private fnvoid validateUnicodeLabel(string lab, IdnaOptions opt) @safeValidate a Unicode label under IDNA options.
fnstring idnaToASCII(string domain) @safeConvert a domain name to its ASCII form using IDNA ToASCII (basic version).
fnstring idnaToASCII(string domain, IdnaOptions opt) @safeIDNA ToASCII with options.
fnstring idnaToUnicode(string asciiDomain) @safeConvert an ASCII domain containing Punycode labels to its Unicode form (IDNA ToUnicode subset). Labels that begin with `xn--` are decoded; others are lowercased and returned as-is.
fnstring idnaToUnicode(string asciiDomain, IdnaOptions opt) @safeIDNA ToUnicode with options.

Variables 5

private varchar[16] HEX_UPPER
private varubyte[256] HEX_TO_VAL
private varbool[256] UNRES
private varbool[256] SUBDELIMS
enumvarMAX_URI_LENGTH = 8000

RFC 7230 recommended maximum URI length.