Salta al contenuto
Note per Studenti Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1)

Esercizio - Client HTTP 1.1 con decodifica del chunked (slide HTTP 1.1)

In questa pagina 7

Testo (slide HTTP 1.1 del corso di Reti di Calcolatori, Ing. Informatica UniPD: «Client HTTP/1.1: Chunked»).

Scrivere un client HTTP/1.1 che gestisca le risposte con Transfer-Encoding: chunked:

  1. leggere i chunk (dimensione in esadecimale, dati, CRLF) e scrivere nel file o su stdout solo i dati, senza le righe con le dimensioni;
  2. fermarsi al chunk di lunghezza zero e leggere gli eventuali trailer fino alla riga vuota;
  3. funzionare anche quando i dati di un chunk contengono CRLF o byte zero, quando la dimensione ha cifre maiuscole o è seguita da estensioni (1A;nome=valore);
  4. gestire, con lo stesso programma, anche le risposte con Content-Length e quelle terminate dalla chiusura della connessione (priorità: Transfer-Encoding batte Content-Length, RFC 9112 par. 6.3).

Teoria: HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encodingHTTP/1.1 (oggi RFC 9110 e 9112) rende la connessione persistente di default (si chiude solo con "Connection: close"), rende obbligatorio l'header Host (virtual hosting) e introduce i nuovi metodi PUT, DELETE, OPTIONS, TRACE, Expect: 100-continue, richieste di intervalli (206) e Transfer-Encoding: chunked; con la connessione persistente il client deve sapere dove finisce ogni risposta: lunghezza del corpo nell'ordine HEAD/1xx/204/304 senza corpo, Transfer-Encoding chunked, Content-Length, altrimenti fino alla chiusura; il chunked divide il corpo in blocchi preceduti dalla lunghezza in esadecimale, termina con un chunk 0 e un trailer facoltativo, e si decodifica contando i byte dichiarati (non cercando CRLF).HTTP 1.1 - connessioni persistenti, Content-Length e chunked transfer encoding →, Esercizio - Client HTTP 1.1 con Content-Length e connessione persistente (slide HTTP 1.1), 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

Il formato (RFC 9112 par. 7.1):

chunked-body = *chunk  last-chunk  trailer-section  CRLF
chunk        = chunk-size [ chunk-ext ] CRLF  chunk-data  CRLF       chunk-size = cifre ESADECIMALI
last-chunk   = 1*("0") [ chunk-ext ] CRLF

Per decodificarlo bisogna leggere in due modi diversi dalla stessa connessione:

  • le righe della dimensione e dei trailer si leggono a righe (fino al CRLF);
  • i dati di un chunk si leggono a byte, in numero esatto (chunk-size), senza interpretarli.

Per questo il client usa un lettore con buffer (struct rbuf) con tre operazioni sullo stesso buffer: rb_line (una riga), rb_read (fino a n byte già disponibili) e rb_fill (rilegge dal socket se il buffer è vuoto). Se le righe fossero lette con una read diretta e i dati con un'altra, i byte già letti nel buffer andrebbero persi.

L'algoritmo di read_chunked:

ciclo:
    riga = rb_line()                       es. "1A;nome=valore"
    size = strtoul(riga, base 16)          si ferma alla ';': l'estensione si ignora
    se size == 0: esci dal ciclo           ultimo chunk
    copia ESATTAMENTE size byte nell'uscita (anche se contengono CRLF)
    rb_line() deve restituire la riga vuota (il CRLF dopo i dati)
poi: righe di trailer fino alla riga vuota

Il punto debole del codice delle slide. Il codice delle slide legge un byte alla volta e riconosce il confine fra dimensione e dati cercando il prossimo CRLF, senza usare size per contare. Va bene per una risposta i cui dati non contengono mai CRLF, ma si rompe appena un chunk contiene un a-capo \r\n (una pagina HTML con righe lo contiene): quel CRLF viene preso per la fine del chunk e la riga successiva, che è testo, viene letta come dimensione. Qui il chunk 1 contiene due CRLF proprio per mostrarlo.

Codice

