std1.0.0-beta-7 SafeC docs

Source encoding/url.h

View in the repository

// SafeC Standard Library — URLs: percent-encoding (RFC 3986), the
// application/x-www-form-urlencoded format (query strings and HTML form
// bodies, as the WHATWG URL standard's URLSearchParams defines it), and a
// structured URL type with parsing, serialization and relative-reference
// resolution (RFC 3986 §5).
//
// Pure string code, no I/O: usable on hosted, bare-metal and wasm32
// targets alike (std/http uses it on the server, std/wasm-side code can use
// it for client-side routing).
#pragma once
#include <std/collections/string.h>
#include <std/collections/vec.h>

namespace std {

// ── Percent-encoding ───────────────────────────────────────────────────────

// Percent-encodes every byte except the unreserved set (A-Z a-z 0-9 - _ . ~)
// — this matches the "component" flavor used for query-string keys/values
// and path segments (JavaScript's encodeURIComponent, not encodeURI).
// Space becomes "%20" (not "+" — that's an application/x-www-form-urlencoded
// convention some callers layer on top, not part of RFC 3986 itself; see
// form_encode below).
struct String url_encode(const &char s);

// Reverses url_encode: "%XX" becomes the raw byte 0xXX, "+" is left as a
// literal '+' (not decoded to space — see url_encode's note; use
// form_decode for form data). On a malformed "%" escape (not followed by
// two hex digits), '*ok' (if non-NULL) is set to 0 and decoding stops at
// that point; otherwise '*ok' is set to 1.
struct String url_decode(const &char s, ?&int ok);

// Like url_encode, but keeps the characters a URL *path* may hold as-is:
// '/', and RFC 3986's sub-delims and ':' '@' (so "/a b/c:d" becomes
// "/a%20b/c:d"). '%' itself is always escaped, so this is not idempotent —
// encode raw paths, not already-encoded ones.
struct String url_encode_path(const &char s);

// application/x-www-form-urlencoded: url_encode, except that space becomes
// '+' and '*' stays as-is (the WHATWG urlencoded serializer's byte set).
struct String form_encode(const &char s);

// Reverses form_encode: '+' becomes a space, "%XX" a byte. Unlike
// url_decode, a malformed '%' escape is kept literally (browsers do the
// same) and never fails.
struct String form_decode(const &char s);

// ── Query strings / form bodies ────────────────────────────────────────────

struct UrlQueryPair {
    struct String key;     // decoded
    struct String value;   // decoded
};

// An ordered multi-map of decoded key/value pairs — the parsed form of
// "a=1&b=two%20words&a=3" (a query string without its '?', or an
// application/x-www-form-urlencoded request body). Keys may repeat; get()
// returns the first. Lookups are case-sensitive, like URLSearchParams.
struct QueryParams {
    struct Vec<struct UrlQueryPair> pairs;

    static struct QueryParams new();
    // Parses 'qs' (a leading '?' is skipped). Empty segments ("a=1&&b=2")
    // are dropped; a segment without '=' is a key with an empty value.
    static struct QueryParams parse(const &char qs);
    // parse() over raw bytes (e.g. a request body, which is not
    // NUL-terminated in general).
    static struct QueryParams parse_bytes([]const char qs);

    unsigned long length() const;
    // The first value for 'key', or null if absent. The reference aliases
    // this object's storage: valid until the next mutation or free().
    const ?&char  get(const &char key) const;
    // get() with a fallback for an absent key.
    const &char   get_or(const &char key, const &char fallback) const;
    // How many pairs have this key, and the n-th of them (0-based; null
    // past the end).
    unsigned long count(const &char key) const;
    const ?&char  get_nth(const &char key, unsigned long n) const;
    int           has(const &char key) const;
    // i-th pair in order (null past the end).
    const ?&char  key_at(unsigned long i) const;
    const ?&char  value_at(unsigned long i) const;

    // Adds a pair at the end (keeps existing ones with the same key).
    void          append(const &char key, const &char value);
    // Replaces the first pair with this key and removes the others, or
    // appends one if there was none.
    void          set(const &char key, const &char value);
    // Removes every pair with this key; returns how many there were.
    unsigned long remove(const &char key);
    // Stable sort by key (byte order), as URLSearchParams.sort().
    void          sort();

    // Serializes back to "k=v&k2=v2" with form_encode on both halves
    // (no leading '?').
    struct String to_string() const;
    struct QueryParams clone() const;
    void          drop();
    void          free() consume;   // = drop()
};

// ── URL ────────────────────────────────────────────────────────────────────

// A URL split into its RFC 3986 components:
//   scheme://username:password@host:port/path?query#fragment
// Components are stored as they appear in the text (still percent-encoded,
// so they can be serialized back unchanged), except that the scheme and
// a registered-name host are lowercased and an IPv6 literal's brackets are
// dropped from 'host' ("[::1]" is stored as "::1"). 'port' is -1 when the
// URL has none. has_authority / has_query / has_fragment tell "no query"
// apart from an empty one ("/p?" vs "/p").
struct Url {
    struct String scheme;     // "https" (no ':'), empty for a relative reference
    struct String username;
    struct String password;
    struct String host;
    int           port;       // -1: none given
    struct String path;       // "/a/b" (may be empty, e.g. "mailto:x" has path "x")
    struct String query;      // without the '?'
    struct String fragment;   // without the '#'
    int           has_authority;   // "//" was present
    int           has_query;
    int           has_fragment;

    // An empty relative reference (every component empty, port -1).
    static struct Url new();
    // Parses an absolute URL or a relative reference. Fails (*ok = 0, and
    // an empty Url) on a bad scheme, a non-numeric or > 65535 port, an
    // unterminated '[' IPv6 literal, or a control character/space.
    static struct Url parse(const &char s, ?&int ok);

    // Back to text (the RFC 3986 §5.3 recomposition).
    struct String to_string() const;
    // Resolves the reference 'ref' against this (absolute) base URL —
    // RFC 3986 §5.2, including dot-segment removal: base
    // "http://a/b/c/d;p?q" + "../g" = "http://a/b/g". Fails if 'ref'
    // doesn't parse or this base has no scheme.
    struct Url    resolve(const &char ref, ?&int ok) const;

    // 'port', or the scheme's default (http/ws 80, https/wss 443, ftp 21)
    // when none was given; -1 for an unknown scheme without a port.
    int           port_or_default() const;
    // "host" or "host:port" (only an explicit port), IPv6 re-bracketed —
    // what an HTTP Host header carries.
    struct String host_port() const;
    // "scheme://host[:port]" — the web origin (for CORS, cookies, ...).
    struct String origin() const;
    // path (or "/" when empty) plus "?query" — an HTTP request-target.
    struct String request_target() const;
    // The decoded query parameters.
    struct QueryParams query_params() const;
    // Replaces the query with params.to_string() (has_query = 1, or 0 for
    // an empty params).
    void          set_query_params(const &QueryParams params);
    // The path with each %XX decoded (for file lookups, routing, ...).
    struct String path_decoded() const;
    int           is_absolute() const;     // has a scheme

    struct Url    clone() const;
    void          drop();
    void          free() consume;   // = drop()
};

// RFC 3986 §5.2.4: "/a/b/../c/./d" -> "/a/c/d".
struct String url_remove_dot_segments(const &char path);

} // namespace std