2
\$\begingroup\$

This is a follow up to Find the Size of a File in a Portable Manner (revision).

Changes made:

  • The return value of fseek() and ftell() are checked. The function should fail for non-seekable file descriptors on a stat-less platform.

  • io_fsize() now checks the result of feof() instead of ferror() to avoid failing if the error indicator was already set before calling this function.

  • The function restores the original file position in case of overflow.

  • On Windows, the file's mode is changed with _set_mode() to _O_BINARY.

Code:

#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

#ifdef _POSIX_C_SOURCE
    #define HAVE_STAT 1
    #include <sys/stat.h>
#endif  /* _POSIX_C_SOURCE */

/* Reasons to not use `fseek()` and `ftell()` to compute the size of the file:
 * 
 * Subclause 7.12.9.2 of the C Standard [ISO/IEC 9899:2011] specifies the
 * following behavior when opening a binary file in binary mode:
 * 
 * >> A binary stream need not meaningfully support fseek calls with a whence 
 * >> value of SEEK_END.
 *
 * In addition, footnote 268 of subclause 7.21.3 says:
 *
 * >> Setting the file position indicator to end-of-file, as with 
 * >> fseek(file, 0, SEEK_END) has undefined behavior for a binary stream.
 *
 * For regular files, the file position indicator returned by `ftell()` is 
 * useful only in calls to `fseek()`. As such, the value returned may not
 * reflect the physical byte offset. */
bool io_fsize(FILE *stream, uintmax_t *size)
{
/*   Windows supports `fileno()`, `struct stat`, and `fstat()` as `_fileno()`,
 *   `_fstat()`, and `struct _stat`.
 *
 *   See: https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fstat-fstat32-fstat64-fstati64-fstat32i64-fstat64i32?view=msvc-170 */
#ifdef _WIN32
    #define HAVE_STAT 1
    
    /* Windows does not define S_ISREG, S_ISLNK, and S_ISDIR macros in stat.h,
     * so we do. We have to define _CRT_INTERNAL_NONSTDC_NAMES before including
     * sys/stat.h in order for Microsoft's stat.h to define names like S_IFMT,
     * S_IFDIR, and S_IFREG et cetera as it normally does. */
    #define _CRT_INTERNAL_NONSTDC_NAMES 1
    #include <sys/stat.h>

    #ifndef S_ISDIR
        #define S_ISDIR(m)  (((m) & S_IFMT) == S_IFDIR) 
    #endif  /* S_ISDIR */

    #ifndef S_ISREG
        #define S_ISREG(m)  (((m) & S_IFMT) == S_IFREG)
    #endif  /* S_ISREG */
    
    #ifdef S_IFLNK
        #ifndef S_ISLNK
            #define S_ISLNK(m)  (((m) & S_IFMT) == S_IFLNK)
        #endif  /* S_ISLINK */
    #else
        #define S_ISLNK(m)  (0)
    #endif  /* S_IFLNK */
    
    #define fileno _fileno

    #ifdef _WIN64
        #define fstat  _fstat64
        #define stat   __stat64
    #else
        /* Does this suffice for a 32-bit system? */
        #define fstat  _fstat
        #define stat   _stat
    #endif    /* _WIN64 */

    #include <io.h>
    #include <fcntl.h>

    /* MSDN requires that `stream` be flushed before calling this function,
     * else the behavior might be unexpected. */
    fflush(stream);

    if (_setmode(fileno(stream), _O_BINARY) == -1) {
        return false;
    }
#endif    /* _WIN32 */

#ifdef HAVE_STAT
    struct stat st;

    if (fstat(fileno(stream), &st) == 0) {
        if (S_ISDIR(st.st_mode)) {
            #ifdef EISDIR
            errno = EISDIR;
            #endif
            return false;
        } else if (!S_ISREG(st.st_mode) || !S_ISLNK(st.st_mode)) {
            #ifdef EPERM
            errno = EPERM;
            #endif
            return false;
        }

        if (st.st_size < 0) {
            return false;
        }
        *size = (uintmax_t) st.st_size;
        return true;
    }
    return false;
#else
    /* Fall back to the default and read it in chunks. */
    uintmax_t rcount = 0;
    char chunk[BUFSIZ];  

    /* Save the original file position indicator. We'd restore this before 
     * returning. */
    const long orig_pos = ftell(stream);

    if (orig_pos == -1) {
        return false;
    }

    do {
        rcount = fread(chunk, 1, sizeof chunk, stream);

        if ((*size + rcount) < *size) {
            /* Overflow. */
            #ifdef EFBIG
            errno = EFBIG;
            #endif
            fseek(stream, orig_pos, SEEK_CUR);
            return false;
        }
        *size += rcount;
    } while (rcount == sizeof chunk);

    return fseek(stream, orig_pos, SEEK_CUR) != -1 && feof(stream);
#endif      /* HAVE_STAT */ 
}

