¿Está fgets () devolviendo NULL con un corto buffer compatible?

En la prueba unitaria una función que contiene fgets() , encontró un resultado inesperado cuando el tamaño del búfer n < 2 . Obviamente, ese tamaño de búfer es tonto, pero la prueba es explorar casos de esquina.

Código simplificado:

 #include  #include  void test_fgets(char * restrict s, int n) { FILE *stream = stdin; s[0] = 42; printf(" errno:%d feof:%d ferror:%d retval:%ps[0]:%d\n\n", errno, feof(stream), ferror(stream), retval, s[0]); } int main(void) { char s[100]; test_fgets(s, sizeof s); // Entered "123\n" and works as expected test_fgets(s, 1); // fgets() --> NULL, feof() --> 0, ferror() --> 0 test_fgets(s, 0); // Same as above return 0; } 

Lo que es sorprendente es que fgets() devuelve NULL y ni feof() ni ferror() son 1 .

La especificación C, a continuación, parece silenciosa en este raro caso.

Preguntas:

  • ¿Está devolviendo NULL sin establecer el comportamiento conforme a feof() ni ferror() ?
  • ¿Podría un resultado diferente ser un comportamiento obediente?
  • ¿Hace una diferencia si n es 1 o menos que 1?

Plataforma: gcc versión 4.5.3 Target: i686-pc-cygwin

Aquí hay un resumen del Estándar C11, un poco de énfasis mío:

7.21.7.2 La función de los fgets

La función fgets lee como máximo uno menos que el número de caracteres especificado por n […]

La función fgets devuelve s si tiene éxito. Si se encuentra el final del archivo y no se han leído caracteres en la matriz, el contenido de la matriz permanece sin cambios y se devuelve un puntero nulo. Si se produce un error de lectura durante la operación, los contenidos de la matriz son indeterminados y se devuelve un puntero nulo.

Publicaciones relacionadas
Cómo usar feof y ferror para fgets (minishell en C)
Problemas para crear un shell en C (seg-Fault y ferror)
Preguntas de fputs (), fgets (), ferror () y equivalentes de C ++
Valor de retorno de fgets ()


[Editar] Comentarios sobre respuestas

@Shafik Yaghmour presentó bien el problema general: dado que la especificación C no menciona qué hacer cuando no lee ningún dato ni escribe ningún dato a s cuando ( n <= 0 ), es un Comportamiento Indefinido. Por lo tanto, cualquier respuesta razonable debería ser aceptable, como devolver NULL , no establecer indicadores, dejar el buffer solo.

En cuanto a lo que debería suceder cuando n==1 , la respuesta de @Oliver Matthews y el comentario de @Matt McNabb indican la falta de claridad de una especificación C considerando un buffer de n == 1 . La especificación C parece favorecer que un búfer de n == 1 devuelva el puntero del búfer con s[0] == '\0' , pero no es lo suficientemente explícito.

El comportamiento es diferente en versiones más nuevas de glibc , para n == 1 , devuelve s que indica éxito, esta no es una lectura irrazonable de 7.19.7.2 La función de los 7.19.7.2 párrafo 2 que dice ( es lo mismo en C99 y C11 , énfasis mío ):

char * fgets (char * restringe s, int n , FILE * restringe transmisión);

La función fgets lee a lo sumo uno menos que la cantidad de caracteres especificada por n desde la secuencia a la que apunta la secuencia en la matriz apuntada por s. No se leen caracteres adicionales después de un carácter de nueva línea (que se conserva) o después del final del archivo. Un carácter nulo se escribe inmediatamente después del último carácter leído en la matriz.

No es terriblemente útil, pero no infringe nada de lo que se dice en el estándar, leerá como máximo 0 caracteres y terminará nulo. Por lo tanto, los resultados que está viendo se ven como un error que se solucionó en versiones posteriores de glibc . También claramente no es un final de archivo ni un error de lectura como se trata en el párrafo 3 :

[…] Si se encuentra el final del archivo y no se han leído caracteres en la matriz, el contenido de la matriz permanece sin cambios y se devuelve un puntero nulo. Si se produce un error de lectura durante la operación, los contenidos de la matriz son indeterminados y se devuelve un puntero nulo.

