En los títulos y los textos vais a encontrar unas cuantas citaciones cinematográficas (y si, soy un cinéfilo). Si no os interesan podéis fingir no verlas, ya que no son fundamentales para la comprensión de los post...

Este blog es la versión en Español de mi blog en Italiano L'arte della programmazione in C. Espero que mis traducciones sean comprensibles...

viernes, 3 de junio de 2016

Sprintf Driver
por qué no hay que usar la sprintf en C

"¿Hablas conmigo? ¿Me lo dices a mí?... Dime, ¿Es a mí?". Sí, al igual que el gran De Niro en Taxi Driver, esa fue mi reacción (incrédula) cuando descubrí (hace muchos años, ahora) que, después de años y años de uso honrado, tendría que dejar de utilizar la sprintf() .
...Y tu me dices de no utilizar la sprintf? A mí?...
Bueno, en realidad, si la usas bien, y tienes el 100% de control sobre el código escrito, también puede utilizarla sin grandes problemas, pero, como dicen los ingleses, la sprintf() es error prone, fácilmente te lleva a cometer errores, incluso graves. El problema más grave y evidente con la sprintf() se llama buffer overflow, y no creo que sea necesario gastar muchas palabras en eso: si el búfer que pasamos como primer argumento no es del tamaño correcto el desastre está detrás de la esquina.

Afortunadamente viene en nuestro auxilio la snprintf(), que es de la misma familia, pero más segura. Vemos los dos prototipos en comparación:
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
La snprintf() nos obliga a poner el size del buffer como segundo argumento, entonces es muy fácil coger la costumbre de escribir de una manera error-free como esta:
char buffer[32];
snprintf(buffer, sizeof(buffer), "Hello world!");
Si en lugar de "Hello world!" hubiéramos escrito una cadena de más de 32 chars, ningún problema: la snprintf() trunca la cadena de manera adecuada y estamos a salvo.
Y ahora os propongo un pequeño ejemplo real: tomamos una nuestra vieja conocida escrita para un viejo post, la getDateUsec() y la vamos a escribir en dos versiones, una buena y otra mala (bad). Veamos:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>

// prototipos locales
char *getDateUsec(char *dest, size_t size);
char *badGetDateUsec(char *dest);

// función main
int main(int argc, char* argv[])
{
    // llama getDateUsec (o badGetDateUsec) y escribe el resultado
    char dest[12];
    printf("fecha con usec: %s\n", getDateUsec(dest, sizeof(dest)));
    //printf("fecha con usec: %s\n", badGetDateUsec(dest));

    return EXIT_SUCCESS;
}

// getDateUsec() - Genera una cadena con fecha y hora (usa los microsegundos)
char *getDateUsec(char *dest, size_t size)
{
    // get time (con gettimeofday()+localtime() en lugar de time()+localtime() para obtener los usec)
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format cadena destinación dest(debe ser alocada por el llamante) y añade los usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    snprintf(dest, size, fmt, tv.tv_usec);

    // return cadena destinacion dest
    return dest;
}

// badGetDateUsec() - Genera una cadena con fecha y hora (usa los microsegundos) (versione bad)
char *badGetDateUsec(char *dest)
{
    /// get time (con gettimeofday()+localtime() en lugar de time()+localtime() para obtener los usec)
    struct timeval tv;
    gettimeofday(&tv, NULL);
    struct tm *tmp = localtime(&tv.tv_sec);

    // format cadena destinación dest(debe ser alocada por el llamante) y añade los usec
    char fmt[128];
    strftime(fmt, sizeof(fmt), "%Y-%m-%d %H:%M:%S.%%06u", tmp);
    sprintf(dest, fmt, tv.tv_usec);

    // return cadena destinacion dest
    return dest;
}
Vale, en aquel entonces, para simplificar, había escrito una getDateUsec() que era, de hecho, una badGetDateUsec() (y más tarde, por la precisión, procedí en modificarla en el post). Esa versión funcionaba, pero podía crear problemas, mientras que la nueva versión es mucho más segura. Intentad compilar el ejemplo, adonde, deliberadamente, he subdimensionado el buffer de destino: comentando badGetDateUsec() y usando la getDateUsec(), funciona perfectamente, truncando el output a 12 chars. Si, sin embargo, se comenta la getDateUsec() y se utiliza la badGetDateUsec() el programa peta mientras se ejecuta. !Inténtelo!

Y ya que estamos en el argumento sprintf() un pequeño consejo un poco OT: si necesitáis agregar secuencialmente una cadena (en un bucle, por ejemplo) sobre una cadena base (para redactar un texto, por ejemplo) no hacerlo nunca así:
char buf[256] = "";
for (int i = 0; i < 5; i++)
    sprintf(buf, "%s añadido a la cadena %d\n", buf, i);
el método aquí arriba parece funcionar, pero, en realidad, funciona cuando le da la gana. Hacerlo, en cambio, así:
char buf[256] = "";
for (int i = 0; i < 5; i++) {
    char tmpbuf[256];
    sprintf(tmpbuf, "%s añadido a la cadena %d\n", buf, i);
    sprintf(buf, "%s", tmpbuf);
}
Y si no me creéis probad a verificar el código con un lint como cppchek (que siempre es una buena idea) o consultad el manual de la 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).
Y, por supuesto, también en este último ejemplo (hecho, por simplicidad, con la sprintf ()) seria recomendable usar la snprintf ().

¡Hasta el próximo post!