Review Request:

Is the call to _set_mode() in the right place?

Have I introduced any new issues/bugs?

Is there anything else the function can benefit from?

\$\endgroup\$
5
  • \$\begingroup\$ But now what if the EOF indicator was set before calling io_fsize()? \$\endgroup\$
    – G. Sliepen
    Commented May 16 at 22:33
  • \$\begingroup\$ @G.Sliepen Well, I see a size zero being returned. \$\endgroup\$
    – Harith
    Commented May 16 at 22:56
  • \$\begingroup\$ Yes, but isn't it possible to make it work anyway? You should be able to recover from EOF, and if you really want to, get into an EOF state again afterwards. \$\endgroup\$
    – G. Sliepen
    Commented May 17 at 7:22
  • \$\begingroup\$ @G.Sliepen I have written as answer. Is this what you were meaning to imply? \$\endgroup\$
    – Harith
    Commented May 17 at 7:56
  • 1
    \$\begingroup\$ Yes, thanks for the nice self-answer! \$\endgroup\$
    – G. Sliepen
    Commented May 17 at 8:07

1 Answer 1

2
\$\begingroup\$

The fread() version has different behavior when the read position indicator is not at the beginning of the file:

In the case where the read position indicator is not at the beginning of the file, and io_fsize() is called, either of these two can happen:

  • stat() exists, assuming the file descriptor was seekable and there were no errors, the return value would be the size of the file.
  • stat() does not exist, the default method is used where the file is read in chunks. Assuming the underlying file descriptor was seekable and there were no errors on the stream, the return value would be the size of the file - current read position.

The same function can return different values depending on the current read position and the presence of stat(). This can lead to some nasty bugs.

The second method (fread() one) can be changed to save the current read position, call rewind(stream), determine the size of the file, and then reset it back to the original position. This has the benefit that the original read position is not modified (assuming fseek() did not fail), and both the stat() and fread() versions have the same behavior, regardless of the current read position.

orig_pos = ftell(stream); // save the original position
rewind(stream); // go to the beginning
read_in_chunks_until_eof(stream);
fseek(stream, orig_pos, SEEK_CUR); // restore the original position

Similarly, if the EOF indicator was already set before calling io_fsize(), this same method can be used to recover from it, except that you no longer require a call to fseek(), as reading the file till EOF would leave the read position indicator at the end. The final algorithm becomes:

is_at_end = !feof(stream);
orig_pos = 0;

if not is_at_end {
    orig_pos = ftell(stream); // check the return value of course    
}

rewind(stream);
read_in_chunks_until_eof(stream);

if not is_at_end {
    fseek(stream, orig_pos, SEEK_CUR); // restore the original position
}

stat() is not used in the code:

The function only makes use of fstat(), so this part can be simplified:

#ifdef _WIN64
    #define fstat  _fstat64
    #define stat   __stat64
#else
    /* Does this suffice for a 32-bit system? */
    #define fstat  _fstat
    #define stat   _stat
#endif    /* _WIN64 */

to:

#ifdef _WIN64
    #define fstat  _fstat64
#else
    /* Does this suffice for a 32-bit system? */
    #define fstat  _fstat
#endif    /* _WIN64 */
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.