En cuanto al caso final donde n == 0 parece que es simplemente un comportamiento indefinido. El borrador de la sección estándar C99 4. párrafo 2 de conformidad dice (el énfasis es mío ):

Si se infringe un requisito de ” debe ” o ” no se ” que aparece fuera de una restricción, el comportamiento no está definido. El comportamiento indefinido está indicado de otra manera en esta Norma Internacional por las palabras “comportamiento indefinido” u omisión de cualquier definición explícita de comportamiento . No hay diferencia en el énfasis entre estos tres; todos describen “comportamiento indefinido”.

La redacción es la misma en C11. Es imposible leer como máximo -1 caracteres y no es un final de archivo ni un error de lectura. Entonces no tenemos una definición explícita del comportamiento en este caso. Parece un defecto, pero no puedo encontrar ningún informe de defectos que lo cubra.

tl; dr: esa versión de glibc tiene un error para n = 1, la especificación tiene (podría decirse) una ambigüedad para n <1; pero creo que los glibc más nuevos toman la opción más sensata.

Entonces, la especificación c99 es básicamente la misma.

El comportamiento de test_fgets(s, 1) es incorrecto. glibc 2.19 da la salida correcta ( retval!=null , s[0]==null .

El comportamiento de test_fgets(s,0) no está definido, realmente. No fue exitoso (no se puede leer a lo sumo -1 caracteres), pero no da en ninguno de los dos criterios de “retorno nulo” (lectura EOF y 0, error de lectura).

Sin embargo, el comportamiento de GCC es posiblemente correcto (devolver el puntero a la s sin cambios también estaría bien) – feof no está configurado, porque no ha tocado eof; Ferror no está configurado porque no hubo un error de lectura.

Sospecho que la lógica en gcc (no tiene el origen a mano) tiene un ‘si n <= 0 return null' cerca de la parte superior.

[editar:]

Pensándolo bien, en realidad creo que el comportamiento de glibc para n=0 es la respuesta más correcta que podría dar:

  • No hay lectura, entonces feof()==0
  • No lee, por lo que no pudo haber ocurrido ningún error de lectura, entonces ferror=0

Ahora, en cuanto al valor de retorno, fgets no puede haber leído -1 caracteres (es imposible). Si fgets devolviera el puntero pasado, parecería una llamada exitosa. – Ignorando este caso de esquina, fgets se compromete a devolver una cadena terminada en nulo. Si no fuera así en este caso, no podría confiar en ello. Pero fgets configurará el personaje después de que el último carácter leído en la matriz sea nulo. dado que leemos en -1 caracteres (aparentemente) en esta llamada, ¿eso haría que establecer el 0º carácter sea nulo?

Entonces, la mejor opción es devolver null (en mi opinión).

El estándar C (borrador C11 n1570) especifica fgets() esta manera (algún énfasis mío):

7.21.7.2 La función de los fgets

Sinopsis

  #include  char *fgets(char * restrict s, int n, FILE * restrict stream); 

Descripción

La función fgets lee a lo sumo uno menos que la cantidad de caracteres especificada por n desde la secuencia a la que apunta la stream en la matriz apuntada por s . No se leen caracteres adicionales después de un carácter de nueva línea (que se conserva) o después del final del archivo. Un carácter nulo se escribe inmediatamente después del último carácter leído en la matriz.

Devoluciones

La función fgets devuelve s si tiene éxito. Si se encuentra el final del archivo y no se han leído caracteres en la matriz, el contenido de la matriz permanece sin cambios y se devuelve un puntero nulo. Si se produce un error de lectura durante la operación, los contenidos de la matriz son indeterminados y se devuelve un puntero nulo.

La frase lee a lo sumo uno menos que el número de caracteres especificado por n no es lo suficientemente preciso. Un número negativo no puede representar * un número de caracteres **, pero 0 significa que no hay caracteres . leer como máximo -1 caracteres no parece posible, por lo que el caso de n <= 0 no está especificado.

Para n = 1 , fgets se especifica como la lectura de un máximo de 0 caracteres, que debería tener éxito a menos que la transmisión no sea válida o se encuentre en una condición de error. La frase Un carácter nulo se escribe inmediatamente después de que el último carácter leído en la matriz es ambiguo, ya que no se han leído caracteres en la matriz, pero tiene sentido interpretar esta especial como s[0] = '\0'; . La especificación para gets_s ofrece la misma lectura, con la misma imprecisión.

La especificación de snprintf es más precisa, el caso de n = 0 está explícitamente especificado, con semántica útil adjunta. Lamentablemente, dicha semántica no puede implementarse para los fgets :

7.21.6.5 La función snprintf

Sinopsis

 #include  int snprintf(char * restrict s, size_t n, const char * restrict format, ...); 

Descripción

La función snprintf es equivalente a fprintf , excepto que la salida se escribe en una matriz (especificada por el argumento s ) en lugar de en una secuencia. Si n es cero, no se escribe nada, y s puede ser un puntero nulo. De lo contrario, los caracteres de salida más allá de la n-1 st se descartan en lugar de escribirse en la matriz, y se escribe un carácter nulo al final de los caracteres escritos en la matriz. Si la copia se lleva a cabo entre objetos que se superponen, el comportamiento no está definido.

La especificación para get_s() también aclara el caso de n = 0 y la convierte en una violación de restricción de tiempo de ejecución:

K.3.5.4.1 La función gets_s

Sinopsis

 #define __STDC_WANT_LIB_EXT1__ 1 #include  char *gets_s(char *s, rsize_t n); 

Restricciones de tiempo de ejecución

s no debe ser un puntero nulo. n no será igual a cero ni superior a RSIZE_MAX . Se debe producir n-1 carácter de nueva línea, fin de archivo o error de lectura al leer n-1 caracteres de stdin.

Si hay una violación de restricción de tiempo de ejecución, s[0] se establece en el carácter nulo, y los caracteres se leen y descartan de stdin hasta que se lee un carácter de nueva línea, o se produce un error de fin de archivo o de lectura.

Descripción

La función gets_s lee a lo sumo uno menos que la cantidad de caracteres especificada por n desde la secuencia apuntada por stdin , hacia la matriz apuntada por s . No se leen caracteres adicionales después de un carácter de nueva línea (que se descarta) o después del final del archivo. El carácter descartado de la nueva línea no cuenta para el número de caracteres leídos. Un carácter nulo se escribe inmediatamente después del último carácter leído en la matriz.

Si se encuentra el final del archivo y no se han leído caracteres en la matriz, o si se produce un error de lectura durante la operación, entonces s[0] se establece en el carácter nulo, y los otros elementos de s toman valores no especificados.

Práctica recomendada

La función fgets permite que los progtwigs escritos adecuadamente procesen con seguridad las líneas de entrada demasiado tiempo para almacenarlas en la matriz de resultados. En general, esto requiere que los que llaman de fgets presten atención a la presencia o ausencia de un carácter de nueva línea en la matriz de resultados. Considere el uso de fgets (junto con cualquier procesamiento necesario basado en caracteres de nueva línea) en lugar de gets_s .

Devoluciones

La función gets_s devuelve s si s exitosa. Si hubo una infracción de restricción de tiempo de ejecución, o si se encontró un fin de archivo y no se han leído caracteres en la matriz, o si se produce un error de lectura durante la operación, se devuelve un puntero nulo.

La biblioteca C que está probando parece tener un error para este caso, que se corrigió en versiones posteriores de glibc. Devolver NULL debe significar algún tipo de condición de falla (lo contrario de éxito): fin de archivo o error de lectura. Otros casos, como flujo no válido o flujo no abierto para lectura, se describen más o menos explícitamente como comportamiento no definido.

El caso de n = 0 n < 0 no están especificados. La devolución de NULL es una elección sensata, pero sería útil aclarar la descripción de fgets() en el Estándar para requerir n > 0 como es el caso de gets_s .

Tenga en cuenta que hay otro problema de especificación para fgets : el tipo del argumento n debería haber sido size_t lugar de int , pero esta función fue originalmente especificada por los autores C antes de que size_t fuera inventada, y se mantuvo sin cambios en el primer estándar C (C89 ) Cambiarlo entonces se consideraba inaceptable porque estaban tratando de estandarizar el uso existente: el cambio de firma habría creado inconsistencias en las bibliotecas de C y un código roto y bien escrito que usa punteros de funciones o funciones sin prototipos.

    Intereting Posts