c
/* client11_chunked.c - client HTTP/1.1 che decodifica Transfer-Encoding: chunked (e anche Content-Length
 * e corpo chiuso dalla connessione). Il corpo decodificato va su stdout o nel file indicato.
 *
 * Compilare:  gcc -Wall -Wextra -o client11_chunked client11_chunked.c
 * Usare:      ./client11_chunked host porta percorso [file_uscita]
 * Diagnostica (numero e dimensione dei chunk, trailer) su stderr.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

#define MAX_LINE 4096
#define MAX_CHUNK (64L * 1024 * 1024)               /* rifiuta chunk assurdi (RFC 9112 7.1: attenzione agli overflow) */

/* ---------- lettore con buffer: righe E byte grezzi dallo stesso buffer ---------- */
struct rbuf {
    int fd;
    char buf[4096];
    size_t pos, len;
};

/* Riempie il buffer se e' vuoto. Torna 0 se ci sono byte disponibili, -1 su EOF/errore. */
static int rb_fill(struct rbuf *r)
{
    while (r->pos == r->len) {
        ssize_t k = read(r->fd, r->buf, sizeof r->buf);
        if (k < 0 && errno == EINTR)
            continue;
        if (k <= 0)
            return -1;
        r->pos = 0;
        r->len = (size_t)k;
    }
    return 0;
}

/* Come fgets: una riga con CRLF o LF tolti. Torna la lunghezza, -1 su EOF o riga troppo lunga. */
static int rb_line(struct rbuf *r, char *line, size_t max)
{
    size_t n = 0;
    for (;;) {
        if (rb_fill(r) < 0)
            return -1;
        char c = r->buf[r->pos++];
        if (c == '\n') {
            if (n > 0 && line[n - 1] == '\r')
                n--;
            line[n] = '\0';
            return (int)n;
        }
        if (n + 1 >= max)
            return -1;
        line[n++] = c;
    }
}

/* Copia fino a n byte (quelli disponibili ora) dal buffer a dst. Torna quanti, -1 su EOF. */
static ssize_t rb_read(struct rbuf *r, char *dst, size_t n)
{
    if (rb_fill(r) < 0)
        return -1;
    size_t avail = r->len - r->pos;
    if (n > avail)
        n = avail;
    memcpy(dst, r->buf + r->pos, n);
    r->pos += n;
    return (ssize_t)n;
}

/* ---------- rete ---------- */
static int connect_to(const char *host, const char *port)
{
    struct addrinfo hints, *res, *p;
    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    if (getaddrinfo(host, port, &hints, &res) != 0)
        return -1;
    int fd = -1;
    for (p = res; p != NULL; p = p->ai_next) {
        fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (fd < 0)
            continue;
        if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
            break;
        close(fd);
        fd = -1;
    }
    freeaddrinfo(res);
    return fd;
}

static int write_all(int fd, const char *buf, size_t n)
{
    while (n > 0) {
        ssize_t w = write(fd, buf, n);
        if (w < 0) {
            if (errno == EINTR)
                continue;
            return -1;
        }
        buf += w;
        n -= (size_t)w;
    }
    return 0;
}

/* ---------- corpo ---------- */

/* Legge un corpo chunked e scrive i dati (senza le lunghezze) in out. Torna i byte di dati, -1 su errore.
 *   chunked-body = *chunk last-chunk trailer-section CRLF
 *   chunk        = chunk-size [ chunk-ext ] CRLF chunk-data CRLF       (chunk-size in ESADECIMALE) */
