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