Search code examples
cmingw-w64

Proper way to use readdir() and stat() together in minGW-W64 to get file/directory information and avoid max path issues?


I didn't ask this question as well as it should have been worded when I first posted it. So, I'm editing a bit here.

There's three blocks of code below, all of which read the contents of a directory and attempt to get the stats on the contents. All three work, except when the path/file name reaches 260 characters or more.

In all cases, readdir() returns the directory contents without issue but stat() fails with the error that the file or directory doesn't exist.

The first block passes a pointer to the path and file name to stat(). The second changes the working directory and then passes a pointer to just the file name. The third attempts to use the prefix //?/ with a fully qualified path to extend the max path accepted. None make a difference. Otherwise, all work fine except for long paths.

My question is is there a way to get this to work such that stat() won't fail for a file name that is the longest that the OS will accept. I'm using Windows 7 and just type a file name until the input box won't accept more characters. Or, is there a better method overall that perhaps doesn't require a pointer to a path but a different identifier?

Thank you.


I think the answer to specific question concerning stat() is that you cannot extend the max length past 260 with the prefix because the prefix is for Win32 functions only and not POSIX functions. And there are no other options.


Original question: In minGW-W64, is there another way to get file information other than repeating the path name in the stat() function as below?

From inspecting the dirent.h header, there appears to be nothing in the dirent struct to simply step through the records in the directory after reading without having to repeat the path.

It's not difficult to repeat the path in the stat() function but it seems to apply different size limits for the max path and i can't get the prefix of \\?\ or \\.\ to work with it. The \\?\ prefix fails altogether; \\.\ returns the stats but still fails at the same size limit as without it.

It seems that there ought to be a better way since readdir() is returning the file/directories and there should be an identifier but I don't see one available. Or to be able to pass just ep->d_name to stat() rather than having to include the path, which is what is in fl_name below, for p is just a pointer to the point in fl_name after the path.

Thank you. More complete code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>

int main ( void )
 {
    DIR *dp;
    struct dirent *ep;
    struct stat info;
    int rc, q, l, i = 0, v = 0;
    const char *path = NULL;
    char *fl_name,
         *p = NULL;
    const char *sec_path = "../../databases/";
    unsigned int len_sec_path = strlen( sec_path );

    fl_name = malloc( len_sec_path + 261 );
    // Want p to be at the point in fl_name to write the actual file and directory names in order to get the stats.
    // memcpy returns the pointer to the destination; so, just add the length of path to that to get to the end.
    p = memcpy( fl_name, sec_path, len_sec_path + 1 ) + len_sec_path;

    if ( ( dp = opendir( fl_name ) ) == NULL )
      {
        printf( "Failed to open the directory of: %s", fl_name );
        return 1;
      }
    
    while ( ep = readdir( dp ) )
      { 
        memcpy( p, ep->d_name, ( l = strlen( ep->d_name ) ) > 259 ? 260 : l + 1 );
        printf( "%d, %s\n", strlen( fl_name ), fl_name );
        if ( ( rc = stat( fl_name, &info ) ) != 0 )
          { 
            printf( "%s\n", fl_name ); 
            printf( "rc = %d, errno : %d, strerror : %s\n", rc, errno, strerror( errno ) ); 
            continue;
          }
      }  
   return 0;
 }

Version 2.

int main ( void )
 {
    DIR *dp;
    struct dirent *ep;
    struct stat info;
    int rc, q, l, i = 0, v = 0;
    const char *path = NULL;
    char *fl_name,
         *p = NULL,
         name;
    const char *sec_path = "../../databases/";
    unsigned int len_sec_path = strlen( sec_path );

    fl_name = malloc( len_sec_path + 261 );
    // Want p to be at the point in fl_name to write the actual file and directory names in order to get the stats.
    // memcpy returns the pointer to the destination; so, just add the length of path to that to get to the end.
    p = memcpy( fl_name, sec_path, len_sec_path + 1 ) + len_sec_path;
    
    char *buffer;
    if ( (buffer = _getcwd( NULL, 0 )) == NULL )
      {
       printf( "Failed to get current working directory." );
       return 1;
      }

    printf( "buffer is %s\n", buffer );

    if ( ( dp = opendir( fl_name ) ) == NULL )
      {
        printf( "Failed to open the directory of: %s", fl_name );
        return 1;
      }
        
    if ( _chdir( sec_path ) ) 
      {
        printf( "Couldn't change directory." );
        return 1;
      }

    while ( ep = readdir( dp ) )
      { 
        memcpy( p, ep->d_name, ( l = strlen( ep->d_name ) ) > 259 ? 260 : l + 1 );
        printf( "%d, %s\n", strlen( fl_name ), fl_name );
        if ( ( rc = stat( p, &info ) ) != 0 )
          { 
            printf( "%s\n", fl_name ); 
            printf( "rc = %d, errno : %d, strerror : %s\n", rc, errno, strerror( errno ) ); 
            continue;
          }    
      }  

    if ( _chdir( buffer ) ) 
      {
        printf( "Couldn't change directory." );
        return 1;
      }

   printf( "buffer is %s\n", buffer );

   free( buffer );
   return 0;
 }