static long read_chunked(struct rbuf *r, FILE *out)
{
    char line[MAX_LINE], buf[4096];
    long total = 0;
    int nchunks = 0;
    for (;;) {
        if (rb_line(r, line, sizeof line) < 0)
            return -1;
        /* "1a;nome=valore": le estensioni dopo ';' si ignorano. strtoul legge solo la parte esadecimale. */
        char *end;
        errno = 0;
        unsigned long size = strtoul(line, &end, 16);
        if (end == line || errno == ERANGE || size > (unsigned long)MAX_CHUNK ||
            !(*end == '\0' || *end == ';' || *end == ' ' || *end == '\t')) {
            fprintf(stderr, "chunk-size non valido: '%s'\n", line);
            return -1;
        }
        if (size == 0)
            break;                                  /* last-chunk: finiscono i dati */
        nchunks++;
        fprintf(stderr, "chunk %d: %lu byte\n", nchunks, size);

        /* chunk-data: ESATTAMENTE 'size' byte, che possono contenere anche CRLF o zeri: non vanno interpretati */
        size_t left = size;
        while (left > 0) {
            ssize_t k = rb_read(r, buf, left < sizeof buf ? left : sizeof buf);
            if (k < 0)
                return -1;
            fwrite(buf, 1, (size_t)k, out);
            left -= (size_t)k;
        }
        total += (long)size;

        /* CRLF che chiude il chunk */
        if (rb_line(r, line, sizeof line) < 0 || line[0] != '\0') {
            fprintf(stderr, "manca il CRLF dopo il chunk\n");
            return -1;
        }
    }
    /* trailer-section: righe "Nome: valore" fino alla riga vuota (di solito non ce ne sono) */
    for (;;) {
        if (rb_line(r, line, sizeof line) < 0)
            return -1;
        if (line[0] == '\0')
            break;
        fprintf(stderr, "trailer: %s\n", line);
    }
    return total;
}

int main(int argc, char **argv)
{
    if (argc < 4 || argc > 5) {
        fprintf(stderr, "uso: %s host porta percorso [file_uscita]\n", argv[0]);
        return 1;
    }
    int fd = connect_to(argv[1], argv[2]);
    if (fd < 0) {
        fprintf(stderr, "connessione fallita\n");
        return 1;
    }
    char req[2048];
    int n = snprintf(req, sizeof req,
                     "GET %s HTTP/1.1\r\nHost: %s:%s\r\nUser-Agent: client11_chunked/1.0\r\nConnection: close\r\n\r\n",
                     argv[3], argv[1], argv[2]);
    if (write_all(fd, req, (size_t)n) < 0) {
        perror("write");
        return 1;
    }

    struct rbuf rb = {.fd = fd};
    char line[MAX_LINE];

    /* status-line */
    int major, minor, status;
    if (rb_line(&rb, line, sizeof line) < 0 || sscanf(line, "HTTP/%d.%d %d", &major, &minor, &status) != 3) {
        fprintf(stderr, "risposta non valida\n");
        return 1;
    }
    fprintf(stderr, "stato %d\n", status);

    /* header: ci servono Transfer-Encoding e Content-Length */
    int chunked = 0;
    long content_length = -1;
    for (;;) {
        int len = rb_line(&rb, line, sizeof line);
        if (len < 0) {
            fprintf(stderr, "risposta interrotta\n");
            return 1;
        }
        if (len == 0)
            break;
        char *colon = strchr(line, ':');
        if (colon == NULL)
            continue;
        *colon = '\0';
        const char *v = colon + 1;
        while (*v == ' ' || *v == '\t')
            v++;
        if (strcasecmp(line, "Transfer-Encoding") == 0 && strstr(v, "chunked") != NULL)
            chunked = 1;
        else if (strcasecmp(line, "Content-Length") == 0)
            content_length = atol(v);
    }

    FILE *out = (argc == 5) ? fopen(argv[4], "wb") : stdout;
    if (out == NULL) {
        perror("fopen");
        return 1;
    }

    /* RFC 9112 par. 6.3: se c'e' Transfer-Encoding chunked, ha la precedenza su Content-Length */
    long total;
    if (chunked) {
        total = read_chunked(&rb, out);
    } else if (content_length >= 0) {
        total = 0;
        char buf[4096];
        while (total < content_length) {
            long want = content_length - total;
            ssize_t k = rb_read(&rb, buf, (size_t)want < sizeof buf ? (size_t)want : sizeof buf);
            if (k < 0) {
                total = -1;
                break;
            }
            fwrite(buf, 1, (size_t)k, out);
            total += k;
        }
    } else {
        total = 0;                                  /* corpo chiuso dalla connessione */
        char buf[4096];
        ssize_t k;
        while ((k = rb_read(&rb, buf, sizeof buf)) > 0) {
            fwrite(buf, 1, (size_t)k, out);
            total += k;
        }
    }
    if (out != stdout)
        fclose(out);
    close(fd);
    if (total < 0) {
        fprintf(stderr, "corpo incompleto o malformato\n");
        return 1;
    }
    fprintf(stderr, "[%s, %ld byte di dati]\n", chunked ? "chunked" : (content_length >= 0 ? "Content-Length" : "fino alla chiusura"), total);
    return 0;
}

