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