File: filesizes.c
   1 /*
   2 The MIT License (MIT)
   3 
   4 Copyright © 2020-2025 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 -O3 -flto -o ./filesizes ./filesizes.c
  29 */
  30 
  31 #include <dirent.h>
  32 #include <stdbool.h>
  33 #include <stdint.h>
  34 #include <stdio.h>
  35 #include <stdlib.h>
  36 #include <string.h>
  37 #include <sys/stat.h>
  38 
  39 #ifdef _WIN32
  40 #include <fcntl.h>
  41 #include <windows.h>
  42 #endif
  43 
  44 #ifdef RED_ERRORS
  45 #define ERROR_STYLE "\x1b[38;2;204;0;0m"
  46 #ifdef __APPLE__
  47 #define ERROR_STYLE "\x1b[31m"
  48 #endif
  49 #define RESET_STYLE "\x1b[0m"
  50 #else
  51 #define ERROR_STYLE
  52 #define RESET_STYLE
  53 #endif
  54 
  55 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n")
  56 
  57 const char* info = ""
  58 "filesizes [options...] [filenames...]\n"
  59 "\n"
  60 "Show the byte-counts for all the files given: output is lines, each with 2\n"
  61 "tab-separated items, the name and the byte-count. First line has the column\n"
  62 "names.\n"
  63 "\n"
  64 "\n"
  65 "Options\n"
  66 "\n"
  67 "    -h, --h            show this help message\n"
  68 "    -help, --help      aliases for option -h\n"
  69 "";
  70 
  71 // handle_stdin counts the standard-input's bytes
  72 void handle_stdin() {
  73     unsigned char buf[32 * 1024];
  74     uint64_t bytes = 0;
  75 
  76     fputc('-', stdout);
  77 
  78     while (!feof(stdin)) {
  79         size_t n = fread(&buf, sizeof(buf[0]), sizeof(buf), stdin);
  80         if (n < 1) {
  81             // assume input is over when no bytes were read
  82             break;
  83         }
  84         bytes += n;
  85     }
  86 
  87     printf("\t%lu\n", (long unsigned int)bytes);
  88 }
  89 
  90 bool handle_file(const char* path);
  91 
  92 // fail_fullpath_alloc gives up by quitting, and is used in func handle_folder
  93 void fail_fullpath_alloc(DIR* entries) {
  94     closedir(entries);
  95     fprintf(stderr, ERROR_LINE("out of memory"));
  96     exit(1);
  97 }
  98 
  99 // handle_folder handles folder entries for func handle_files: these 2 funcs
 100 // may involve mutual recursion, when nested files/folders are involved
 101 bool handle_folder(const char* path) {
 102     DIR* entries = opendir(path);
 103 
 104     if (entries == NULL) {
 105         return false;
 106     }
 107 
 108     size_t path_len = strlen(path);
 109     // find the slash's position, whether included in the folder path or not
 110     bool trailing_slash = path[path_len - 1] == '/';
 111     size_t slash = trailing_slash ? path_len - 1 : path_len;
 112     // ensure starting capacity can fit a slash either way
 113     size_t cap = slash + 2;
 114 
 115     // fullpath is a reusable string-area to keep appending the final parts
 116     // of full pathnames for all the folder's entries
 117     char* fullpath = malloc(cap);
 118 
 119     // if allocation fails, simply give and quit the app with a message
 120     if (fullpath == NULL) {
 121         fail_fullpath_alloc(entries);
 122         return false;
 123     }
 124 
 125     // start full-path string with the folder's path
 126     strcpy(fullpath, path);
 127 
 128     // ensure a slash is between the folder's path and its entries' names
 129     if (fullpath[slash] != '/') {
 130         fullpath[slash + 0] = '/';
 131         fullpath[slash + 1] = 0;
 132     }
 133 
 134     // remember where to start appending entry names in the full-path string
 135     size_t start = slash + 1;
 136 
 137     while (!feof(stdout)) {
 138         const struct dirent* item = readdir(entries);
 139         if (item == NULL) {
 140             break;
 141         }
 142 
 143         const char* name = item->d_name;
 144 
 145         // ignore entries `.` and `..`
 146         if (name[0] == '.') {
 147             if ((name[1] == 0) || (name[1] == '.' && name[2] == 0)) {
 148                 continue;
 149             }
 150         }
 151 
 152         // ensure capacity of the full-path is enough for this entry's name
 153         size_t extra = strlen(name);
 154         if (start + extra >= cap) {
 155             char* old = fullpath;
 156             cap = start + extra + 2;
 157             fullpath = realloc(fullpath, cap);
 158 
 159             // if allocation fails, simply give and quit the app with a message
 160             if (fullpath == NULL) {
 161                 free(old);
 162                 fail_fullpath_alloc(entries);
 163                 return false;
 164             }
 165         }
 166 
 167         // complete full-path using the name of the current entry
 168         strcpy(fullpath + slash + 1, name);
 169 
 170         // handle entry, possibly recursively in case of another folder
 171         if (!handle_file(fullpath)) {
 172             free(fullpath);
 173             return false;
 174         }
 175     }
 176 
 177     closedir(entries);
 178     free(fullpath);
 179     return true;
 180 }
 181 
 182 // handle_file handles data from the filename given; returns false only when
 183 // the file can't be queried for its size, likely because it doesn't exist
 184 bool handle_file(const char* path) {
 185     struct stat st;
 186     if (stat(path, &st) != 0) {
 187         fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path);
 188         return false;
 189     }
 190 
 191     if (!S_ISDIR(st.st_mode)) {
 192         printf("%s\t%ld\n", path, st.st_size);
 193         return true;
 194     }
 195 
 196     return handle_folder(path);
 197 }
 198 
 199 // is_help_option simplifies control-flow for func run
 200 bool is_help_option(const char* s) {
 201     return (s[0] == '-') && (s[1] != 0) && (
 202         strcmp(s, "-h") == 0 ||
 203         strcmp(s, "-help") == 0 ||
 204         strcmp(s, "--h") == 0 ||
 205         strcmp(s, "--help") == 0
 206     );
 207 }
 208 
 209 // run returns the number of errors
 210 int run(int argc, char** argv) {
 211     puts("file\tbytes");
 212 
 213     size_t errors = 0;
 214     for (size_t i = 1; i < argc && !feof(stdout); i++) {
 215         // a `-` filename stands for the standard input
 216         if (argv[i][0] == '-' && argv[i][1] == 0) {
 217             handle_stdin();
 218             continue;
 219         }
 220 
 221         if (!handle_file(argv[i])) {
 222             errors++;
 223         }
 224     }
 225 
 226     // no filenames means use stdin as the only input
 227     if (argc < 2) {
 228         handle_stdin();
 229     }
 230 
 231     return errors;
 232 }
 233 
 234 int main(int argc, char** argv) {
 235 #ifdef _WIN32
 236     setmode(fileno(stdin), O_BINARY);
 237     // ensure output lines end in LF instead of CRLF on windows
 238     setmode(fileno(stdout), O_BINARY);
 239     setmode(fileno(stderr), O_BINARY);
 240 #endif
 241 
 242     if (argc > 1 && is_help_option(argv[1])) {
 243         fprintf(stderr, "%s", info);
 244         return 0;
 245     }
 246 
 247     return run(argc, argv) == 0 ? 0 : 1;
 248 }