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