Salta al contenuto
Note per Studenti Esercizio - URI, scomposizione, percent-encoding e riferimenti relativi (sul modello della prova pratica)

Esercizio - URI, scomposizione, percent-encoding e riferimenti relativi (sul modello della prova pratica)

In questa pagina 6

Testo (sul modello della prova pratica di Reti di Calcolatori, Ing. Informatica UniPD; sintassi da RFC 3986).

Scrivere in C, senza librerie, le funzioni per lavorare con gli URI:

  1. uri_split: scomporre scheme://userinfo@host:porta/percorso?query#frammento nei suoi componenti, distinguendo un componente assente da uno vuoto;
  2. percent_encode e percent_decode: codificare con %HH i byte che non sono caratteri unreserved (lettere, cifre, - . _ ~) e decodificarli, rifiutando le sequenze rotte; verificare caffè & latte → caff%C3%A8%20%26%20latte;
  3. remove_dot_segments: togliere i segmenti . e .. da un percorso (RFC 3986 par. 5.2.4), per esempio /a/b/c/./../../g → /a/g;
  4. uri_resolve: risolvere un riferimento relativo rispetto a una base (par. 5.2.2) e ricomporre l'URI (par. 5.3);
  5. provare tutto con gli esempi della RFC (base http://a/b/c/d;p?q).

Teoria: Caching, autenticazione, tipi MIME e URI in HTTPLa cache HTTP riusa le risposte senza contattare il server finche' sono fresche (Cache-Control: max-age, Expires; in mancanza una durata euristica pari a circa il 10% del tempo trascorso da Last-Modified) e, quando sono scadute, le rivalida con una richiesta condizionale (If-None-Match con ETag, If-Modified-Since con Last-Modified) alla quale il server risponde 304 senza corpo; no-store vieta di memorizzare, no-cache obbliga a rivalidare, private esclude le cache condivise. L'autenticazione usa 401 con WWW-Authenticate e la risposta Authorization: Basic (base64 di utente:password, solo sopra TLS) o Digest (hash con nonce); 407 vale per i proxy. Content-Type porta il tipo MIME (tipo/sottotipo; parametri come charset e boundary), Accept e gli altri header Accept-* guidano la negoziazione. Un URI (RFC 3986) e' scheme://userinfo@host:porta/percorso?query#frammento, con percent-encoding %HH per i byte non ammessi e regole per risolvere i riferimenti relativi.Caching, autenticazione, tipi MIME e URI in HTTP →, Esercizio - Parsing di request line, header e URI (sul modello della prova pratica), Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endiannessIn C un programma di rete maneggia byte, non oggetti: un processo ha codice, dati statici, heap e stack; i tipi hanno dimensioni fisse solo se si usano <stdint.h> (uint8_t, uint16_t, uint32_t); i dati che arrivano da un socket sono un buffer di byte con una lunghezza, NON una stringa C terminata da '\0'; i puntatori e l'aritmetica dei puntatori (buf + totale) permettono di riempire un buffer a pezzi; una struct puo' contenere byte di riempimento (padding) per l'allineamento, quindi non si spedisce con write(&s, sizeof s); sulla rete i numeri a piu' byte viaggiano in big endian (network byte order) e si convertono con htons, htonl, ntohs, ntohl, oppure si serializzano a mano con shift e maschere.Richiami di C per la programmazione di rete - memoria, puntatori, struct ed endianness →.


Idea

Un URI è una stringa con una sintassi precisa (RFC 3986), e le trappole stanno nei dettagli:

  • un componente può mancare (http://a/b, nessuna query) oppure essere vuoto (http://a/b?, query vuota) e la ricomposizione deve distinguerli: per questo la struttura ha dei flag has_query, has_fragment, has_authority, has_scheme;
  • il percorso può contenere . e .., che si risolvono prima di usarlo (e prima di concederne l'accesso a un file);
  • i caratteri non ammessi si scrivono come %HH, byte per byte della forma UTF-8: è sono due byte (C3 A8) e quindi %C3%A8.

Il programma non apre socket e ha un main con autotest: 41 coppie (riferimento, risultato atteso) dalla tabella degli esempi normali e anomali di RFC 3986 par. 5.4, più i casi di scomposizione e codifica.

remove_dot_segments è l'algoritmo del par. 5.2.4: si scorre il percorso in ingresso e si costruisce l'uscita, un segmento alla volta:

  1. se l'ingresso comincia con ../ o ./, si toglie quel prefisso;
  2. se comincia con /./ o è esattamente /., si sostituisce con /;
  3. se comincia con /../ o è /.., si sostituisce con / e si toglie dall'uscita l'ultimo segmento;
  4. se è solo . o .., si toglie;
  5. altrimenti si sposta il primo segmento (con l'eventuale / iniziale) dall'ingresso all'uscita.

uri_resolve (par. 5.2.2) in sintesi: se il riferimento ha uno schema, è già assoluto; se ha un'autorità, si prendono schema e basi dalla base e il resto dal riferimento; se il percorso è vuoto si usa quello della base (e la query della base se il riferimento non ne ha una); se il percorso comincia per / si usa quello; altrimenti si fonde con la base (la base fino all'ultima /, più il riferimento) e si rimuovono i segmenti punto.

Codice

c
/* uri_demo.c - URI secondo RFC 3986: scomposizione, percent-encoding, rimozione dei segmenti "." e "..",
 * risoluzione di un riferimento relativo, con autotest sugli esempi della RFC (par. 5.4).
 * Compilare e provare:  gcc -Wall -Wextra -o uri_demo uri_demo.c && ./uri_demo
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#undef NDEBUG                      /* gli assert dell'autotest devono restare attivi anche con -DNDEBUG */
#include <assert.h>

struct uri {
    char scheme[16], userinfo[64], host[128], port[8], path[256], query[256], fragment[128];
    int has_scheme, has_authority, has_query, has_fragment;
};

/* Scompone  scheme:[//[userinfo@]host[:port]]path[?query][#fragment]  (RFC 3986, appendice B). */
static int uri_split(const char *s, struct uri *u)
{
    memset(u, 0, sizeof *u);
    const char *p = s;
    /* scheme: lettere, cifre, + - . fino ai ':' ; deve esserci un ':' prima di '/', '?', '#' */
    size_t k = strcspn(p, ":/?#");
    if (p[k] == ':' && k > 0 && isalpha((unsigned char)p[0]) && k < sizeof u->scheme) {
        memcpy(u->scheme, p, k);
        u->has_scheme = 1;
        p += k + 1;
    }
    if (p[0] == '/' && p[1] == '/') {                      /* autorita': fra "//" e il prossimo '/', '?' o '#' */
        p += 2;
        size_t a = strcspn(p, "/?#");
        char auth[256];
        if (a >= sizeof auth)
            return -1;
        memcpy(auth, p, a);
        auth[a] = '\0';
        u->has_authority = 1;
        p += a;
        char *at = strrchr(auth, '@');
        char *hostpart = auth;
        if (at) {
            *at = '\0';
            snprintf(u->userinfo, sizeof u->userinfo, "%s", auth);
            hostpart = at + 1;
        }
        char *colon = strrchr(hostpart, ':');              /* l'ultimo ':' (e non dentro [IPv6]) separa la porta */
        if (colon && !(hostpart[0] == '[' && strchr(colon, ']'))) {
            snprintf(u->port, sizeof u->port, "%s", colon + 1);
            *colon = '\0';
        }
        snprintf(u->host, sizeof u->host, "%s", hostpart);
    }
    size_t pl = strcspn(p, "?#");                          /* percorso: fino a '?' o '#' */
    if (pl >= sizeof u->path)
        return -1;
    memcpy(u->path, p, pl);
    p += pl;
    if (*p == '?') {
        size_t q = strcspn(++p, "#");
        if (q >= sizeof u->query)
            return -1;
        memcpy(u->query, p, q);
        u->has_query = 1;
        p += q;
    }
    if (*p == '#') {
        snprintf(u->fragment, sizeof u->fragment, "%s", p + 1);
        u->has_fragment = 1;
    }
    return 0;
}

/* Algoritmo di RFC 3986 par. 5.2.4: toglie i segmenti "." e ".." da un percorso. */
static void remove_dot_segments(const char *in, char *out, size_t size)
{
    char buf[256];
    snprintf(buf, sizeof buf, "%s", in);
    char *i = buf;
    size_t o = 0;
    out[0] = '\0';
    while (*i) {
        if (strncmp(i, "../", 3) == 0)       i += 3;
        else if (strncmp(i, "./", 2) == 0)   i += 2;
        else if (strncmp(i, "/./", 3) == 0)  i += 2;                 /* "/./x" -> "/x" */
        else if (strcmp(i, "/.") == 0)       { i[1] = '\0'; }        /* "/." -> "/" */
        else if (strncmp(i, "/../", 4) == 0 || strcmp(i, "/..") == 0) {
            if (strcmp(i, "/..") == 0) i[1] = '\0'; else i += 3;     /* "/../x" -> "/x", "/.." -> "/" */
            while (o > 0 && out[o - 1] != '/')                       /* si toglie l'ultimo segmento gia' in uscita */
                o--;
            if (o > 0)
                o--;
            out[o] = '\0';
        }
        else if (strcmp(i, ".") == 0 || strcmp(i, "..") == 0)  *i = '\0';
        else {                                                       /* altrimenti si sposta un segmento in uscita */
            size_t n = strcspn(i + (*i == '/'), "/") + (*i == '/');
            if (o + n + 1 > size)
                return;
            memcpy(out + o, i, n);
            o += n;
            out[o] = '\0';
            i += n;
        }
    }
}

/* Risoluzione di un riferimento relativo rispetto a una base (RFC 3986 par. 5.2.2). */
static void uri_resolve(const struct uri *base, const struct uri *ref, char *out, size_t size)
{
    struct uri t;
    memset(&t, 0, sizeof t);
    if (ref->has_scheme) {
        t = *ref;
        remove_dot_segments(ref->path, t.path, sizeof t.path);
    } else {
        if (ref->has_authority) {
            t = *ref;
            remove_dot_segments(ref->path, t.path, sizeof t.path);
        } else {
            if (ref->path[0] == '\0') {
                snprintf(t.path, sizeof t.path, "%s", base->path);
                t.has_query = ref->has_query ? 1 : base->has_query;
                snprintf(t.query, sizeof t.query, "%s", ref->has_query ? ref->query : base->query);
            } else {
                char merged[512];
                if (ref->path[0] == '/') {
                    snprintf(merged, sizeof merged, "%s", ref->path);
                } else if (base->has_authority && base->path[0] == '\0') {
                    snprintf(merged, sizeof merged, "/%s", ref->path);
                } else {
                    snprintf(merged, sizeof merged, "%s", base->path);
                    char *slash = strrchr(merged, '/');            /* si tiene la base fino all'ultima '/' */
                    if (slash) slash[1] = '\0'; else merged[0] = '\0';
                    strncat(merged, ref->path, sizeof merged - strlen(merged) - 1);
                }
                remove_dot_segments(merged, t.path, sizeof t.path);
                t.has_query = ref->has_query;
                snprintf(t.query, sizeof t.query, "%s", ref->query);
            }
            t.has_authority = base->has_authority;
            snprintf(t.userinfo, sizeof t.userinfo, "%s", base->userinfo);
            snprintf(t.host, sizeof t.host, "%s", base->host);
            snprintf(t.port, sizeof t.port, "%s", base->port);
        }
        t.has_scheme = base->has_scheme;
        snprintf(t.scheme, sizeof t.scheme, "%s", base->scheme);
    }
    t.has_fragment = ref->has_fragment;
    snprintf(t.fragment, sizeof t.fragment, "%s", ref->fragment);

    /* ricomposizione (par. 5.3) */
    size_t o = 0;
    out[0] = '\0';
    if (t.has_scheme)
        o += (size_t)snprintf(out + o, size - o, "%s:", t.scheme);
    if (t.has_authority) {
        o += (size_t)snprintf(out + o, size - o, "//");
        if (t.userinfo[0])
            o += (size_t)snprintf(out + o, size - o, "%s@", t.userinfo);
        o += (size_t)snprintf(out + o, size - o, "%s", t.host);
        if (t.port[0])
            o += (size_t)snprintf(out + o, size - o, ":%s", t.port);
    }
    o += (size_t)snprintf(out + o, size - o, "%s", t.path);
    if (t.has_query)
        o += (size_t)snprintf(out + o, size - o, "?%s", t.query);
    if (t.has_fragment)
        snprintf(out + o, size - o, "#%s", t.fragment);
}

/* Percent-encoding: ogni byte che non e' "unreserved" (lettere, cifre, - . _ ~) diventa %HH.
 * I caratteri non ASCII si codificano byte per byte nella loro forma UTF-8. */
static int percent_encode(const char *s, char *out, size_t size)
{
    size_t o = 0;
    for (; *s; s++) {
        unsigned char c = (unsigned char)*s;
        if (isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
            if (o + 2 > size) return -1;
            out[o++] = (char)c;
        } else {
            if (o + 4 > size) return -1;
            o += (size_t)sprintf(out + o, "%%%02X", c);
        }
    }
    out[o] = '\0';
    return 0;
}

static int hexval(int c)
{
    if (c >= '0' && c <= '9') return c - '0';
    if (c >= 'a' && c <= 'f') return c - 'a' + 10;
    if (c >= 'A' && c <= 'F') return c - 'A' + 10;
    return -1;
}

/* Inverso: %HH -> byte. Torna -1 se una sequenza e' rotta. Per i dati di un modulo ('+' = spazio) serve in piu'
 * la sostituzione di '+', che NON vale nel percorso di un URI. */
static int percent_decode(const char *s, char *out, size_t size)
{
    size_t o = 0;
    for (; *s; s++) {
        int c = (unsigned char)*s;
        if (c == '%') {
            int h = hexval((unsigned char)s[1]);
            int l = (h >= 0) ? hexval((unsigned char)s[2]) : -1;
            if (h < 0 || l < 0) return -1;
            c = h * 16 + l;
            s += 2;
        }
        if (o + 1 >= size) return -1;
        out[o++] = (char)c;
    }
    out[o] = '\0';
    return 0;
}

int main(void)
{
    struct uri u;
    char out[512];

    /* 1) scomposizione */
    assert(uri_split("http://mario:pwd@www.example.com:8080/a/b/../c.html?x=1&y=2#sezione", &u) == 0);
    assert(!strcmp(u.scheme, "http") && !strcmp(u.userinfo, "mario:pwd") && !strcmp(u.host, "www.example.com"));
    assert(!strcmp(u.port, "8080") && !strcmp(u.path, "/a/b/../c.html") && !strcmp(u.query, "x=1&y=2") && !strcmp(u.fragment, "sezione"));
    assert(uri_split("http://[::1]:8080/x", &u) == 0 && !strcmp(u.host, "[::1]") && !strcmp(u.port, "8080"));
    assert(uri_split("mailto:mario@example.com", &u) == 0 && !strcmp(u.scheme, "mailto") && !u.has_authority && !strcmp(u.path, "mario@example.com"));
    assert(uri_split("/solo/percorso?q", &u) == 0 && !u.has_scheme && !u.has_authority && u.has_query);
    assert(uri_split("www.example.com:80", &u) == 0 && !strcmp(u.scheme, "www.example.com"));   /* "host:porta" senza // e' ambiguo: si legge come schema */

    /* 2) percent-encoding */
    assert(percent_encode("caff\xC3\xA8 & latte", out, sizeof out) == 0 && !strcmp(out, "caff%C3%A8%20%26%20latte"));
    assert(percent_decode("caff%C3%A8%20%26%20latte", out, sizeof out) == 0 && !strcmp(out, "caff\xC3\xA8 & latte"));
    assert(percent_decode("100%", out, sizeof out) == -1);

    /* 3) rimozione dei segmenti . e .. */
    remove_dot_segments("/a/b/c/./../../g", out, sizeof out);
    assert(!strcmp(out, "/a/g"));
    remove_dot_segments("mid/content=5/../6", out, sizeof out);
    assert(!strcmp(out, "mid/6"));
    remove_dot_segments("/../../etc/passwd", out, sizeof out);
    assert(!strcmp(out, "/etc/passwd"));                   /* ".." non esce dalla radice, ma qui e' solo la sintassi */

    /* 4) risoluzione di riferimenti relativi: tabella della RFC 3986 par. 5.4, base http://a/b/c/d;p?q */
    static const char *table[][2] = {
        {"g:h", "g:h"}, {"g", "http://a/b/c/g"}, {"./g", "http://a/b/c/g"}, {"g/", "http://a/b/c/g/"},
        {"/g", "http://a/g"}, {"//g", "http://g"}, {"?y", "http://a/b/c/d;p?y"}, {"g?y", "http://a/b/c/g?y"},
        {"#s", "http://a/b/c/d;p?q#s"}, {"g#s", "http://a/b/c/g#s"}, {"g?y#s", "http://a/b/c/g?y#s"},
        {";x", "http://a/b/c/;x"}, {"g;x", "http://a/b/c/g;x"}, {"g;x?y#s", "http://a/b/c/g;x?y#s"},
        {"", "http://a/b/c/d;p?q"}, {".", "http://a/b/c/"}, {"./", "http://a/b/c/"}, {"..", "http://a/b/"},
        {"../", "http://a/b/"}, {"../g", "http://a/b/g"}, {"../..", "http://a/"}, {"../../", "http://a/"},
        {"../../g", "http://a/g"}, {"../../../g", "http://a/g"}, {"../../../../g", "http://a/g"},
        {"/./g", "http://a/g"}, {"/../g", "http://a/g"}, {"g.", "http://a/b/c/g."}, {".g", "http://a/b/c/.g"},
        {"g..", "http://a/b/c/g.."}, {"..g", "http://a/b/c/..g"}, {"./../g", "http://a/b/g"},
        {"./g/.", "http://a/b/c/g/"}, {"g/./h", "http://a/b/c/g/h"}, {"g/../h", "http://a/b/c/h"},
        {"g;x=1/./y", "http://a/b/c/g;x=1/y"}, {"g;x=1/../y", "http://a/b/c/y"},
        {"g?y/./x", "http://a/b/c/g?y/./x"}, {"g?y/../x", "http://a/b/c/g?y/../x"},
        {"g#s/./x", "http://a/b/c/g#s/./x"}, {"g#s/../x", "http://a/b/c/g#s/../x"},
    };
    struct uri base, ref;
    uri_split("http://a/b/c/d;p?q", &base);
    for (size_t i = 0; i < sizeof table / sizeof table[0]; i++) {
        uri_split(table[i][0], &ref);
        uri_resolve(&base, &ref, out, sizeof out);
        if (strcmp(out, table[i][1]) != 0) {
            fprintf(stderr, "ERRORE: \"%s\" -> \"%s\" invece di \"%s\"\n", table[i][0], out, table[i][1]);
            return 1;
        }
    }
    printf("uri_demo: tutti i test passati (comprese %zu risoluzioni della RFC 3986)\n", sizeof table / sizeof table[0]);
    return 0;
}

Compilare e provare

$ gcc -Wall -Wextra -o uri_demo uri_demo.c
$ ./uri_demo
uri_demo: tutti i test passati (comprese 41 risoluzioni della RFC 3986)

Il programma si ferma al primo caso sbagliato con un messaggio come ERRORE: "g?y/./x" -> "..." invece di "http://a/b/c/g?y/./x". Per aggiungere un caso di scomposizione:

c
assert(uri_split("ftp://u:p@h.it:21/x", &u) == 0 && !strcmp(u.scheme, "ftp") && !strcmp(u.port, "21"));

Per confrontare con uno strumento standard, python3 -c "from urllib.parse import urljoin; print(urljoin('http://a/b/c/d;p?q', '../g'))" stampa http://a/b/g, come uri_resolve.

Spiegazione dei punti chiave

uri_split.

  • Lo schema è il tratto prima del primo :, ma solo se il : compare prima di /, ? e # e il primo carattere è una lettera: strcspn(p, ":/?#") cerca il primo delimitatore. Per questo mailto:mario@example.com ha schema mailto, e anche www.example.com:80 (senza //) è letto come schema www.example.com e percorso 80 (è la lettura prevista dalla RFC: host:porta senza schema è ambiguo).
  • L'autorità esiste se subito dopo ci sono due /: arriva fino al prossimo /, ? o #. Al suo interno: l'ultimo @ separa userinfo; l'ultimo : (fuori dalle parentesi [ ] di un indirizzo IPv6) separa la porta.
  • Il percorso arriva fino a ? o #; la query (dopo ?) fino a #; il frammento è il resto. Ogni parte controlla di entrare nel buffer, altrimenti -1.
  • has_query vale 1 anche per una query vuota (?): http://a/b? e http://a/b sono diversi.

percent_encode. Per ogni byte: se è alfanumerico o - . _ ~ lo copia; altrimenti scrive % e due cifre esadecimali maiuscole (sprintf("%%%02X", c)). Il cast a unsigned char è indispensabile: un byte oltre 0x7F in un char con segno darebbe un numero negativo e %FFFFFFC3. Si controlla lo spazio residuo prima di ogni scrittura.

percent_decode. Per ogni % servono due cifre esadecimali (hexval restituisce -1 altrimenti); l'espressione h >= 0 ? hexval(s[2]) : -1 evita di leggere s[2] se s[1] era già il terminatore. Il + non si converte in spazio: vale solo nei dati dei moduli (application/x-www-form-urlencoded), non nel percorso.

remove_dot_segments. Lavora su una copia (buf) e su un indice di uscita o. Per il caso 3 toglie dall'uscita l'ultimo segmento: indietreggia fino al / precedente (while (o > 0 && out[o - 1] != '/') o--) e poi elimina anche quello. I casi si controllano nell'ordine della RFC (../, ./, /./, /., /../, /.., ., ..): l'ordine conta, perché /./ e /../ contengono i prefissi più corti. Un .. in testa non può uscire dalla radice: /../../etc/passwd diventa /etc/passwd (è solo sintassi: per un server di file la protezione è un'altra cosa, Esercizio - Parsing di request line, header e URI (sul modello della prova pratica)).

uri_resolve. Segue passo passo il par. 5.2.2, usando i flag per distinguere "assente" da "vuoto": per esempio, per ?y (riferimento con percorso vuoto e query) il risultato prende percorso e autorità della base ma la query del riferimento: http://a/b/c/d;p?y; per #s prende anche la query della base: http://a/b/c/d;p?q#s. La fusione (merge) prende la base fino all'ultima / e vi accoda il percorso del riferimento: per g e base /b/c/d;p si ottiene /b/c/g. La ricomposizione concatena schema:, //userinfo@host:porta, percorso, ?query, #frammento, ciascuno solo se presente.

Gli autotest. Verificano la scomposizione (anche con IPv6 e mailto:), la codifica e la decodifica, tre casi di rimozione dei segmenti punto e tutte le 41 risoluzioni della RFC (normali e anomale, come ../../../g → http://a/g, g?y/./x che lascia intatti i punti dentro la query, .g e ..g che non sono segmenti speciali).

Errori tipici

  • Confondere componente assente e componente vuoto: una ricomposizione senza flag trasforma http://a/b? in http://a/b.
  • Cercare lo schema senza controllare che i : vengano prima di /: un percorso come /a:b non ha schema.
  • Prendere il primo : dell'autorità come separatore della porta (un indirizzo IPv6 ne contiene molti): si usa l'ultimo, fuori dalle parentesi quadre.
  • Codificare con % i byte char con segno senza il cast a unsigned char.
  • Decodificare il + come spazio nel percorso, o decodificare due volte (%252e).
  • Rimuovere i segmenti punto dopo aver aperto il file, invece che prima.
  • Nella fusione dei percorsi, tenere la base fino al segmento finale invece che fino all'ultima /.
  • Pensare che il frammento (#...) vada al server: non si invia mai.

Varianti per esercitarsi

  • Normalizzare un URI: schema e host in minuscolo, porta predefinita tolta (:80 per http), segmenti punto rimossi, percent-encoding portato a maiuscolo.
  • Confrontare due URI come equivalenti dopo la normalizzazione.
  • Scomporre la query in coppie chiave=valore con decodifica (+ e %XX come nei moduli).
  • Usare uri_resolve per scrivere un piccolo crawler: dato il testo di una pagina HTML, estrarre i link e risolverli rispetto all'URL della pagina.

Versione ripasso

  • Testo. uri_split (scheme://userinfo@host:porta/percorso?query#frammento, assente <> vuoto), percent_encode/percent_decode, remove_dot_segments (RFC 3986 par. 5.2.4), uri_resolve (par. 5.2.2) con le 41 risoluzioni della RFC (base http://a/b/c/d;p?q).
  • Struttura. struct uri con i componenti e i flag has_scheme, has_authority, has_query, has_fragment: la ricomposizione distingue http://a/b? (query vuota) da http://a/b.
  • uri_split. Schema = prima di : se il : viene prima di /, ?, # e il primo carattere è una lettera; autorità se segue // (fino a /, ?, #); ultimo @ = userinfo; ultimo : fuori da [ ] = porta; percorso fino a ?/#; query fino a #; frammento = resto.
  • Percent-encoding. Unreserved: lettere, cifre, - . _ ~; altrimenti % + 2 cifre esadecimali maiuscole del byte (cast a unsigned char); caffè & latte -> caff%C3%A8%20%26%20latte. Decodifica: due cifre dopo % (-1 altrimenti); + non è spazio nel percorso.
  • remove_dot_segments (ordine della RFC): (1) ../ o ./ in testa: tolti; (2) /./ -> /, /. -> /; (3) /../ o /.. -> / e si toglie l'ultimo segmento dall'uscita; (4) . o .. soli: tolti; (5) altrimenti si sposta un segmento in uscita. /a/b/c/./../../g -> /a/g; ../../../g non supera la radice.
c
else if (strncmp(i, "/../", 4) == 0 || strcmp(i, "/..") == 0) {
    if (strcmp(i, "/..") == 0) i[1] = '\0'; else i += 3;
    while (o > 0 && out[o - 1] != '/') o--;      /* via l'ultimo segmento */
    if (o > 0) o--;  out[o] = '\0'; }
  • uri_resolve. Schema nel riferimento: assoluto. Autorità nel riferimento: base solo per lo schema. Percorso vuoto: percorso della base (e query della base se il riferimento non ne ha). Percorso che inizia per /: quello. Altrimenti merge = base fino all'ultima / + riferimento, poi remove_dot_segments. Esempi: g -> http://a/b/c/g; ?y -> http://a/b/c/d;p?y; #s -> http://a/b/c/d;p?q#s; //g -> http://g; ../../g -> http://a/g; g?y/./x lascia intatti i punti nella query.
  • Prova. gcc -Wall -Wextra -o uri_demo uri_demo.c && ./uri_demo -> tutti i test passati (comprese 41 risoluzioni della RFC 3986); confronto con urllib.parse.urljoin.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static int uri_split(const char *s, struct uri *u)
{
    memset(u, 0, sizeof *u);
    const char *p = s;
    size_t k = strcspn(p, ":/?#");
    if (p[k] == ':' && k > 0 && isalpha((unsigned char)p[0]) && k < sizeof u->scheme) {
        memcpy(u->scheme, p, k);
        u->has_scheme = 1;
        p += k + 1;
    }
    if (p[0] == '/' && p[1] == '/') {
        p += 2;
        size_t a = strcspn(p, "/?#");
        char auth[256];
        if (a >= sizeof auth)
            return -1;
        memcpy(auth, p, a);
        auth[a] = '\0';
        u->has_authority = 1;
        p += a;
        char *at = strrchr(auth, '@');
        char *hostpart = auth;
        if (at) {
            *at = '\0';
            snprintf(u->userinfo, sizeof u->userinfo, "%s", auth);
            hostpart = at + 1;
        }
        char *colon = strrchr(hostpart, ':');
        if (colon && !(hostpart[0] == '[' && strchr(colon, ']'))) {
            snprintf(u->port, sizeof u->port, "%s", colon + 1);
            *colon = '\0';
        }
        snprintf(u->host, sizeof u->host, "%s", hostpart);
    }
    size_t pl = strcspn(p, "?#");
    if (pl >= sizeof u->path)
        return -1;
    memcpy(u->path, p, pl);
    p += pl;
    if (*p == '?') {
        size_t q = strcspn(++p, "#");
        if (q >= sizeof u->query)
            return -1;
        memcpy(u->query, p, q);
        u->has_query = 1;
        p += q;
    }
    if (*p == '#') {
        snprintf(u->fragment, sizeof u->fragment, "%s", p + 1);
        u->has_fragment = 1;
    }
    return 0;
}

static int percent_encode(const char *s, char *out, size_t size)
{
    size_t o = 0;
    for (; *s; s++) {
        unsigned char c = (unsigned char)*s;
        if (isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') {
            if (o + 2 > size) return -1;
            out[o++] = (char)c;
        } else {
            if (o + 4 > size) return -1;
            o += (size_t)sprintf(out + o, "%%%02X", c);
        }
    }
    out[o] = '\0';
    return 0;
}

static int percent_decode(const char *s, char *out, size_t size)
{
    size_t o = 0;
    for (; *s; s++) {
        int c = (unsigned char)*s;
        if (c == '%') {
            int h = hexval((unsigned char)s[1]);
            int l = (h >= 0) ? hexval((unsigned char)s[2]) : -1;
            if (h < 0 || l < 0) return -1;
            c = h * 16 + l;
            s += 2;
        }
        if (o + 1 >= size) return -1;
        out[o++] = (char)c;
    }
    out[o] = '\0';
    return 0;
}
  • Errori tipici: assente e vuoto confusi; schema cercato senza controllare che : preceda /; primo : dell'autorità preso per la porta (IPv6); unsigned char dimenticato nella codifica; + come spazio nel percorso o doppia decodifica; punti rimossi dopo l'apertura del file; merge sulla base intera invece che fino all'ultima /.

Teoria collegata