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
URLandURN(via composition overURI) - 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 objectUriTemplate.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
Appenderinternally to avoid O(n²) string
concatenation in loops.
- Query parameter building functions use
Appenderfor 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
Base exception type for URI-related errors.
this(string msg, string file = __FILE__, size_t line = __LINE__)Construct a `UriException`.Exception type for URI parsing/validation errors.
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.
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.
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.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`).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.
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.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 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).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.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.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).
URI innerUniform Resource Name wrapper (scheme == "urn").
Provides convenient access to the Namespace Specific String (NSS) while delegating serialization and parsing to the underlying URI.
URI innerUnderlying `URI` instance; guaranteed to have scheme `"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.Exception type for RFC 6570 URI Template parsing/expansion errors.
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).
One variable specification inside an RFC 6570 expression.
Example varspecs:
varvar:3(prefix modifier)list*(explode modifier)
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 ) `}`.
UriTemplateOperator opOperator that controls rendering and encoding.UriTemplateVarSpec[] varSpecsVariable specifications in order.Kind of a template part.
One part of a template: either a literal substring or an expression.
UriTemplatePartKind kindWhether this part is a literal or expression.string literalLiteral substring (valid when `kind == LITERAL`).UriTemplateExpression expressionExpression value (valid when `kind == EXPRESSION`).RFC 6570 URI Template.
A template is a sequence of literal and expression parts.
Notes: Parsing and expansion are implemented incrementally in tasks T4+.
UriTemplatePart[] partsTemplate parts in order.string expand(const UriTemplateVars vars) const @safeExpand this URI Template with the provided variables.Operator properties needed for expansion.
string prefixstring separatorbool namedbool ifEmptyNameOnlybool allowReservedRFC 6570 variable value kind.
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.
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`).UriTemplateValue fromMap(Tuple!(string, string)[] pairs) @safeConstruct a map value from ordered key/value pairs.Expansion scope for URI Templates.
Maps variable names to their values.
private UriTemplateValue[string] valuesstring ssize_t ibool strictbool allowNonAsciiRegNameptrdiff_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`.string parsePathRootlessOrNoScheme(bool hasScheme) @safeParse a rootless path in the appropriate mode for the URI context.this(string s)Construct a parser for the given input string.Options controlling IDNA processing.
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.
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
void enforceEx(E)(bool cond, lazy string msg) if (is(E : Exception))Helper that throws the given exception type when `cond` is false.UriTemplateOpInfo uriTemplateOpInfo(UriTemplateOperator op) @safe nothrowGet expansion settings for an RFC 6570 operator.string uriTemplateExpandExpression(const UriTemplateExpression expr, const UriTemplateVars vars) @safeExpand a single expression using the given variable scope.UriTemplateExpression parseUriTemplateExpression(string exprStr, size_t basePos) @safeParse a single expression body (without braces) into an AST expression.UriTemplateVarSpec parseUriTemplateVarSpec(string exprStr, ref size_t i, size_t basePos) @safeParse a varspec starting at `i` inside an expression.UriTemplateOperator parseUriTemplateOperator(char c) @safeParse an RFC 6570 operator character.bool isUriTemplateAllowed(dchar c, bool allowReserved) @nogc nothrow @safeReturns true if `c` is allowed unescaped by URI Template encoding.void uriTemplateEncodeTo(R)(string s, bool allowReserved, ref R dst) @safeWrite URI Template-encoded representation of `s` to `dst`.string uriTemplateEncodeSimple(string s) @safeURI Template encoding for simple operators (allow only unreserved).string uriTemplateEncodeReserved(string s) @safeURI Template encoding for reserved operators (allow unreserved + reserved).string uriTemplateApplyPrefix(string s, uint maxLen) @safeApply the RFC 6570 prefix modifier to `s`.string[] uriTemplateValueToTokens(const UriTemplateVarSpec spec, const UriTemplateValue value,
bool allowReserved) @safeConvert a value to expansion tokens for a given varspec.ubyte[256] initHexToVal() @safeInitialize a lookup table that maps ASCII hex digits to their numeric values.size_t utf8Encode(dchar d, ref ubyte[4] outBuf) @safe nothrow @nogcEncode a single Unicode code point `d` to UTF-8 into `outBuf`.bool[256] initUnreserved() @safeInitialize the ASCII unreserved character set lookup table (RFC 3986 §2.3).bool[256] initSubDelims() @safeInitialize the ASCII sub-delims character set lookup table.bool isUnreserved(dchar c) @nogc nothrow @safeReturns true if `c` is an unreserved ASCII character (RFC 3986 §2.3).bool isSubDelim(dchar c) @nogc nothrow @safeReturns true if `c` is a sub-delimiter ASCII character (RFC 3986).bool isGenDelim(dchar c) @safe nothrow @nogcReturns true if `c` is a gen-delimiter ASCII character (RFC 3986).void pctEncodeTo(R)(string s, Component comp, ref R dst) @safeWrite percent-encoded representation of `s` for component `comp` to `dst`.string pctEncode(string s, Component comp) @safePercent-encode `s` according to RFC 3986 rules for a specific component.void pctNormalizeTo(R)(string s, Component comp, ref R dst) @safeWrite normalized percent-encoding of `s` for component `comp` to `dst`.string pctNormalize(string s, Component comp) @safeNormalize percent-encoding for a component string.bool isPctAllowed(dchar c, Component comp) @nogc nothrow @safeReturns true if character `c` is allowed to appear unescaped (literal ASCII byte) in `comp`.int defaultPortForScheme(string scheme) @safeReturn the default TCP port for a known URI scheme, or `-1` if unknown.void validatePctEscapes(string s, string what) @safeValidate that any percent signs in `s` form valid `%HH` escapes.void validateComponentChars(string s, Component comp, bool allowNonAscii, string what) @safeValidate that `s` contains only RFC 3986-allowed characters for `comp`.Tuple!(string, string)[] parseQueryParams(string q) @safeParse a query string into key/value pairs without decoding.Tuple!(string, string)[] parseQueryParamsDecoded(string q, bool plusAsSpace = false) @safeParse a query string into key/value pairs with optional form-style decoding.string buildQueryParams(T)(T pairs) if (is(T : Tuple!(string, string)[])) @safeBuild a query string from key/value pairs without adding percent-encoding.string buildQueryParamsEncoded(T)(T pairs, bool spaceAsPlus = false) if (is(T : Tuple!(string, string)[])) @safeBuild a percent-encoded query string from key/value pairs.void encodeQueryTo(R)(string s, bool spaceAsPlus, ref R dst) @safeWrite a single query key or value applying percent-encoding and optional `+` mapping.char basicDigitToCodePoint(int d) @safe nothrow @nogcConvert a digit value to a basic code point (Punycode).int basicCodePointToDigit(dchar c) @safe nothrow @nogcConvert a basic code point to its digit value (Punycode), or `-1`.int adaptBias(int delta, int numPoints, bool firstTime) @safe nothrow @nogcAdapt the Punycode bias value.string punycodeEncodeLabel(string label) @safeEncode a single DNS label to Punycode (without adding the `xn--` prefix).string punycodeDecodeLabel(string ascii) @safeDecode a single Punycode label (without the `xn--` prefix) to Unicode.dchar mapDot(dchar c) @safe nothrow @nogcMap various dot-like separators to a literal `'.'` per IDNA.string[] splitDomainLabels(string domain) @safeSplit a domain name into labels using mapped dots.bool isProhibitedIdna(dchar c) @nogc nothrow @safeReturn true if code point is prohibited under strict IDNA options.void validateUnicodeLabel(string lab, IdnaOptions opt) @safeValidate a Unicode label under IDNA options.string idnaToASCII(string domain) @safeConvert a domain name to its ASCII form using IDNA ToASCII (basic version).string 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.Variables 5
char[16] HEX_UPPERubyte[256] HEX_TO_VALbool[256] UNRESbool[256] SUBDELIMSMAX_URI_LENGTH = 8000RFC 7230 recommended maximum URI length.