7
\$\begingroup\$

It's been some time since I wrote something in C. I am looking for some advices related to my code in terms of coding style, bad/wrong logic, optimization (performance) and good practices.

The code speaks for itself, but if there are any questions, I'll add some comments to make it clearer.

#include <stdio.h>
#include <string.h>
#include <windows.h>

#define START_COUNTING 0
#define NEW_FILENAME_MAXCHARS 25
#define CURRENT_INDEX_MAXCHAR 25

typedef unsigned int uint;

// Basic logger.
struct Logger
{
    uint failed;
    uint succeeded;
};

void print_warning(void);

// Add a token at the end of a file
char *add_token(char *path, char *token);
void rename_files(char *path);

// If the cwd is not the path where the files are located
// than change the cwd to that path.
void check_dir(const char *path);

int main(int argc, char *argv[])
{
    print_warning();
    if(argc != 2)
    {
        printf("Usage: <program> <path>\n");
        exit(EXIT_FAILURE);
    }
    rename_files(argv[1]);

    return 0;
}

void check_dir(const char *path)
{
    char *buffer = _getcwd(NULL, 0);
    if(buffer == NULL)
    {
        printf("FAILED TO GET THE CURRENT WORKING DIRECTORY\n");
        exit(EXIT_FAILURE);
    }
    else
    {
        if(strcmp(buffer, path) != 0)
        {
        #ifdef DEBUG
            printf("Changing path to: %s\n", path);
        #endif
            _chdir(path);
        }
    }

    free(buffer);
}

char *add_token(char *path, char *token)
{
    char *new_array = malloc(strlen(path) + strlen(token) + 1);

    if(new_array == NULL)
    {
        printf("MALLOC FAILED");
        exit(EXIT_FAILURE);
    }
    strcpy(new_array, path);
    strcat(new_array, token);

    return new_array;
}

