File: id3pic.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 -march=native -mtune=native -flto -o ./id3pic ./id3pic.c
  29 */
  30 
  31 #include <stdbool.h>
  32 #include <stdint.h>
  33 #include <stdio.h>
  34 #include <string.h>
  35 
  36 #ifdef RED_ERRORS
  37 #define ERROR_STYLE "\x1b[38;2;204;0;0m"
  38 #ifdef __APPLE__
  39 #define ERROR_STYLE "\x1b[31m"
  40 #endif
  41 #define RESET_STYLE "\x1b[0m"
  42 #else
  43 #define ERROR_STYLE
  44 #define RESET_STYLE
  45 #endif
  46 
  47 #define ERROR_LINE(MSG) (ERROR_STYLE MSG RESET_STYLE "\n")
  48 
  49 #ifndef IBUF_SIZE
  50 #define IBUF_SIZE (32 * 1024)
  51 #endif
  52 
  53 const char* info = ""
  54 "id3pic [options...] [file...]\n"
  55 "\n"
  56 "Extract picture/thumbnail bytes from ID3/MP3 metadata, if available.\n"
  57 "\n"
  58 "All (optional) leading options start with either single or double-dash:\n"
  59 "\n"
  60 "    -h          show this help message\n"
  61 "    -help       show this help message\n"
  62 "";
  63 
  64 typedef struct bufreader {
  65     // buf is the buffer, (re)filled periodically as needed
  66     unsigned char* buf;
  67 
  68     // len is how many buffer bytes are being used, out of its max capacity
  69     size_t len;
  70 
  71     // cap is the buffer's capacity, or the most bytes it can hold at once
  72     size_t cap;
  73 
  74     // pos is the current position, up to the current buffer length
  75     size_t pos;
  76 
  77     // src is the data source used to fill the buffer
  78     FILE* src;
  79 } bufreader;
  80 
  81 // init_bufreader is the constructor for type bufreader
  82 void init_bufreader(bufreader* r, FILE* src, unsigned char* buf, size_t cap) {
  83     r->buf = buf;
  84     r->len = 0;
  85     r->cap = cap;
  86     r->pos = 0;
  87     r->src = src;
  88 }
  89 
  90 // read_byte does as it says: check its return for the value EOF, before
  91 // using it as the next byte
  92 int read_byte(bufreader* r) {
  93     if (r->pos < r->len) {
  94         // inside current chunk
  95         const unsigned char b = r->buf[r->pos];
  96         r->pos++;
  97         return b;
  98     }
  99 
 100     // need to read the next block
 101     r->pos = 0;
 102     r->len = fread(r->buf, sizeof(r->buf[0]), r->cap, r->src);
 103     if (r->len > 0) {
 104         const unsigned char b = r->buf[r->pos];
 105         r->pos++;
 106         return b;
 107     }
 108 
 109     // reached the end of data
 110     return EOF;
 111 }
 112 
 113 void copy_bytes(FILE* w, bufreader* r, int64_t n) {
 114     if (n < 1) {
 115         return;
 116     }
 117 
 118     // emit what's left of the current read-buffer
 119     const int64_t trail = r->len - r->pos;
 120     if (n < trail) {
 121         fwrite(r->buf + r->pos, 1, n, w);
 122         return;
 123     }
 124     if (fwrite(r->buf + r->pos, 1, trail, w) < 1) {
 125         return;
 126     }
 127     n -= trail;
 128 
 129     // read/write full-capacity chunks
 130     while (n > r->cap) {
 131         const int64_t in = fread(r->buf, sizeof(r->buf[0]), r->cap, r->src);
 132         if (in < 0 || in < r->cap) {
 133             return;
 134         }
 135         if (fwrite(r->buf, 1, r->cap, w) < 1) {
 136             return;
 137         }
 138         n -= r->cap;
 139     }
 140 
 141     // emit the last bytes with a custom-sized read/write
 142     if (n > 0) {
 143         const int64_t in = fread(r->buf, sizeof(r->buf[0]), n, r->src);
 144         if (in > 0) {
 145             fwrite(r->buf, 1, in, w);
 146         }
 147     }
 148 }
 149 
 150 int64_t discard_bytes(bufreader* r, size_t n) {
 151     if (r->pos + n < r->len) {
 152         r->pos += n;
 153         return n;
 154     }
 155 
 156     int64_t discarded = 0;
 157     for (; n > 0; n--, discarded++) {
 158         if (read_byte(r) == EOF) {
 159             break;
 160         }
 161     }
 162     return discarded;
 163 }
 164 
 165 int64_t discard_until(bufreader* r, unsigned char what) {
 166     for (int64_t n = 0; true; n++) {
 167         int b = read_byte(r);
 168         if (b == EOF) {
 169             return -(n + 1);
 170         }
 171         if (b == what) {
 172             return n + 1;
 173         }
 174     }
 175 }
 176 
 177 bool match(bufreader* r, const char* what) {
 178     for (size_t i = 0; what[i] != 0; i++) {
 179         if (read_byte(r) != what[i]) {
 180             return false;
 181         }
 182     }
 183     return true;
 184 }
 185 
 186 bool any(const int* data, size_t n, int what) {
 187     for (size_t i = 0; i < n; i++) {
 188         if (data[i] == what) {
 189             return true;
 190         }
 191     }
 192     return false;
 193 }
 194 
 195 void show_error(const char* msg) {
 196     fprintf(stderr, ERROR_LINE("%s"), msg);
 197 }
 198 
 199 int64_t skip_thumbnail_type_APIC(bufreader* r) {
 200     int64_t skipped = 0;
 201     int64_t n = 0;
 202 
 203     n = discard_bytes(r, 2);
 204     if (n != 2) {
 205         show_error("failed to sync to APIC flags");
 206         return false;
 207     }
 208     skipped += n;
 209 
 210     n = discard_bytes(r, 1);
 211     if (n != 1) {
 212         show_error("failed to sync to APIC text-encoding");
 213         return false;
 214     }
 215     skipped += n;
 216 
 217     n = discard_until(r, 0);
 218     if (n < 0) {
 219         show_error("failed to sync to APIC thumbnail MIME-type");
 220         return -1;
 221     }
 222     skipped += n;
 223 
 224     n = discard_bytes(r, 1);
 225     if (n != 1) {
 226         show_error("failed to sync to APIC picture type");
 227         return false;
 228     }
 229     skipped += n;
 230 
 231     n = discard_until(r, 0);
 232     if (n < 0) {
 233         show_error("failed to sync to APIC thumbnail description");
 234         return -1;
 235     }
 236     skipped += n;
 237 
 238     return skipped;
 239 }
 240 
 241 int64_t decode_big_endian_int64(const int v[4]) {
 242     return (v[0] << 24) + (v[1] << 16) + (v[2] << 8) + (v[3] << 0);
 243 }
 244 
 245 bool handle_apic(FILE* w, bufreader* r) {
 246     // section-size seems stored as 4 big-endian bytes
 247     int ss[4];
 248     ss[0] = read_byte(r);
 249     ss[1] = read_byte(r);
 250     ss[2] = read_byte(r);
 251     ss[3] = read_byte(r);
 252     if (any(ss, sizeof(ss), EOF)) {
 253         return false;
 254     }
 255 
 256     const int64_t section_size = decode_big_endian_int64(ss);
 257     const int64_t header_size = skip_thumbnail_type_APIC(r);
 258     if (header_size < 0 || header_size > section_size) {
 259         return false;
 260     }
 261 
 262     copy_bytes(w, r, section_size - header_size);
 263     return true;
 264 }
 265 
 266 bool id3pic(FILE* w, bufreader* r) {
 267     // match the ID3 mark
 268     while (true) {
 269         int b = read_byte(r);
 270         if (b == EOF) {
 271             show_error("no thumbnail found\n");
 272             return false;
 273         }
 274 
 275         if (b == 'I' && match(r, "D3")) {
 276             break;
 277         }
 278     }
 279 
 280     while (true) {
 281         int b = read_byte(r);
 282         if (b == EOF) {
 283             show_error("no thumbnail found\n");
 284             return false;
 285         }
 286 
 287         // handle APIC-type chunks
 288         if (b == 'A' && match(r, "PIC")) {
 289             return handle_apic(w, r);
 290         }
 291     }
 292 }
 293 
 294 // handle_file handles data from the filename given; returns false only when
 295 // the file can't be opened
 296 bool handle_file(FILE* w, const char* path, bufreader* r) {
 297     FILE* f = fopen(path, "rb");
 298     if (f == NULL) {
 299         fprintf(stderr, ERROR_LINE("can't open file named '%s'"), path);
 300         return false;
 301     }
 302 
 303     r->src = f;
 304     const bool ok = id3pic(w, r);
 305     fclose(f);
 306     return ok;
 307 }
 308 
 309 // is_help_option simplifies control-flow for func main
 310 bool is_help_option(const char* s) {
 311     return (s[0] == '-') && (
 312         strcmp(s, "-h") == 0 ||
 313         strcmp(s, "-help") == 0 ||
 314         strcmp(s, "--h") == 0 ||
 315         strcmp(s, "--help") == 0
 316     );
 317 }
 318 
 319 bool run(const char* name) {
 320     unsigned char buf[IBUF_SIZE];
 321     bufreader r;
 322     init_bufreader(&r, stdin, buf, sizeof(buf));
 323 
 324     if (name[0] == '-' && name[1] == 0) {
 325         r.src = stdin;
 326         return id3pic(stdout, &r);
 327     }
 328     return handle_file(stdout, name, &r);
 329 }
 330 
 331 int main(int argc, char** argv) {
 332     // handle any of the help options, if given
 333     if (argc > 1 && is_help_option(argv[1])) {
 334         printf("%s", info);
 335         return 0;
 336     }
 337 
 338     if (argc > 2) {
 339         show_error("can only handle 1 file");
 340         return 1;
 341     }
 342 
 343     // enable full/block-buffering for standard output
 344     setvbuf(stdout, NULL, _IOFBF, 0);
 345 
 346     return run(argc == 2 ? argv[1] : "-") ? 0 : 1;
 347 }