Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

venerdì 3 giugno 2016

Sprintf Driver
perché non bisogna usare la sprintf in C

"Ma dici a me? Ma dici a me?... Ma dici a me?...". Si, proprio come il grande De Niro in Taxi Driver, questa è stata la mia reazione (incredula) quando ho scoperto (molti anni fa, oramai) che, dopo anni e anni di onorato uso, avrei dovuto smettere di usare la sprintf().
...e tu dici a me di non usare più la sprintf? A me?...
Beh, in effetti se la usi bene e hai il 100% di controllo sul codice scritto puoi anche usarla senza grossi problemi ma, come dicono gli inglesi, la sprintf() è error prone, induce facilmente a errori, anche gravi. Il problema più grave ed evidente con la sprintf() si chiama buffer overflow, e non credo che sia necessario spenderci molte parole: se il buffer che passiamo come primo argomento non è correttamente dimensionato il disastro è dietro l'angolo.

Fortunatamente ci viene in aiuto la snprintf(), che è della stessa famiglia ma molto più sicura. Vediamo i due prototipi a confronto:
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
La snprintf() ci obbliga a mettere il size del buffer come secondo argomento, per cui è facilissimo prendere l'abitudine a scrivere in un modo error-free come questo:
char buffer[32];
snprintf(buffer, sizeof(buffer), "Hello world!");
Se al posto di "Hello world!" avessimo scritto una stringa di più di 32 chars, nessun problema: la snprintf() la tronca opportunamente e siamo salvi.

E adesso vi propongo un piccolo esempio reale: prendiamo una nostra vecchia conoscenza scritta per un vecchio post, la getDateUsec() e la scriviamo in due versioni, una buona e una cattiva (bad). Vediamo:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>

// prototipi locali
char *getDateUsec(char *dest, size_t size);
char *badGetDateUsec(char *dest);

// funzione main
int main(int argc, char* argv[])
{
    // chiama getDateUsec (o badGetDateUsec) e scrive il risultato
    char dest[12];
    printf("date con usec: %s\n", getDateUsec(dest, sizeof(dest)));
    //printf("date con usec: %s\n", badGetDateUsec(dest));

    return EXIT_SUCCESS;
}

// getDateUsec() - Genera una stringa con data e ora (usa i microsecondi)
char *getDateUsec(char *dest, size_t size)
{
    // get time (con gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec)
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format stringa destinazione dest(deve essere allocata dal chiamante) e aggiunge i usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    snprintf(dest, size, fmt, tv.tv_usec);

    // return stringa destinazione dest
    return dest;
}

// badGetDateUsec() - Genera una stringa con data e ora (usa i microsecondi) (versione bad)
char *badGetDateUsec(char *dest)
{
    // get time (con gettimeofday()+localtime() invece di time()+localtime() per ottenere i usec)
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format stringa destinazione dest(deve essere allocata dal chiamante) e aggiunge i usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    sprintf(dest, fmt, tv.tv_usec);

    // return stringa destinazione dest
    return dest;
}
Ecco, a suo tempo, per semplificare, avevo scritto una getDateUsec() che era, in realtà, una badGetDateUsec() (e in seguito, per precisione, ho provveduto a modificarla anche sul post). Quella versione funzionava ma poteva creare problemi, mentre la nuova versione è molto più sicura. Provate a compilare l'esempio, dove, volutamente, ho sottodimensionato il buffer di destinazione: commentando la badGetDateUsec() e usando la getDateUsec(), funziona perfettamente, troncando l'output a 12 chars. Se, invece, commentate la getDateUsec() e usate la badGetDateUsec() il programma si schianta durante l'esecuzione. Provare per credere!

E già che siamo in argomento sprintf() un piccolo consiglio un po' OT: se dovete aggiungere sequenzialmente delle stringhe (in un loop, ad esempio) su una stringa base (per comporre un testo, ad esempio) non fate mai cosi:
char buf[256] = "";
for (int i = 0; i < 5; i++)
    sprintf(buf, "%s aggiunto alla stringa %d\n", buf, i);
il metodo qui sopra sembra funzionare, ma, in realtà, funziona quando c'ha voglia lui. Fate invece così:
char buf[256] = "";
for (int i = 0; i < 5; i++) {
    char tmpbuf[256];
    sprintf(tmpbuf, "%s aggiunto alla stringa %d\n", buf, i);
    sprintf(buf, "%s", tmpbuf);
}
E se non ci credete provate a passare il codice con un lint tipo cppchek (che è sempre una buona idea) o consultate il manuale della sprintf():
C99 and POSIX.1-2001 specify that the results are undefined if  a  call
to  sprintf(), snprintf(), vsprintf(), or vsnprintf() would cause copy‐
ing to take place between objects that overlap  (e.g.,  if  the  target
string  array and one of the supplied input arguments refer to the same
buffer).
E, ovviamente, anche in quest'ultimo esempio (fatto per semplicità con la sprintf()) sarebbe raccomandabile usare la snprintf().

Ciao e al prossimo post!