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 }