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 }