Mercurial > hg > sidinfo
annotate sidinfo.c @ 19:16cfbdf20eaf
Handle multiple files.
author | Matti Hamalainen <ccr@tnsp.org> |
---|---|
date | Thu, 25 Sep 2014 02:49:24 +0300 |
parents | c30fe2b4251f |
children | 6058339ffe0e |
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 BOOL optParsable = FALSE, | |
7 | 74 optNoNamePrefix = FALSE, |
2 | 75 optHexadecimal = FALSE; |
76 uint32_t optFields = PSF_ALL; | |
19 | 77 int optNFiles = 0; |
2 | 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 typedef struct | |
226 { | |
227 char magic[PSID_MAGIC_LEN + 1]; // "PSID" / "RSID" magic identifier | |
228 uint16_t | |
229 version, // Version number | |
0 | 230 dataOffset, // Start of actual c64 data in file |
231 loadAddress, // Loading address | |
232 initAddress, // Initialization address | |
233 playAddress, // Play one frame | |
234 nSongs, // Number of subsongs | |
235 startSong; // Default starting song | |
236 uint32_t speed; // Speed | |
2 | 237 char sidName[PSID_STR_LEN + 1]; // Descriptive text-fields, ASCIIZ |
238 char sidAuthor[PSID_STR_LEN + 1]; | |
239 char sidCopyright[PSID_STR_LEN + 1]; | |
0 | 240 |
241 // PSIDv2 data | |
242 uint16_t flags; // Flags | |
243 uint8_t startPage, pageLength; | |
244 uint16_t reserved; | |
245 | |
1 | 246 // Extra data |
0 | 247 BOOL isRSID; |
1 | 248 size_t dataSize; // Total size of data - header |
249 th_md5hash_t hash; // Songlength database hash | |
0 | 250 |
251 } PSIDHeader; | |
252 | |
253 | |
2 | 254 static void siAppendHash16(th_md5state_t *state, uint16_t data) |
255 { | |
256 uint8_t ib8[2]; | |
257 ib8[0] = data & 0xff; | |
258 ib8[1] = data >> 8; | |
259 th_md5_append(state, (uint8_t *) &ib8, sizeof(ib8)); | |
260 } | |
261 | |
262 | |
263 int siReadPSIDFile(FILE *inFile, PSIDHeader *psid) | |
0 | 264 { |
1 | 265 th_md5state_t state; |
266 uint8_t tmp8, *fileData = NULL; | |
267 int index, ret = -1; | |
268 size_t read; | |
269 BOOL first; | |
270 | |
271 memset(psid, 0, sizeof(*psid)); | |
272 | |
273 if ((fileData = (uint8_t *) th_malloc(PSID_BUFFER_SIZE)) == NULL) | |
274 { | |
275 THERR("Error allocating temporary data buffer of %d bytes.\n", PSID_BUFFER_SIZE); | |
276 goto error; | |
277 } | |
3 | 278 |
0 | 279 // Read PSID header in |
2 | 280 if (!th_fread_str(inFile, (uint8_t *) psid->magic, PSID_MAGIC_LEN) || |
1 | 281 !th_fread_be16(inFile, &psid->version) || |
282 !th_fread_be16(inFile, &psid->dataOffset) || | |
283 !th_fread_be16(inFile, &psid->loadAddress) || | |
284 !th_fread_be16(inFile, &psid->initAddress) || | |
285 !th_fread_be16(inFile, &psid->playAddress) || | |
286 !th_fread_be16(inFile, &psid->nSongs) || | |
287 !th_fread_be16(inFile, &psid->startSong) || | |
288 !th_fread_be32(inFile, &psid->speed)) | |
0 | 289 { |
1 | 290 THERR("Could not read PSID/RSID header.\n"); |
0 | 291 goto error; |
292 } | |
293 | |
2 | 294 psid->magic[PSID_MAGIC_LEN] = 0; |
295 | |
0 | 296 if ((psid->magic[0] != 'R' && psid->magic[0] != 'P') || |
297 psid->magic[1] != 'S' || psid->magic[2] != 'I' || psid->magic[3] != 'D' || | |
298 psid->version < 1 || psid->version > 3) | |
299 { | |
1 | 300 THERR("Not a supported PSID or RSID file.\n"); |
0 | 301 goto error; |
302 } | |
303 | |
1 | 304 psid->isRSID = psid->magic[0] == 'R'; |
0 | 305 |
2 | 306 if (!th_fread_str(inFile, (uint8_t *)psid->sidName, PSID_STR_LEN) || |
307 !th_fread_str(inFile, (uint8_t *)psid->sidAuthor, PSID_STR_LEN) || | |
308 !th_fread_str(inFile, (uint8_t *)psid->sidCopyright, PSID_STR_LEN)) | |
0 | 309 { |
1 | 310 THERR("Error reading SID file header.\n"); |
0 | 311 goto error; |
312 } | |
313 | |
2 | 314 psid->sidName[PSID_STR_LEN] = 0; |
315 psid->sidAuthor[PSID_STR_LEN] = 0; | |
316 psid->sidCopyright[PSID_STR_LEN] = 0; | |
317 | |
0 | 318 // Check if we need to load PSIDv2NG header ... |
319 if (psid->version >= 2) | |
320 { | |
321 // Yes, we need to | |
1 | 322 if (!th_fread_be16(inFile, &psid->flags) || |
323 !th_fread_byte(inFile, &psid->startPage) || | |
324 !th_fread_byte(inFile, &psid->pageLength) || | |
325 !th_fread_be16(inFile, &psid->reserved)) | |
0 | 326 { |
1 | 327 THERR("Error reading PSID/RSID v2+ extra header data.\n"); |
0 | 328 goto error; |
329 } | |
330 } | |
331 | |
1 | 332 // Initialize MD5-hash calculation |
333 th_md5_init(&state); | |
0 | 334 |
1 | 335 // Process actual data |
336 psid->dataSize = 0; | |
337 first = TRUE; | |
2 | 338 do |
339 { | |
1 | 340 read = fread(fileData, sizeof(uint8_t), PSID_BUFFER_SIZE, inFile); |
341 psid->dataSize += read; | |
0 | 342 |
1 | 343 if (first && psid->loadAddress == 0) |
344 { | |
8
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
345 if (read < 4) |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
346 { |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
347 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
|
348 goto error; |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
349 } |
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
350 |
1 | 351 // Strip load address (2 first bytes) |
352 th_md5_append(&state, &fileData[2], read - 2); | |
353 first = FALSE; | |
354 } | |
355 else | |
8
cfc74ec918dc
Fix handling of "large" SID files.
Matti Hamalainen <ccr@tnsp.org>
parents:
7
diff
changeset
|
356 if (read > 0) |
1 | 357 { |
358 // Append "as is" | |
359 th_md5_append(&state, fileData, read); | |
360 } | |
361 } while (read > 0 && !feof(inFile)); | |
0 | 362 |
1 | 363 // Append header data to hash |
2 | 364 siAppendHash16(&state, psid->initAddress); |
365 siAppendHash16(&state, psid->playAddress); | |
366 siAppendHash16(&state, psid->nSongs); | |
0 | 367 |
368 // Append song speed data to hash | |
1 | 369 tmp8 = psid->isRSID ? 60 : 0; |
0 | 370 for (index = 0; index < psid->nSongs && index < 32; index++) |
371 { | |
1 | 372 if (psid->isRSID) |
373 tmp8 = 60; | |
0 | 374 else |
1 | 375 tmp8 = (psid->speed & (1 << index)) ? 60 : 0; |
0 | 376 |
1 | 377 th_md5_append(&state, &tmp8, sizeof(tmp8)); |
0 | 378 } |
379 | |
380 // Rest of songs (more than 32) | |
381 for (index = 32; index < psid->nSongs; index++) | |
1 | 382 th_md5_append(&state, &tmp8, sizeof(tmp8)); |
0 | 383 |
384 // PSIDv2NG specific | |
385 if (psid->version >= 2) | |
386 { | |
387 // REFER TO SIDPLAY HEADERS FOR MORE INFORMATION | |
1 | 388 tmp8 = (psid->flags >> 2) & 3; |
389 if (tmp8 == 2) | |
390 th_md5_append(&state, &tmp8, sizeof(tmp8)); | |
0 | 391 } |
392 | |
393 // Calculate the hash | |
1 | 394 th_md5_finish(&state, psid->hash); |
395 ret = 0; | |
0 | 396 |
1 | 397 error: |
0 | 398 // Free buffer |
1 | 399 th_free(fileData); |
400 return ret; | |
0 | 401 } |
402 | |
403 | |
2 | 404 static void siPrintLinePrefix(FILE *outFile, const BOOL parsable, const char *name) |
405 { | |
7 | 406 if (!optNoNamePrefix) |
14
0d1bde382e5c
Oops, missed ' : ' from normal output.
Matti Hamalainen <ccr@tnsp.org>
parents:
9
diff
changeset
|
407 fprintf(outFile, parsable ? "%s=" : "%-20s : ", name); |
2 | 408 } |
409 | |
410 | |
411 static void siPrintPSIDInfoLine(FILE *outFile, const BOOL parsable, const BOOL hex, | |
412 const uint32_t flags, const int xindex, const char *xfmt, const char *xaltfmt, ...) | |
413 { | |
414 const PSFOption *opt = &optPSFlags[xindex]; | |
415 if (flags & opt->flag) | |
416 { | |
417 va_list ap; | |
418 const char *fmt = hex ? (xaltfmt != NULL ? xaltfmt : xfmt) : xfmt; | |
419 | |
420 siPrintLinePrefix(outFile, parsable, (parsable || opt->lname == NULL) ? opt->name : opt->lname); | |
421 | |
422 va_start(ap, xaltfmt); | |
423 | |
424 if (parsable) | |
425 vfprintf(outFile, fmt, ap); | |
426 else | |
427 vfprintf(outFile, fmt, ap); | |
428 | |
429 va_end(ap); | |
3 | 430 |
2 | 431 fprintf(outFile, "\n"); |
432 } | |
433 } | |
434 | |
435 #define PR(xindex, xfmt, xaltfmt, ...) siPrintPSIDInfoLine(outFile, parsable, hex, flags, xindex, xfmt, xaltfmt, __VA_ARGS__ ) | |
436 | |
437 | |
9
c1fba4abf56f
Make filename printing optional.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
438 void siPrintPSIDInformation(FILE *outFile, const BOOL parsable, const BOOL hex, |
c1fba4abf56f
Make filename printing optional.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
439 const uint32_t flags, const char *filename, const PSIDHeader *psid) |
2 | 440 { |
16 | 441 PR(13, "%s", NULL, filename); |
9
c1fba4abf56f
Make filename printing optional.
Matti Hamalainen <ccr@tnsp.org>
parents:
8
diff
changeset
|
442 |
2 | 443 PR( 0, "%s", NULL, psid->magic); |
444 PR( 1, "%d.%d", NULL, (psid->version & 0xff), (psid->version >> 8)); | |
445 PR( 2, "%d", "$%08x", psid->dataOffset); | |
446 PR( 3, "%d", "$%08x", psid->dataSize); | |
447 PR( 4, "%d", "$%04x", psid->loadAddress); | |
448 PR( 5, "%d", "$%04x", psid->initAddress); | |
449 PR( 6, "%d", "$%04x", psid->playAddress); | |
450 PR( 7, "%d", "$%04x", psid->nSongs); | |
451 PR( 8, "%d", "$%04x", psid->startSong); | |
452 | |
453 PR( 9, "%s", NULL, psid->sidName); | |
454 PR(10, "%s", NULL, psid->sidAuthor); | |
3 | 455 PR(11, "%s", NULL, psid->sidCopyright); |
2 | 456 |
457 if (flags & PSF_HASH) | |
458 { | |
459 siPrintLinePrefix(outFile, parsable, "Hash"); | |
460 th_md5_print(outFile, psid->hash); | |
461 fprintf(outFile, "\n"); | |
462 } | |
463 } | |
464 | |
0 | 465 |
19 | 466 BOOL argHandleFile(char *filename) |
467 { | |
468 static PSIDHeader psid; | |
469 static FILE *inFile = NULL; | |
470 optNFiles++; | |
471 | |
472 if ((inFile = fopen(filename, "rb")) == NULL) | |
473 { | |
474 THERR("Could not open file '%s'.\n", filename); | |
475 return FALSE; | |
476 } | |
477 | |
478 // Read PSID data | |
479 if (siReadPSIDFile(inFile, &psid) != 0) | |
480 goto error; | |
481 | |
482 // Output | |
483 siPrintPSIDInformation(stdout, optParsable, optHexadecimal, optFields, filename, &psid); | |
484 | |
485 // Shutdown | |
486 error: | |
487 if (inFile != NULL) | |
488 fclose(inFile); | |
489 | |
490 return TRUE; | |
491 } | |
492 | |
493 | |
0 | 494 int main(int argc, char *argv[]) |
495 { | |
496 // Initialize | |
15 | 497 th_init("SIDInfo", "PSID/RSID information displayer", "0.2", NULL, NULL); |
0 | 498 th_verbosityLevel = 0; |
499 | |
2 | 500 // Parse command line arguments |
501 if (!th_args_process(argc, argv, optList, optListN, | |
502 argHandleOpt, argHandleFile, FALSE)) | |
503 return -1; | |
504 | |
19 | 505 if (optNFiles == 0) |
2 | 506 { |
507 argShowHelp(); | |
19 | 508 THERR("No filename(s) specified.\n"); |
2 | 509 } |
1 | 510 |
19 | 511 return 0; |
0 | 512 } |