Mercurial > hg > sidinfo
annotate sidinfo.c @ 29:d4ec69beb160
Add one line output format.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Thu, 25 Sep 2014 17:41:24 +0300 |
parents | 6240788fe172 |
children | 727c012d5de4 |
rev | line source |
---|---|
2 | 1 /* |
2 * SIDInfo - PSID/RSID information displayer | |
3 * Written by Matti 'ccr' Hämäläinen | |
4 * (C) Copyright 2014 Tecnic Software productions (TNSP) | |
5 */ | |
0 | 6 #include "th_args.h" |
7 #include "th_endian.h" | |
2 | 8 #include "th_string.h" |
0 | 9 #include "th_crypto.h" |
10 | |
28 | 11 // Some constants |
2 | 12 #define PSID_MAGIC_LEN 4 |
13 #define PSID_STR_LEN 32 | |
1 | 14 #define PSID_BUFFER_SIZE (1024 * 16) |
15 | |
0 | 16 |
28 | 17 // Flags for various information fields |
2 | 18 enum |
19 { | |
20 PSF_NONE = 0, | |
21 | |
22 PSF_TYPE = 0x00000001, | |
23 PSF_VERSION = 0x00000002, | |
24 PSF_DATA_OFFS = 0x00000004, | |
25 PSF_LOAD_ADDR = 0x00000008, | |
26 PSF_INIT_ADDR = 0x00000010, | |
27 PSF_PLAY_ADDR = 0x00000020, | |
28 PSF_SONGS = 0x00000040, | |
29 PSF_START_SONG = 0x00000080, | |
30 PSF_SPEEDS = 0x00000100, | |
31 PSF_SID_NAME = 0x00000200, | |
32 PSF_SID_AUTHOR = 0x00000400, | |
33 PSF_SID_COPYRIGHT = 0x00000800, | |
34 | |
35 PSF_DATA_SIZE = 0x00100000, | |
36 PSF_HASH = 0x00200000, | |
9
c1fba4abf56f
Make filename printing optional.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
37 PSF_FILENAME = 0x10000000, |
2 | 38 |
39 PSF_ALL = 0xffffffff, | |
40 }; | |
41 | |
42 | |
0 | 43 typedef struct |
44 { | |
2 | 45 uint32_t flag; |
46 char *name; | |
47 char *lname; | |
48 } PSFOption; | |
49 | |
50 | |
51 const PSFOption optPSFlags[] = | |
52 { | |
53 { PSF_TYPE , "Type" , NULL }, | |
54 { PSF_VERSION , "Version" , NULL }, | |
55 { PSF_DATA_OFFS , "DataOffs" , "Data offset" }, | |
56 { PSF_DATA_SIZE , "DataSize" , "Data size" }, | |
57 { PSF_LOAD_ADDR , "LoadAddr" , "Load address" }, | |
58 { PSF_INIT_ADDR , "InitAddr" , "Init address" }, | |
59 { PSF_PLAY_ADDR , "PlayAddr" , "Play address" }, | |
60 { PSF_SONGS , "Songs" , "Songs" }, | |
61 { PSF_START_SONG , "StartSong" , "Start song" }, | |
62 { PSF_SID_NAME , "Name" , NULL }, | |
63 { PSF_SID_AUTHOR , "Author" , NULL }, | |
64 { PSF_SID_COPYRIGHT , "Copyright" , NULL }, | |
65 { PSF_HASH , "Hash" , NULL }, | |
9
c1fba4abf56f
Make filename printing optional.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
66 { PSF_FILENAME , "Filename" , NULL }, |
28 | 67 |
68 | |
2 | 69 { PSF_ALL , "All" , NULL }, |
70 }; | |
71 | |
72 const int noptPSFlags = sizeof(optPSFlags) / sizeof(optPSFlags[0]); | |
73 | |
74 | |
28 | 75 // Option variables |
2 | 76 BOOL optParsable = FALSE, |
7 | 77 optNoNamePrefix = FALSE, |
29 | 78 optHexadecimal = FALSE, |
79 optOneLine = FALSE; | |
2 | 80 uint32_t optFields = PSF_ALL; |
19 | 81 int optNFiles = 0; |
2 | 82 |
83 | |
28 | 84 // Define option arguments |
2 | 85 static optarg_t optList[] = |
86 { | |
87 { 0, '?', "help", "Show this help", OPT_NONE }, | |
24
16d6b5029543
Disable verbose option for now, it does not do anything at the moment.
Matti Hamalainen <ccr@tnsp.org>
parents:
23
diff
changeset
|
88 // { 1, 'v', "verbose", "Be more verbose", OPT_NONE }, |
2 | 89 { 2, 'p', "parsable", "Output in script-parsable format", OPT_NONE }, |
7 | 90 { 5, 'n', "noprefix", "Output without field name prefix", OPT_NONE }, |
29 | 91 { 6, 'l', "oneline", "Output in one line format", OPT_NONE }, |
2 | 92 { 3, 'f', "fields", "Show only specified field(s)", OPT_ARGREQ }, |
93 { 4, 'x', "hex", "Use hexadecimal values", OPT_NONE }, | |
94 }; | |
95 | |
96 static const int optListN = (sizeof(optList) / sizeof(optList[0])); | |
97 | |
98 | |
99 void argShowHelp(void) | |
100 { | |
101 int index, n; | |
102 | |
20 | 103 th_print_banner(stdout, th_prog_name, "[options] <sid filename> [sid filename #2 ..]"); |
2 | 104 th_args_help(stdout, optList, optListN); |
105 printf( | |
106 "\n" | |
107 "Available fields:\n"); | |
3 | 108 |
2 | 109 for (index = n = 0; index < noptPSFlags; index++) |
110 { | |
111 const PSFOption *opt = &optPSFlags[index]; | |
112 printf("%s%s", opt->name, (index < noptPSFlags - 1) ? ", " : "\n\n"); | |
113 if (++n > 5) | |
114 { | |
115 printf("\n"); | |
116 n = 0; | |
117 } | |
118 } | |
5 | 119 |
120 printf("Example: %s -x -p -f hash,copyright somesidfile.sid\n", th_prog_name); | |
2 | 121 } |
122 | |
123 | |
124 int argMatchPSField(const char *field) | |
125 { | |
126 int index, found = -1; | |
127 size_t len = strlen(field); | |
128 for (index = 0; index < noptPSFlags; index++) | |
129 { | |
130 const PSFOption *opt = &optPSFlags[index]; | |
131 if (strncasecmp(opt->name, field, len) == 0) | |
132 { | |
133 if (found >= 0) | |
134 return -2; | |
135 found = index; | |
136 } | |
137 } | |
138 | |
139 return found; | |
140 } | |
141 | |
142 | |
143 BOOL argParsePSField(char *opt, char *end, uint32_t *fields) | |
144 { | |
145 // Trim whitespace | |
146 if (end != NULL) | |
147 { | |
148 *end = 0; | |
149 for (end--; end > opt && *end && th_isspace(*end); end--) | |
150 *end = 0; | |
151 } | |
152 while (*opt && th_isspace(*opt)) opt++; | |
153 | |
154 // Match field name | |
155 int found = argMatchPSField(opt); | |
156 switch (found) | |
157 { | |
158 case -1: | |
159 THERR("No such flag '%s'.\n", opt); | |
160 return FALSE; | |
161 | |
162 case -2: | |
163 THERR("Flag '%s' is ambiguous.\n", opt); | |
164 return FALSE; | |
3 | 165 |
2 | 166 default: |
167 *fields |= optPSFlags[found].flag; | |
168 return TRUE; | |
169 } | |
170 } | |
171 | |
172 | |
173 BOOL argHandleOpt(const int optN, char *optArg, char *currArg) | |
174 { | |
175 switch (optN) | |
176 { | |
177 case 0: | |
178 argShowHelp(); | |
179 exit(0); | |
180 break; | |
181 | |
182 case 1: | |
183 th_verbosityLevel++; | |
184 break; | |
185 | |
186 case 2: | |
187 optParsable = TRUE; | |
188 break; | |
189 | |
190 case 3: | |
191 { | |
192 char *start = optArg; | |
193 optFields = PSF_NONE; | |
194 | |
195 while (*start) | |
196 { | |
197 char *end = strchr(start, ','); | |
198 | |
199 if (!argParsePSField(start, end, &optFields)) | |
200 return FALSE; | |
201 | |
202 if (!end) | |
203 break; | |
204 | |
205 start = end + 1; | |
206 } | |
3 | 207 |
2 | 208 //fprintf(stderr, "%08x\n", optFields); |
209 } | |
210 break; | |
211 | |
212 case 4: | |
213 optHexadecimal = TRUE; | |
214 break; | |
215 | |
7 | 216 case 5: |
217 optNoNamePrefix = TRUE; | |
218 break; | |
219 | |
29 | 220 case 6: |
221 optOneLine = TRUE; | |
222 break; | |
223 | |
2 | 224 default: |
225 THERR("Unknown option '%s'.\n", currArg); | |
226 return FALSE; | |
227 } | |
228 | |
229 return TRUE; | |
230 } | |
231 | |
232 | |
233 typedef struct | |
234 { | |
235 char magic[PSID_MAGIC_LEN + 1]; // "PSID" / "RSID" magic identifier | |
236 uint16_t | |
237 version, // Version number | |
0 | 238 dataOffset, // Start of actual c64 data in file |
239 loadAddress, // Loading address | |
240 initAddress, // Initialization address | |
241 playAddress, // Play one frame | |
242 nSongs, // Number of subsongs | |
243 startSong; // Default starting song | |
244 uint32_t speed; // Speed | |
2 | 245 char sidName[PSID_STR_LEN + 1]; // Descriptive text-fields, ASCIIZ |
246 char sidAuthor[PSID_STR_LEN + 1]; | |
247 char sidCopyright[PSID_STR_LEN + 1]; | |
0 | 248 |
249 // PSIDv2 data | |
250 uint16_t flags; // Flags | |
251 uint8_t startPage, pageLength; | |
252 uint16_t reserved; | |
253 | |
1 | 254 // Extra data |
0 | 255 BOOL isRSID; |
1 | 256 size_t dataSize; // Total size of data - header |
257 th_md5hash_t hash; // Songlength database hash | |
0 | 258 |
259 } PSIDHeader; | |
260 | |
261 | |
2 | 262 static void siAppendHash16(th_md5state_t *state, uint16_t data) |
263 { | |
264 uint8_t ib8[2]; | |
265 ib8[0] = data & 0xff; | |
266 ib8[1] = data >> 8; | |
267 th_md5_append(state, (uint8_t *) &ib8, sizeof(ib8)); | |
268 } | |
269 | |
270 | |
271 int siReadPSIDFile(FILE *inFile, PSIDHeader *psid) | |
0 | 272 { |
1 | 273 th_md5state_t state; |
274 uint8_t tmp8, *fileData = NULL; | |
275 int index, ret = -1; | |
276 size_t read; | |
277 BOOL first; | |
278 | |
279 memset(psid, 0, sizeof(*psid)); | |
280 | |
281 if ((fileData = (uint8_t *) th_malloc(PSID_BUFFER_SIZE)) == NULL) | |
282 { | |
283 THERR("Error allocating temporary data buffer of %d bytes.\n", PSID_BUFFER_SIZE); | |
284 goto error; | |
285 } | |
3 | 286 |
0 | 287 // Read PSID header in |
2 | 288 if (!th_fread_str(inFile, (uint8_t *) psid->magic, PSID_MAGIC_LEN) || |
1 | 289 !th_fread_be16(inFile, &psid->version) || |
290 !th_fread_be16(inFile, &psid->dataOffset) || | |
291 !th_fread_be16(inFile, &psid->loadAddress) || | |
292 !th_fread_be16(inFile, &psid->initAddress) || | |
293 !th_fread_be16(inFile, &psid->playAddress) || | |
294 !th_fread_be16(inFile, &psid->nSongs) || | |
295 !th_fread_be16(inFile, &psid->startSong) || | |
296 !th_fread_be32(inFile, &psid->speed)) | |
0 | 297 { |
1 | 298 THERR("Could not read PSID/RSID header.\n"); |
0 | 299 goto error; |
300 } | |
301 | |
2 | 302 psid->magic[PSID_MAGIC_LEN] = 0; |
303 | |
0 | 304 if ((psid->magic[0] != 'R' && psid->magic[0] != 'P') || |
305 psid->magic[1] != 'S' || psid->magic[2] != 'I' || psid->magic[3] != 'D' || | |
306 psid->version < 1 || psid->version > 3) | |
307 { | |
1 | 308 THERR("Not a supported PSID or RSID file.\n"); |
0 | 309 goto error; |
310 } | |
311 | |
1 | 312 psid->isRSID = psid->magic[0] == 'R'; |
0 | 313 |
2 | 314 if (!th_fread_str(inFile, (uint8_t *)psid->sidName, PSID_STR_LEN) || |
315 !th_fread_str(inFile, (uint8_t *)psid->sidAuthor, PSID_STR_LEN) || | |
316 !th_fread_str(inFile, (uint8_t *)psid->sidCopyright, PSID_STR_LEN)) | |
0 | 317 { |
1 | 318 THERR("Error reading SID file header.\n"); |
0 | 319 goto error; |
320 } | |
321 | |
2 | 322 psid->sidName[PSID_STR_LEN] = 0; |
323 psid->sidAuthor[PSID_STR_LEN] = 0; | |
324 psid->sidCopyright[PSID_STR_LEN] = 0; | |
325 | |
0 | 326 // Check if we need to load PSIDv2NG header ... |
327 if (psid->version >= 2) | |
328 { | |
329 // Yes, we need to | |
1 | 330 if (!th_fread_be16(inFile, &psid->flags) || |
331 !th_fread_byte(inFile, &psid->startPage) || | |
332 !th_fread_byte(inFile, &psid->pageLength) || | |
333 !th_fread_be16(inFile, &psid->reserved)) | |
0 | 334 { |
1 | 335 THERR("Error reading PSID/RSID v2+ extra header data.\n"); |
0 | 336 goto error; |
337 } | |
338 } | |
339 | |
1 | 340 // Initialize MD5-hash calculation |
341 th_md5_init(&state); | |
0 | 342 |
1 | 343 // Process actual data |
344 psid->dataSize = 0; | |
345 first = TRUE; | |
2 | 346 do |
347 { | |
1 | 348 read = fread(fileData, sizeof(uint8_t), PSID_BUFFER_SIZE, inFile); |
349 psid->dataSize += read; | |
0 | 350 |
1 | 351 if (first && psid->loadAddress == 0) |
352 { | |
8
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
353 if (read < 4) |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
354 { |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
355 THERR("Error reading song data, unexpectedly small file.\n"); |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
356 goto error; |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
357 } |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
358 |
1 | 359 // Strip load address (2 first bytes) |
360 th_md5_append(&state, &fileData[2], read - 2); | |
361 first = FALSE; | |
362 } | |
363 else | |
8
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
364 if (read > 0) |
1 | 365 { |
366 // Append "as is" | |
367 th_md5_append(&state, fileData, read); | |
368 } | |
369 } while (read > 0 && !feof(inFile)); | |
0 | 370 |
1 | 371 // Append header data to hash |
2 | 372 siAppendHash16(&state, psid->initAddress); |
373 siAppendHash16(&state, psid->playAddress); | |
374 siAppendHash16(&state, psid->nSongs); | |
0 | 375 |
376 // Append song speed data to hash | |
1 | 377 tmp8 = psid->isRSID ? 60 : 0; |
0 | 378 for (index = 0; index < psid->nSongs && index < 32; index++) |
379 { | |
1 | 380 if (psid->isRSID) |
381 tmp8 = 60; | |
0 | 382 else |
1 | 383 tmp8 = (psid->speed & (1 << index)) ? 60 : 0; |
0 | 384 |
1 | 385 th_md5_append(&state, &tmp8, sizeof(tmp8)); |
0 | 386 } |
387 | |
388 // Rest of songs (more than 32) | |
389 for (index = 32; index < psid->nSongs; index++) | |
1 | 390 th_md5_append(&state, &tmp8, sizeof(tmp8)); |
0 | 391 |
392 // PSIDv2NG specific | |
393 if (psid->version >= 2) | |
394 { | |
395 // REFER TO SIDPLAY HEADERS FOR MORE INFORMATION | |
1 | 396 tmp8 = (psid->flags >> 2) & 3; |
397 if (tmp8 == 2) | |
398 th_md5_append(&state, &tmp8, sizeof(tmp8)); | |
0 | 399 } |
400 | |
401 // Calculate the hash | |
1 | 402 th_md5_finish(&state, psid->hash); |
403 ret = 0; | |
0 | 404 |
1 | 405 error: |
0 | 406 // Free buffer |
1 | 407 th_free(fileData); |
408 return ret; | |
0 | 409 } |
410 | |
411 | |
29 | 412 static void siPrintLinePrefix(FILE *outFile, const char *name) |
2 | 413 { |
7 | 414 if (!optNoNamePrefix) |
29 | 415 fprintf(outFile, optParsable ? "%s=" : "%-20s : ", name); |
2 | 416 } |
417 | |
418 | |
29 | 419 static void siPrintPSIDInfoLine(FILE *outFile, BOOL *shown, const int xindex, const char *xfmt, const char *xaltfmt, ...) |
2 | 420 { |
421 const PSFOption *opt = &optPSFlags[xindex]; | |
29 | 422 if (optFields & opt->flag) |
2 | 423 { |
424 va_list ap; | |
29 | 425 const char *fmt = optHexadecimal ? (xaltfmt != NULL ? xaltfmt : xfmt) : xfmt; |
2 | 426 |
29 | 427 siPrintLinePrefix(outFile, (optParsable || opt->lname == NULL) ? opt->name : opt->lname); |
2 | 428 |
429 va_start(ap, xaltfmt); | |
430 | |
29 | 431 if (optParsable) |
2 | 432 vfprintf(outFile, fmt, ap); |
433 else | |
434 vfprintf(outFile, fmt, ap); | |
435 | |
436 va_end(ap); | |
3 | 437 |
29 | 438 fprintf(outFile, optOneLine ? "|" : "\n"); |
439 *shown = TRUE; | |
2 | 440 } |
441 } | |
442 | |
29 | 443 #define PR(xindex, xfmt, xaltfmt, ...) siPrintPSIDInfoLine(outFile, &shown, xindex, xfmt, xaltfmt, __VA_ARGS__ ) |
2 | 444 |
445 | |
29 | 446 void siPrintPSIDInformation(FILE *outFile, const char *filename, const PSIDHeader *psid) |
2 | 447 { |
29 | 448 BOOL shown = FALSE; |
449 | |
16 | 450 PR(13, "%s", NULL, filename); |
9
c1fba4abf56f
Make filename printing optional.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
451 |
2 | 452 PR( 0, "%s", NULL, psid->magic); |
453 PR( 1, "%d.%d", NULL, (psid->version & 0xff), (psid->version >> 8)); | |
454 PR( 2, "%d", "$%08x", psid->dataOffset); | |
455 PR( 3, "%d", "$%08x", psid->dataSize); | |
456 PR( 4, "%d", "$%04x", psid->loadAddress); | |
457 PR( 5, "%d", "$%04x", psid->initAddress); | |
458 PR( 6, "%d", "$%04x", psid->playAddress); | |
459 PR( 7, "%d", "$%04x", psid->nSongs); | |
460 PR( 8, "%d", "$%04x", psid->startSong); | |
461 | |
462 PR( 9, "%s", NULL, psid->sidName); | |
463 PR(10, "%s", NULL, psid->sidAuthor); | |
3 | 464 PR(11, "%s", NULL, psid->sidCopyright); |
2 | 465 |
29 | 466 if (optFields & PSF_HASH) |
2 | 467 { |
29 | 468 siPrintLinePrefix(outFile, "Hash"); |
2 | 469 th_md5_print(outFile, psid->hash); |
29 | 470 fprintf(outFile, optOneLine ? "|" : "\n"); |
471 } | |
472 | |
473 if (shown) | |
2 | 474 fprintf(outFile, "\n"); |
475 } | |
476 | |
0 | 477 |
19 | 478 BOOL argHandleFile(char *filename) |
479 { | |
480 static PSIDHeader psid; | |
481 static FILE *inFile = NULL; | |
482 optNFiles++; | |
483 | |
484 if ((inFile = fopen(filename, "rb")) == NULL) | |
485 { | |
486 THERR("Could not open file '%s'.\n", filename); | |
27
4dd463eebb74
Fix argument handling when file does not exist.
Matti Hamalainen <ccr@tnsp.org>
parents:
25
diff
changeset
|
487 return TRUE; |
19 | 488 } |
489 | |
490 // Read PSID data | |
491 if (siReadPSIDFile(inFile, &psid) != 0) | |
23 | 492 { |
493 THERR("Error reading %s\n", filename); | |
19 | 494 goto error; |
23 | 495 } |
19 | 496 |
497 // Output | |
29 | 498 siPrintPSIDInformation(stdout, filename, &psid); |
19 | 499 |
500 // Shutdown | |
501 error: | |
502 if (inFile != NULL) | |
503 fclose(inFile); | |
504 | |
505 return TRUE; | |
506 } | |
507 | |
508 | |
0 | 509 int main(int argc, char *argv[]) |
510 { | |
511 // Initialize | |
25 | 512 th_init("SIDInfo", "PSID/RSID information displayer", "0.4", NULL, NULL); |
0 | 513 th_verbosityLevel = 0; |
514 | |
2 | 515 // Parse command line arguments |
516 if (!th_args_process(argc, argv, optList, optListN, | |
23 | 517 argHandleOpt, argHandleFile, OPTH_ONLY_OPTS)) |
2 | 518 return -1; |
519 | |
29 | 520 if (optOneLine) |
521 { | |
522 optParsable = FALSE; | |
523 optNoNamePrefix = TRUE; | |
524 } | |
525 | |
23 | 526 // Process files |
527 if (!th_args_process(argc, argv, optList, optListN, | |
528 argHandleOpt, argHandleFile, OPTH_ONLY_OTHER)) | |
529 return -2; | |
530 | |
19 | 531 if (optNFiles == 0) |
2 | 532 { |
533 argShowHelp(); | |
19 | 534 THERR("No filename(s) specified.\n"); |
2 | 535 } |
1 | 536 |
19 | 537 return 0; |
0 | 538 } |