Code attempting to use the \\?\ prefix to increase max path. I thought version 2 above was working and I thought that this version with the prefix was working but discovered that neither really works. Even when the directory is changed such that a pointer to the file name is passed to stat(), without the need for a path, it appears that the path is still included in the max in some manner because if the path plus filename reaches 260 stat() fails, stating that the file cannot be found. I guess it's not that big a deal, for I can catch and handle the errors and inform the user. It's just irritating that the user can use filenames in their directories that the OS will accept but I can't display them in a UI within an application.

int get_dir_2( void )    
 {    
    DIR *dp;
    struct dirent *ep;
    struct _stat info;
    int rc, q, i = 0, v = 0;

    char *fl_name,
         *p = NULL,
         *path = NULL;
    const char *sec_path = "../../databases/",
                   *prefix = "\\\\?\\"; 

    unsigned int l, len_sec_path = strlen( sec_path );

    if ( _chdir( sec_path ) ) 
      {
        printf( "Couldn't change directory." );
        return 1;
      }

    char *dirCWD;

    if ( (dirCWD = _getcwd( NULL, 0 )) == NULL )
      {
       printf( "Failed to get current working directory." );
       return 1;
      }

    path = "";
    l = strlen( prefix ) + strlen( dirCWD ) + strlen( path );
    fl_name = malloc( l + 301 );
    p = ( fl_name + l );

    sprintf( fl_name, "%s%s%s", prefix, dirCWD, path );
    printf( "fl_name is %s\n", fl_name );

    if ( _chdir( fl_name ) ) 
      {
        printf( "Couldn't change directory." );
        return 1;
      }

    if ( ( dp = opendir( fl_name ) ) == NULL )
      {
        printf( "Failed to open the directory of: %s", fl_name );
        return 1;
      }
    
    while ( ep = readdir( dp ) )
      { 
        memcpy( p, ep->d_name, ( l = strlen( ep->d_name ) ) > 259 ? 261 : l + 1 );
        //printf( "%d, %s\n", strcmp( ep->d_name, p ), p );
        printf( "%d, %s\n", strlen( fl_name ), fl_name );
        if ( ( rc = _stat( p, &info ) ) != 0 )
          { 
            printf( "%s\n", fl_name ); 
            printf( "rc = %d, errno : %d, strerror : %s\n", rc, errno, strerror( errno ) ); 
            continue;
          }
       printf( "ctime : %d, mtime : %d, atime : %d\n", info.st_ctime, info.st_mtime, info.st_atime ); 
      }  

   free( fl_name );
   return 0;
}

