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