void rename_files(char *path)
{
    // INIT FUNCTIONS
    check_dir(path);
    path = add_token(path, "\\*");

    // INIT DATA
    WIN32_FIND_DATA find_files;
    HANDLE handle_FindFiles = FindFirstFile(path, &find_files);
    uint current_index = START_COUNTING;

    // INIT LOGGER
    struct Logger logger;
    logger.failed    = 0;
    logger.succeeded = 0;

    if(handle_FindFiles == INVALID_HANDLE_VALUE)
    {
        printf("FAIL TO FIND FIRST FILE IN DIRECTORY: %s\n", path);
        exit(EXIT_FAILURE);
    }
    else
    {
        while(FindNextFile(handle_FindFiles, &find_files) != 0)
        {
            // TODO: Add support for renaming files in directories.
            // Make sure we don't rename a directory.
            if(!(find_files.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
            {
                current_index++;

                // Get the extenstion of the file.
                char *ext = strrchr(find_files.cFileName, '.') + 1;

                #ifdef DEBUG
                    printf("%s => %u.%s\n", find_files.cFileName, current_index, ext);
                #endif

                // Conver the current index to a string
                char current_index_str[CURRENT_INDEX_MAXCHAR];

                // Store the file's new name here.
                char new_filename[NEW_FILENAME_MAXCHARS];

                // Conver the index
                sprintf(current_index_str, "%d", current_index);

                // Construct the new file name
                strcpy(new_filename, current_index_str);
                strcat(new_filename, ".");
                strcat(new_filename, ext);

                int result = rename(find_files.cFileName, new_filename);

                if(!result)
                {
                #ifdef DEBUG
                    printf("FAILED TO RENAME FILE %s with new name %s\n", find_files.cFileName, new_filename);
                #endif 
                    logger.failed++;
                }
                else
                {
                    logger.succeeded++;
                }
            }
        }
    }

    // DEBUG INFO.
    #ifdef DEBUG
        printf("\n---------------------------\n");
        printf("INFO\n");
        printf("FAILED:    %10u\n", logger.failed);
        printf("SUCCEEDED: %10u\n", logger.succeeded);
    #endif // DEBUG

    printf("Done renaming files\n");

    free(path);
}

void print_warning(void)
{
    printf("USE THIS PROGRAM AT YOUR OWN RISK\n");
    printf("IT MAY DAMAGE FILES AND/OR CAUSE DELETE OF DATA\n");
    printf("THE AUTHOR OF THIS PROGRAM IS NOT RESPOSABLE OF ANY DAMAGE"
           "DONE BY THE PROGRAM OR OF YOUR ACTIONS WITH IT\n");
}

Any constructive critique is welcomed.

\$\endgroup\$
2
  • \$\begingroup\$ Please use good English in anything legal-looking. For example, "delete of data" and "responsable [sic] of damage" are both not English grammar. \$\endgroup\$
    – anon
    May 9, 2016 at 11:03
  • 2
    \$\begingroup\$ Just a small thing but you should send all error messages to stderr using fprintf(stderr, ...) instead of just using printf() \$\endgroup\$
    – jess
    May 9, 2016 at 13:16

3 Answers 3

2
\$\begingroup\$

Improvements

  1. What does program do? "Simple file renamer" clearly states a file is to be renamed and it is is evident the program takes only 1 argument. Yet I would expect a comment to state the purpose of code and what the file name is changed to.

  2. When using using typedef'd uint, take time to minimize work should the type change.

    // printf("%s => %u.%s\n", find_files.cFileName, current_index, ext);
    printf("%s => %u.%s\n", find_files.cFileName, (unsigned) current_index, ext);
    
    // or better
    printf("%s => %llu.%s\n", find_files.cFileName, 
        (unsigned long long) current_index, ext);
    
    // or best (but I suspect your windows compiler does not like ju)
    printf("%s => %ju.%s\n", find_files.cFileName, (uintmax_t) current_index, ext);
    
  3. Buffer size bordering on being too small. Consider either right-sizing the buffer and/or use snprint()

  4. When printing an error message about an invalid string, bracketing the offending string helps highlight strings that may have leading/trailing whitespace or strange characters. Note: better to use fprintf() for error messages.

    // printf("FAIL TO FIND FIRST FILE IN DIRECTORY: %s\n", 
    printf("FAIL TO FIND FIRST FILE IN DIRECTORY: \"%s\"\n", 
    
  5. Missing check if '.' not found

     char *ext = strrchr(find_files.cFileName, '.') + 1;
     assert(ext);
    
     printf("%s => %u.%s\n", find_files.cFileName, current_index, 
     strcat(new_filename, ext);
    

Minor

  1. Unclear what NEW_FILENAME_MAXCHARS precisely means. Is it the maximum length of a string containing a filename like foo.txt (7) or the size of the buffer to hold a maximal long file name (8)? Suggest NEW_FILENAME_SIZE instead for clarity.

    // char current_index_str[CURRENT_INDEX_MAXCHAR];
    char current_index_str[NEW_FILENAME_SIZE];
    
  2. DEBUG vs NDEBUG. NDEBUG is well defined in the C spec. Consider negating the macros and using NDEBUG.

  3. rename_files(char *path) does not change elements of path. Consider rename_files(const char *path) Like-wise char *add_token(const char *path, const char *token)

  4. void rename_files(char *path) returns nothing, yet such a function certainly has many chances for I/O, allocation errors. Better to return an error code.

  5. CURRENT_INDEX_MAXCHAR 25 is so removed from use and not detailed as to why it is 25 leads to a potential future problem when it gets changed from 25 to TBD. Suggest a more meaningful name and size to meet the needs

      // #define CURRENT_INDEX_MAXCHAR 25
      ...
      // char current_index_str[CURRENT_INDEX_MAXCHAR];
      // sprintf(current_index_str, "%d", current_index);
    
      // Size needed for a decimal string of an unsigned integer.
      #define INT_STRING_SIZE(type) ((sizeof (type)*CHAR_BIT - 1)/3 + 3)
      #define UINT_STRING_SIZE(type) (sizeof (type)*CHAR_BIT/3 + 2)
      ...
      char current_index_str[UINT_STRING_SIZE(unsigned long long)];
      sprintf(current_index_str, "%llu", (unsigned long long) current_index);
    
\$\endgroup\$
1
\$\begingroup\$

Compatibility

Code provided seems to be Windows only. It would be nice if You'd mentioned that on the original question or provide build script which does relevant checks and notes that it's not going to work on non-Windows systems.

Better solution would be providing non-Windows compatible code.

Inconsistency

First of all, You're using inconsistent naming for You macros. NEW_FILENAME_MAXCHAR and CURRENT_INDEX_MAXCHARS. Different wording on START_COUNTING as well.

Misleading

Why would You want to do something like this: typedef unsigned int uint;? It's... a bit misleading. Reassembles to uint_xxt types too much. And why the hell would You typedef that? You're using it only a few times.

Common issues

exit() in the middle of some function is just wrong. As a programmer using some library You wouldn't want it to exit on some random places, would You? Also it prevents You from managing resources properly. Please, consider designing proper API:

  • functions return error codes or 0 on success
  • proper constructors/destructors for Your structures
  • consistent management of resources, clear "owner" of resources, which allocates and deallocates memory in general and faulty cases.

Btw, You have a few potential memory leaks.

And please... Please, sanitize ALL Your inputs. rename_files(NULL), add_token(NULL, NULL), etc. Also... You're forgetting some corners cases. Like... a file with no extension. strrchr() going to fail and You're going to deal with NULL pointer. Again.

It would be really nice if Your application had different return codes for different errors. It makes way easier debugging and usage in some batch scripts. Parsing output strings just sucks.

If You still insist typedefing Your integers, then consider typedefing their print-modifier as well. Or just provide single macros/function which sends error/status report to stderr (as mentioned in comments by psychedelic_alex). Why? Because one of the reasons for typedefing something is... idea of changing something in the future. If You're going to change Your integers and leave print-modifiers untouched ---- it might get broken.

Generic warnings/errors

exit() and EXIT_FAILURE requires stdlib.h.

%d in format string (sprintf(current_index_str, "%d", current_index); ) requires a signed integer.

Random notes

Please consider printing usage information and what exactly Your application does instead of some unrelated warning.

Your logger sucks. I would really like to see the reasons WHY renaming failed. On which files it failed. Maybe the application itself failed somewhere in-between? Proper logger with line-numbers and sensible error message would help tremendously.

Having something like create_logger(), destroy_logger(), print_logger(), etc. would be nice. And that would shorten Your current functions a bit.

\$\endgroup\$
1
\$\begingroup\$

Since this clearly is not cross-platform code as is, I'm going to advocate the other end of the spectrum: go whole hog leveraging proper Windows capability sets and idioms. It's not always the right choice, by any means, but here's what doing so might look like.

Focus on programming for Windows

  • Use UNICODE

    By using char and (as far as I can tell) not defining UNICODE, you are limiting yourself to the ANSI character set of the local language. This means there are file names that you cannot handle. FindFirstFile will report them with question marks, and then when you pass them to rename(), it won't be able to rename the desired file. The flip side? This supports Windows 9x systems, which are long since end-of-life.

    If you #define UNICODE, this problem goes away. Instead of char, you will use WCHAR or wchar_t buffers for your filenames, and the WIN32_FIND_DATAW structure will contain them as well. The str* functions can be replaced with wcs* equivalents.

  • Use Windows API

    Posix compatibility functions like rename and _getcwd don't feel native to Windows the way functions like MoveFile and GetCurrentDirectory do. They also introduce a dependency on the C runtime library that you may not want. If you're linking statically to avoid a separate dependency, they can increase the size of your executable than using the Windows APIs will.

  • Match usual Windows limitations

    Consider using buffers of size MAX_PATH instead of the custom NEW_FILENAME_MAXCHARS. While your code today won't overflow, if you tweak your approach and forget to update your buffer, you are less likely to hit surprising boundary conditions with larger buffers.

  • Use buffers safely

    Consider using APIs from strsafe.h to manipulate your buffers instead of those from the C runtime. Or at least consider using the _s variants of the C runtime APIs.

  • Use the APIs correctly

    If FindFirstFile succeeds, it has already placed file information in the WIN32_FIND_DATA structure. Now odds are really good that the first files it finds are . and .., so you may not have seen any symptoms of skipping this file, but your current code will skip whatever the first file is. FindFirstFile/FindNextFile is one of those places I would consider using a do-while loop.

Smaller ideas

  • Consider using common Windows types. Why define a type called uint when the Windows headers provide UINT and friends?
  • Consider avoiding malloc; by mixing some of the above advice, you can end up passing a local buffer to add_token, or replacing it with a simple string concatenation, or even requiring the trailing \* be part of the input on the command line. As a bonus, possibly making up for its added difficulty of use, the last option would enable your program to rename just a subset of the files in the directory.
  • Consider combining the pattern of strcpy, strcat, strcat with a single formatting call, such as a variant of sprintf or StringCchPrintf. Multiple subsequent calls to strcat on the same buffer retread the string multiple times, acting like Shlemiel the painter.
  • Provide deeper error information. If the rename fails, include errno or GetLastError()'s value in your output (errno if you stick with rename, GetLastError() if you change to MoveFile).
  • Embrace static analysis, and use SAL annotations to specify the kind of input your functions accept. This clarifies whether your functions are allowed to receive null pointers, among other possible misuses.

Again, deciding to focus on writing the program for Windows (rather than cross-platform compatibility) is not the only possible approach, but I hope the above ideas are useful for anyone who chooses that path. Many of the those ideas, such as safe buffer usage and matching local expectations, are still relevant in cross-platform code; however they will be expressed differently because of differences in the APIs available.

\$\endgroup\$

Your Answer

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

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