Server di prova

Per provare il client senza internet serve un server che risponda con un corpo chunked "difficile": questo piccolo programma Python invia, a qualunque richiesta, tre chunk (il primo con due CRLF nei dati e la dimensione in maiuscolo con un'estensione) e un trailer.

python
"""chunked_test_server.py - server di prova che risponde SEMPRE con un corpo chunked "difficile":
un chunk con dentro CRLF, una estensione di chunk, la dimensione in esadecimale maiuscolo e un trailer.
Avvio:  python3 chunked_test_server.py 8001
"""
import socketserver
import sys

# i dati dei tre chunk: il primo contiene due CRLF (una pagina HTML con le righe a-capo li ha)
C1 = b"Prima riga\r\nseconda riga\r\n"      # 26 byte = 0x1A
C2 = b"0123456789ABCDEF"                     # 16 byte = 0x10
C3 = b"fine"                                 # 4 byte

BODY = (
    b"1A;nome=valore\r\n" + C1 + b"\r\n"     # dimensione maiuscola + estensione
    + b"10\r\n" + C2 + b"\r\n"
    + b"4\r\n" + C3 + b"\r\n"
    + b"0\r\nX-Totale: 46\r\n\r\n"           # chunk finale + trailer + riga vuota
)
HEAD = (
    b"HTTP/1.1 200 OK\r\n"
    b"Content-Type: text/plain\r\n"
    b"Transfer-Encoding: chunked\r\n"
    b"Connection: close\r\n"
    b"\r\n"
)


class Handler(socketserver.StreamRequestHandler):
    def handle(self):
        while self.rfile.readline().strip():     # legge e scarta la richiesta fino alla riga vuota
            pass
        self.wfile.write(HEAD + BODY)


if __name__ == "__main__":
    socketserver.TCPServer.allow_reuse_address = True
    with socketserver.TCPServer(("127.0.0.1", int(sys.argv[1])), Handler) as srv:
        srv.serve_forever()

Compilare e provare

$ gcc -Wall -Wextra -o client11_chunked client11_chunked.c
$ python3 chunked_test_server.py 8001 &
$ ./client11_chunked 127.0.0.1 8001 / risposta.txt
stato 200
chunk 1: 26 byte
chunk 2: 16 byte
chunk 3: 4 byte
trailer: X-Totale: 46
[chunked, 46 byte di dati]
$ od -c risposta.txt | head -3
0000000   P   r   i   m   a       r   i   g   a  \r  \n   s   e   c   o
0000020   n   d   a       r   i   g   a  \r  \n   0   1   2   3   4   5
0000040   6   7   8   9   A   B   C   D   E   F   f   i   n   e

Il file contiene i 46 byte di dati (26 + 16 + 4), con i due CRLF del primo chunk intatti e senza le righe 1A;nome=valore, 10, 4. Altre prove:

Risposte volutamente sbagliate (da provare con nc -l o con il server Python modificato): un chunk troncato (5\r\nabc e poi chiusura) produce corpo incompleto o malformato; una dimensione non esadecimale (ZZ) produce chunk-size non valido: 'ZZ'.

Spiegazione dei punti chiave

rb_fill, rb_line, rb_read.

  • rb_fill: se il buffer è vuoto (pos == len) fa una read da 4 KiB (riprova su EINTR); 0 byte o errore → -1.
  • rb_line: prende byte dal buffer fino al \n, toglie un \r che lo precede; protetta contro righe troppo lunghe (-1).
  • rb_read(r, dst, n): restituisce fino a n byte, ma solo quelli già nel buffer (riempie solo se è vuoto): chi chiama ripete finché ne ha ricevuti n.

read_chunked.

  • strtoul(line, &end, 16): legge cifre esadecimali (minuscole o maiuscole) e si ferma al primo carattere non valido. Poi si controlla che sia \0, ;, spazio o tabulazione: 1A;nome=valore è valido, 1AZ no. errno == ERANGE e il limite MAX_CHUNK proteggono da dimensioni enormi (RFC 9112 par. 7.1: il ricevente deve prevedere numeri esadecimali molto grandi e non farli andare in overflow).
  • size == 0 è l'ultimo chunk: si esce dal ciclo.
  • I dati: while (left > 0) { k = rb_read(r, buf, min(left, sizeof buf)); fwrite(buf, 1, k, out); left -= k; }. fwrite con la lunghezza (non fputs) perché i dati sono binari.
  • Dopo i dati ci deve essere un CRLF: rb_line deve restituire la riga vuota; altrimenti la risposta è malformata (-1).
  • Dopo l'ultimo chunk, le righe di trailer (Nome: valore) fino alla riga vuota. Il client le stampa (un hash o un conteggio calcolati dal server durante l'invio).

