File: filesizes.c
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2024 pacman64
   5 
   6 Permission is hereby granted, free of charge, to any person obtaining a copy of
   7 this software and associated documentation files (the “Software”), to deal
   8 in the Software without restriction, including without limitation the rights to
   9 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
  10 of the Software, and to permit persons to whom the Software is furnished to do
  11 so, subject to the following conditions:
  12 
  13 The above copyright notice and this permission notice shall be included in all
  14 copies or substantial portions of the Software.
  15 
  16 THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22 SOFTWARE.
  23 */
  24 
  25 /*
  26 You can build this command-line app by running
  27 
  28 cc -Wall -s -O2 -o ./filesizes ./filesizes.c
  29 */
  30 
  31 #include <dirent.h>
  32 #include <fcntl.h>
  33 #include <math.h>
  34 #include <stdbool.h>
  35 #include <stdint.h>
  36 #include <stdio.h>
  37 #include <stdlib.h>
  38 #include <string.h>
  39 #include <sys/stat.h>
  40 
  41 #ifdef _WIN32
  42 #include <windows.h>
  43 #endif
  44 
  45 // info is the multi-line help message
  46 const char* info = ""
  47     "filesizes [options...] [filenames...]\n"
  48     "\n"
  49     "Show the byte-counts for all the files given: output is lines, each with 2\n"
  50     "tab-separated items, the name and the byte-count. First line has the column\n"
  51     "names.\n"
  52     "\n"
  53     "\n"
  54     "Options\n"
  55     "\n"
  56     "    -h, --h            show this help message\n"
  57     "    -help, --help      aliases for option -h\n"
  58     "";
  59 
  60 // handle_stdin counts the standard-input's bytes
  61 void handle_stdin() {
  62     unsigned char buf[32 * 1024];
  63     uint64_t bytes = 0;
  64 
  65     putc('-', stdout);
  66 
  67     while (!feof(stdin)) {
  68         size_t n = fread(&buf, sizeof(buf[0]), sizeof(buf), stdin);
  69         if (n < 1) {
  70             // assume input is over when no bytes were read
  71             break;
  72         }
  73         bytes += n;
  74     }
  75 
  76     printf("\t%lu\n", (long unsigned int)bytes);
  77 }
  78 
  79 bool handle_file(const char* path);
  80 
  81 // fail_fullpath_alloc gives up by quitting, and is used in func handle_folder
  82 void fail_fullpath_alloc(DIR* entries) {
  83     char* msg = "\x1b[31mcan't get memory for the full file names\x1b[0m\n";
  84     closedir(entries);
  85     fflush(stdout);
  86     fprintf(stderr, msg);
  87     exit(1);
  88 }
  89 
  90 // handle_folder handles folder entries for func handle_files: these 2 funcs
  91 // may involve mutual recursion, when nested files/folders are involved
  92 bool handle_folder(const char* path) {
  93     DIR* entries = opendir(path);
  94 
  95     if (entries == NULL) {
  96         return false;
  97     }
  98 
  99     size_t path_len = strlen(path);
 100     // find the slash's position, whether included in the folder path or not
 101     bool trailing_slash = path[path_len - 1] == '/';
 102     size_t slash = trailing_slash ? path_len - 1 : path_len;
 103     // ensure starting capacity can fit a slash either way
 104     size_t cap = slash + 2;
 105 
 106     // fullpath is a reusable string-area to keep appending the final parts
 107     // of full pathnames for all the folder's entries
 108     char* fullpath = malloc(cap);
 109 
 110     // if allocation fails, simply give and quit the app with a message
 111     if (fullpath == NULL) {
 112         fail_fullpath_alloc(entries);
 113         return false;
 114     }
 115 
 116     // start full-path string with the folder's path
 117     strcpy(fullpath, path);
 118 
 119     // ensure a slash is between the folder's path and its entries' names
 120     if (fullpath[slash] != '/') {
 121         fullpath[slash + 0] = '/';
 122         fullpath[slash + 1] = 0;
 123     }
 124 
 125     // remember where to start appending entry names in the full-path string
 126     size_t start = slash + 1;
 127 
 128     while (!feof(stdout)) {
 129         struct dirent* item = readdir(entries);
 130         if (item == NULL) {
 131             break;
 132         }
 133 
 134         const char* name = item->d_name;
 135 
 136         // ignore entries `.` and `..`
 137         if (name[0] == '.') {
 138             if ((name[1] == 0) || (name[1] == '.' && name[2] == 0)) {
 139                 continue;
 140             }
 141         }
 142 
 143         // ensure capacity of the full-path is enough for this entry's name
 144         size_t extra = strlen(name);
 145         if (start + extra >= cap) {
 146             char* old = fullpath;
 147             cap = start + extra + 2;
 148             fullpath = realloc(fullpath, cap);
 149 
 150             // if allocation fails, simply give and quit the app with a message
 151             if (fullpath == NULL) {
 152                 free(old);
 153                 fail_fullpath_alloc(entries);
 154                 return false;
 155             }
 156         }
 157 
 158         // complete full-path using the name of the current entry
 159         strcpy(fullpath + slash + 1, name);
 160 
 161         // handle entry, possibly recursively in case of another folder
 162         if (!handle_file(fullpath)) {
 163             free(fullpath);
 164             return false;
 165         }
 166     }
 167 
 168     closedir(entries);
 169     free(fullpath);
 170     return true;
 171 }
 172 
 173 // handle_file handles data from the filename given; returns false only when
 174 // the file can't be queried for its size, likely because it doesn't exist
 175 bool handle_file(const char* path) {
 176     struct stat st;
 177     if (stat(path, &st) != 0) {
 178         fprintf(stderr, "\x1b[31mcan't open file named %s\x1b[0m\n", path);
 179         return false;
 180     }
 181 
 182     if (!S_ISDIR(st.st_mode)) {
 183         printf("%s\t%ld\n", path, st.st_size);
 184         return true;
 185     }
 186 
 187     return handle_folder(path);
 188 }
 189 
 190 // is_help_option simplifies control-flow for func run
 191 bool is_help_option(char* s) {
 192     return (s[0] == '-') && (s[1] != 0) && (
 193         strcmp(s, "-h") == 0 || strcmp(s, "-help") == 0 ||
 194         strcmp(s, "--h") == 0 || strcmp(s, "--help") == 0
 195     );
 196 }
 197 
 198 // run returns the number of errors
 199 int run(int argc, char** argv) {
 200     size_t errors = 0;
 201 
 202     // handle special cmd-line options
 203     for (size_t i = 1; i < argc; i++) {
 204         if (is_help_option(argv[i])) {
 205             // help option is handled right away, also quitting the app
 206             fprintf(stderr, "%s", info);
 207             return 0;
 208         }
 209     }
 210 
 211     puts("file\tbytes");
 212 
 213     for (size_t i = 1; i < argc && !feof(stdout); i++) {
 214         // a `-` filename stands for the standard input
 215         if (argv[i][0] == '-' && argv[i][1] == 0) {
 216             handle_stdin();
 217             continue;
 218         }
 219 
 220         if (!handle_file(argv[i])) {
 221             errors++;
 222         }
 223     }
 224 
 225     // no filenames means use stdin as the only input
 226     if (argc < 2) {
 227         handle_stdin();
 228     }
 229 
 230     return errors;
 231 }
 232 
 233 int main(int argc, char** argv) {
 234 #ifdef _WIN32
 235     setmode(fileno(stdin), O_BINARY);
 236     // ensure output lines end in LF instead of CRLF on windows
 237     setmode(fileno(stdout), O_BINARY);
 238     setmode(fileno(stderr), O_BINARY);
 239 #endif
 240 
 241     return run(argc, argv) == 0 ? 0 : 1;
 242 }