Solution

  • There are several areas where I suspect you had your problem occurring, but without validation checks to catch the errors when they occur, and with your use of non-standard C functions, it is difficult to tell exactly where the problem lay. In addition to the validations, you were attempting to use pointer-arithmetic on a void* pointer with:

    p = memcpy( fl_name, sec_path, len_sec_path + 1 ) + len_sec_path;
    

    which can also cause problems.

    You use _getcwd() and _chdir() which are not standard C functions. I'm not sure where those were obtained.

    To avoid the problems, use only valid C functions and validate every step in your program that is necessary to the continued defined operation of your code. For example, validate every allocation:

    #define FN_BUF_LEN 260      /* if you need a constant, #define one (or more) */
    ...
        const char *sec_path = "../../cb/";
        
        size_t len_sec_path = strlen (sec_path);
    
        if (!(fl_name = malloc (len_sec_path + FN_BUF_LEN + 1))) {  /* validate malloc */
            perror ("malloc-fl_name");
            exit (EXIT_FAILURE);
        }
    

    Do not attempt pointer arithmetic on the void* return of memcpy(), e.g.

        /* Want p to be at the point in fl_name to write the actual file and 
         * directory names in order to get the stats. memcpy returns the pointer to the
         * destination; so, just add the length of path to that to get to the end.
         */
        p = memcpy (fl_name, sec_path, len_sec_path + FN_BUF_LEN + 1);
        p += len_sec_path;      /* memcpy() is type void*, cannot use with arithmetic */
    

    Validate every call to chddir(), e.g.

        if (chdir (sec_path) == -1) {                   /* _chdir() is not std C */
            perror ("chdir()");                         /* validate result */
            return 1;
        }
    

    and

        if (chdir (buffer)) {
            perror ("chdir()");
            return 1;
        }
    

    Putting all of the pieces together and adding additional thoughts in-line as comments below, you could do:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <dirent.h>
    #include <errno.h>
    #include <sys/stat.h>
    #include <limits.h>
    #include <unistd.h>
    
    #define FN_BUF_LEN 260      /* if you need a constant, #define one (or more) */
    
    int main (void) {
        
        DIR *dp;
        struct dirent *ep;
        struct stat info;
        
        char *fl_name,
            *p = NULL, 
            *buffer = NULL;
            
        // const char *sec_path = "../../databases/";
        const char *sec_path = "../../cb/";
        
        size_t len_sec_path = strlen (sec_path);
    
        if (!(fl_name = malloc (len_sec_path + FN_BUF_LEN + 1))) {  /* validate malloc */
            perror ("malloc-fl_name");
            exit (EXIT_FAILURE);
        }
        
        /* Want p to be at the point in fl_name to write the actual file and 
         * directory names in order to get the stats. memcpy returns the pointer to the
         * destination; so, just add the length of path to that to get to the end.
         */
        p = memcpy (fl_name, sec_path, len_sec_path + FN_BUF_LEN + 1);
        p += len_sec_path;      /* memcpy() is type void*, cannot use with arithmetic */
        
        if ((buffer = getcwd (NULL, 0)) == NULL) {      /* _getcwd() is not std C */
            perror ("getcwd()");                        /* validate result */
            return 1;
        }
    
        printf ("buffer is %s\n", buffer);
    
        if ((dp = opendir (fl_name)) == NULL) {
            printf ("Failed to open the directory of: %s", fl_name);
            return 1;
        }
    
        if (chdir (sec_path) == -1) {                   /* _chdir() is not std C */
            perror ("chdir()");                         /* validate result */
            return 1;
        }
    
        while ((ep = readdir (dp))) {
            size_t l = strlen (ep->d_name);             /* declare vars in scope needed */
            if (l > 259) {                              /* validate length before memcpy */
                fprintf (stderr, "error: %s length %u exceeds allowable.\n", ep->d_name, l);
                continue;
            }
            /* filter the "." and ".." directories out */
            if (strcmp (ep->d_name, ".") == 0 || strcmp (ep->d_name, "..") == 0)
                continue;
            
            memcpy (p, ep->d_name, l + 1);
            printf ("%u, %s\n", strlen (fl_name), fl_name);
            
            int rc = stat (p, &info);
            if (rc == -1) {
                printf ("%s\n", fl_name);
                printf ("rc = %d, errno : %d, strerror : %s\n", rc, errno, strerror (errno));
                continue;
            }
        }
    
        if (chdir (buffer)) {
            perror ("chdir()");
            return 1;
        }
    
        printf ("buffer is %s\n", buffer);
    
        free (buffer);      /* free all memory you have allocated */
        free (fl_name);
        
        return 0;
    }
    

    Example Use/Output

    Testing on Win7 with MinGW 5.1 (old TDM-MinGW) and changing sec_path to "../../cb/" so there are files and directories present there on the Win7 guest, you would have:

    >bin\readdir_prob.exe
    buffer is C:\Users\david\Documents\dev\src-c\tmp
    18, ../../cb/debugtest
    21, ../../cb/default.conf
    19, ../../cb/farmtotals
    22, ../../cb/farmtotalsfmt
    14, ../../cb/first
    15, ../../cb/helopp
    19, ../../cb/matrixsolv
    16, ../../cb/toupper
    18, ../../cb/windialog
    17, ../../cb/winframe
    buffer is C:\Users\david\Documents\dev\src-c\tmp
    

    It appears the issues were either due to use of non-standard functions, your pointer arithmetic on void* or the lack of validation on one of the steps led to a failure going unnoticed and ultimately Undefined Behavior.

    If you always compile with warnings enabled -- the compiler would have pointed out many of those problems. For gcc at minimum use:

    -Wall -Wextra -pedantic -Wshadow
    

    Look over the changes and let me know if you have further questions.