Ordine di priorità (nel main). Se chunked, si usa read_chunked anche se c'è Content-Length (prova: il server Python manda solo il chunked, ma una risposta con entrambi viene letta come chunked); altrimenti con Content-Length si leggono quei byte; altrimenti fino alla chiusura.

Richiesta con Connection: close. Qui è comoda: dopo il corpo il server chiude e il client termina in ogni caso. Con una connessione persistente basta non chiudere e riusare il lettore rbuf (il buffer può contenere già l'inizio della risposta successiva).

Che cosa non fa. Non gestisce la decompressione (Content-Encoding: gzip), non riusa la connessione, non verifica che il trailer corrisponda a quanto annunciato in Trailer.

Errori tipici

  • Cercare CRLF per trovare la fine di un chunk invece di contare chunk-size byte: si rompe con dati che contengono CRLF.
  • Convertire la dimensione come decimale (atoi, %d) invece che esadecimale (b = 11, 14 = 20, 1A = 26).
  • Includere le righe con le dimensioni nel contenuto.
  • Dimenticare il CRLF dopo i dati o la riga vuota finale: il client legge un byte in più, o resta in attesa.
  • Non tollerare estensioni (;nome=valore) o maiuscole nella dimensione.
  • Ignorare il trailer: il parser interpreta le sue righe come l'inizio della risposta successiva (connessione persistente).
  • Usare fputs/strlen sui dati.
  • Convertire dimensioni con int senza controlli: una dimensione molto lunga provoca overflow.
  • Leggere i dati con una read diretta dopo aver letto le righe con il buffer (i byte nel buffer vanno persi).

Varianti per esercitarsi

Versione ripasso

  • Testo. Client HTTP/1.1 che decodifica Transfer-Encoding: chunked (solo dati nell'uscita), si ferma al chunk 0, legge i trailer; funziona con CRLF nei dati, dimensioni maiuscole, estensioni; gestisce anche Content-Length e corpo fino alla chiusura (Transfer-Encoding ha la precedenza).
  • Formato. chunk-size (esadecimale) [;ext] CRLF, chunk-size byte di dati, CRLF; ripetuto; 0 [;ext] CRLF; trailer (Nome: valore); riga vuota.
  • Lettore con buffer. rb_fill (una read da 4 KiB se vuoto), rb_line (riga senza CRLF), rb_read(n) (fino a n byte già disponibili): righe e dati dallo stesso buffer.
c
for (;;) {
    rb_line(r, line, sizeof line);                       /* "1A;nome=valore" */
    unsigned long size = strtoul(line, &end, 16);        /* base 16, si ferma a ';' */
    if (end == line || errno == ERANGE || size > MAX_CHUNK) return -1;
    if (size == 0) break;                                /* ultimo chunk */
    for (left = size; left > 0; left -= k) { k = rb_read(r, buf, min(left, sizeof buf)); fwrite(buf, 1, k, out); }
    rb_line(r, line, sizeof line);                       /* CRLF dopo i dati: riga vuota */
}
while (rb_line(r, line, sizeof line) > 0) puts(line);    /* trailer fino alla riga vuota */
  • Controlli. Dopo strtoul: \0, ;, spazio o tab; errno == ERANGE e MAX_CHUNK contro l'overflow; riga dopo i dati vuota; fwrite con la lunghezza (dati binari).
  • Punto debole delle slide. Riconoscere il fine chunk cercando CRLF (senza contare size) si rompe con dati che contengono CRLF.
  • Prove. python3 chunked_test_server.py 8001; ./client11_chunked 127.0.0.1 8001 / risposta.txt -> chunk 1: 26 byte, chunk 2: 16, chunk 3: 4, trailer: X-Totale: 46, [chunked, 46 byte di dati]; file di 46 byte con i due CRLF intatti. 5\r\nabc + chiusura -> corpo incompleto o malformato; ZZ -> chunk-size non valido.
  • Codice essenziale (le funzioni centrali, senza commenti):
c
static int rb_fill(struct rbuf *r)
{
    while (r->pos == r->len) {
        ssize_t k = read(r->fd, r->buf, sizeof r->buf);
        if (k < 0 && errno == EINTR)
            continue;
        if (k <= 0)
            return -1;
        r->pos = 0;
        r->len = (size_t)k;
    }
    return 0;
}

static int rb_line(struct rbuf *r, char *line, size_t max)
{
    size_t n = 0;
    for (;;) {
        if (rb_fill(r) < 0)
            return -1;
        char c = r->buf[r->pos++];
        if (c == '\n') {
            if (n > 0 && line[n - 1] == '\r')
                n--;
            line[n] = '\0';
            return (int)n;
        }
        if (n + 1 >= max)
            return -1;
        line[n++] = c;
    }
}

static ssize_t rb_read(struct rbuf *r, char *dst, size_t n)
{
    if (rb_fill(r) < 0)
        return -1;
    size_t avail = r->len - r->pos;
    if (n > avail)
        n = avail;
    memcpy(dst, r->buf + r->pos, n);
    r->pos += n;
    return (ssize_t)n;
}

static long read_chunked(struct rbuf *r, FILE *out)
{
    char line[MAX_LINE], buf[4096];
    long total = 0;
    int nchunks = 0;
    for (;;) {
        if (rb_line(r, line, sizeof line) < 0)
            return -1;
        char *end;
        errno = 0;
        unsigned long size = strtoul(line, &end, 16);
        if (end == line || errno == ERANGE || size > (unsigned long)MAX_CHUNK ||
            !(*end == '\0' || *end == ';' || *end == ' ' || *end == '\t')) {
            fprintf(stderr, "chunk-size non valido: '%s'\n", line);
            return -1;
        }
        if (size == 0)
            break;
        nchunks++;
        fprintf(stderr, "chunk %d: %lu byte\n", nchunks, size);
        size_t left = size;
        while (left > 0) {
            ssize_t k = rb_read(r, buf, left < sizeof buf ? left : sizeof buf);
            if (k < 0)
                return -1;
            fwrite(buf, 1, (size_t)k, out);
            left -= (size_t)k;
        }
        total += (long)size;
        if (rb_line(r, line, sizeof line) < 0 || line[0] != '\0') {
            fprintf(stderr, "manca il CRLF dopo il chunk\n");
            return -1;
        }
    }
    for (;;) {
        if (rb_line(r, line, sizeof line) < 0)
            return -1;
        if (line[0] == '\0')
            break;
        fprintf(stderr, "trailer: %s\n", line);
    }
    return total;
}
  • Errori tipici: fine chunk cercata con CRLF; dimensione letta in decimale; righe delle dimensioni nel contenuto; manca il CRLF dopo i dati o la riga vuota finale; estensioni o maiuscole rifiutate; trailer ignorato; fputs/strlen sui dati; overflow della dimensione; read diretta mescolata al buffer.

Esercizi su questo argomento

Teoria collegata