1 /* vim: set et ts=4 sw=4 sts=4 fdm=marker syntax=c.doxygen: */
2
3 /** \file src/lib/sldb.c
4 * \brief Songlength database handling
5 *
6 * \author Bas Wassink <b.wassink@ziggo.nl>
7 */
8
9 /*
10 * HVSClib - a library to work with High Voltage SID Collection files
11 * Copyright (C) 2018 Bas Wassink <b.wassink@ziggo.nl>
12 *
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License along
24 * with this program; if not, write to the Free Software Foundation, Inc.,
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.*
26 */
27
28 #include <stdio.h>
29 #include <stdlib.h>
30 #include <string.h>
31 #include <inttypes.h>
32 #include <ctype.h>
33
34 #ifdef HVSC_USE_MD5
35 # include <gcrypt.h>
36 #endif
37
38 #include "hvsc.h"
39
40 #include "hvsc_defs.h"
41 #include "base.h"
42
43 #include "sldb.h"
44
45
46 #ifdef HVSC_USE_MD5
47
48 /** \brief Calculate MD5 hash of file \a psid
49 *
50 * \param[in] psid PSID file
51 * \param[out] digest memory to store MD5 digest, needs to be 16+ bytes
52 *
53 * \return bool
54 */
create_md5_hash(const char * psid,unsigned char * digest)55 static int create_md5_hash(const char *psid, unsigned char *digest)
56 {
57 unsigned char *data;
58 long size;
59 gcry_md_hd_t handle;
60 gcry_error_t err;
61 unsigned char *d;
62
63 /* attempt to open file */
64 hvsc_dbg("reading '%s\n", psid);
65 size = hvsc_read_file(&data, psid);
66 if (size < 0) {
67 fprintf(stderr, "failed!\n");
68 return 0;
69 }
70 hvsc_dbg("got %ld bytes\n", size);
71
72 /*
73 * calculate MD5 hash
74 */
75 err = gcry_md_open(&handle, GCRY_MD_MD5, 0);
76 if (err != 0) {
77 hvsc_errno = HVSC_ERR_GCRYPT;
78 free(data);
79 return 0;
80 }
81
82 gcry_md_write(handle, data, (size_t)size);
83 d = gcry_md_read(handle, GCRY_MD_MD5);
84 memcpy(digest, d, HVSC_DIGEST_SIZE);
85
86 gcry_md_close(handle);
87
88 free(data);
89
90 return 1;
91 }
92 #endif
93
94
95 #ifdef HVSC_USE_MD5
96 /** \brief Find SLDB entry by \a digest
97 *
98 * The \a digest has to be in the same string form as the SLDB. So 32 bytes
99 * representing a 16-byte hex data, in lower case.
100 *
101 * \param[in] digest string representation of the MD5 digest (32 bytes)
102 *
103 * \return line of text from SLDB or `NULL` when not found
104 */
find_sldb_entry_md5(const char * digest)105 static char *find_sldb_entry_md5(const char *digest)
106 {
107 hvsc_text_file_t handle;
108 const char *line;
109
110 if (!hvsc_text_file_open(hvsc_sldb_path, &handle)) {
111 return NULL;
112 }
113
114 while (1) {
115 line = hvsc_text_file_read(&handle);
116 if (line == NULL) {
117 hvsc_text_file_close(&handle);
118 return NULL;
119 }
120 #if 0
121 printf("%s\n", line);
122 #endif
123 if (memcmp(digest, line, HVSC_DIGEST_SIZE * 2) == 0) {
124 /* copy the current line before closing the file */
125 char *s = hvsc_strdup(handle.buffer);
126 hvsc_text_file_close(&handle);
127 if (s == NULL) {
128 return NULL;
129 }
130 return s;
131 }
132 }
133
134 hvsc_text_file_close(&handle);
135 hvsc_errno = HVSC_ERR_NOT_FOUND;
136 return NULL;
137 }
138 #endif
139
140
141 /** \brief Find song length entry by PSID name in the comments
142 *
143 * \param[in] path relative path in the HVSC to the SID
144 *
145 * \return text line with the song length info or `NULL` on failure
146 */
find_sldb_entry_txt(const char * path)147 static char *find_sldb_entry_txt(const char *path)
148 {
149 hvsc_text_file_t handle;
150 size_t plen;
151 const char *line;
152
153 if (!hvsc_text_file_open(hvsc_sldb_path, &handle)) {
154 return NULL;
155 }
156
157 plen = strlen(path);
158
159 while (1) {
160 line = hvsc_text_file_read(&handle);
161 if (line == NULL) {
162 hvsc_text_file_close(&handle);
163 return NULL;
164 }
165
166
167 if (*line == ';') {
168 if (strncmp(path, line + 2, plen) == 0) {
169 /* next line contains the actual entry */
170 char *s;
171 line = hvsc_text_file_read(&handle);
172 if (line == NULL) {
173 hvsc_text_file_close(&handle);
174 return NULL;
175 }
176 s = hvsc_strdup(handle.buffer);
177 hvsc_text_file_close(&handle);
178 return s;
179 }
180 }
181 }
182
183 hvsc_text_file_close(&handle);
184 hvsc_errno = HVSC_ERR_NOT_FOUND;
185 return NULL;
186 }
187
188
189
190 /** \brief Parse SLDB entry
191 *
192 * The song lengths array is heap-allocated and should freed after use.
193 *
194 * \param[in] line SLDB entry (including hash + '=')
195 * \param[out] lengths object to store pointer to array of song lengths
196 *
197 * \return number of songs or -1 on error
198 */
parse_sldb_entry(char * line,long ** lengths)199 static int parse_sldb_entry(char *line, long **lengths)
200 {
201 char *p;
202 char *endptr;
203 long *entries;
204 int i = 0;
205 long secs;
206
207 entries = malloc(256 * sizeof *entries);
208 if (entries == NULL) {
209 return -1;
210 }
211
212 p = line + (HVSC_DIGEST_SIZE * 2 + 1); /* skip MD5HASH and '=' */
213
214 while (*p != '\0') {
215 /* skip whitespace */
216 while (*p != '\0' && isspace((int)(*p))) {
217 p++;
218 }
219 if (*p == '\0') {
220 *lengths = entries;
221 return i;
222 }
223
224 secs = hvsc_parse_simple_timestamp(p, &endptr);
225 if (secs < 0) {
226 free(entries);
227 return -1;
228 }
229 entries[i++] = secs;
230 p = endptr;
231 }
232
233 *lengths = entries;
234 return i;
235 }
236
237
238
239 #ifdef HVSC_USE_MD5
240 /** \brief Get the SLDB entry for PSID file \a psid
241 *
242 * \param[in] psid path to PSID file
243 *
244 * \return heap-allocated entry or `NULL` on failure
245 */
hvsc_sldb_get_entry_md5(const char * psid)246 char *hvsc_sldb_get_entry_md5(const char *psid)
247 {
248 unsigned char hash[HVSC_DIGEST_SIZE];
249 char hash_text[HVSC_DIGEST_SIZE * 2 + 1];
250 int result;
251 int i;
252 char *entry;
253
254 result = create_md5_hash(psid, hash);
255 if (!result) {
256 return NULL;
257 }
258
259 /* generate text version of hash */
260 hvsc_dbg("HASH = ");
261 for (i = 0; i < HVSC_DIGEST_SIZE; i++) {
262 #ifdef HVSC_DEBUG
263 printf("%02x", hash[i]);
264 #endif
265 snprintf(hash_text + i * 2, 3, "%02x", hash[i]);
266 }
267 #ifdef HVSC_DEBUG
268 putchar('\n');
269 #endif
270
271 /* parse SLDB */
272 entry = find_sldb_entry_md5(hash_text);
273 if (entry == NULL) {
274 return NULL;
275 }
276 hvsc_dbg("Got it: %s\n", entry);
277 return entry;
278 }
279
280 #endif /* ifdef HVSC_USE_MD5 */
281
282
283 /** \brief Find SLDB entry by using text lookup
284 *
285 * This function uses the "; /path/to/file" lines to identify the SID entry,
286 * which makes using/linking against libgcrypt no longer required.
287 *
288 * \param [in] psid absolute path to SID in the HVSC
289 *
290 * \return line of text containing the song length info or `NULL` on failure
291 */
hvsc_sldb_get_entry_txt(const char * psid)292 char *hvsc_sldb_get_entry_txt(const char *psid)
293 {
294 char *path;
295 char *entry;
296
297 /* strip HVSC root from path */
298 path = hvsc_path_strip_root(psid);
299 #if defined(_WIN32) || defined(_WIN64)
300 /* fix directory separators */
301 hvsc_path_fix_separators(path);
302 #endif
303 if (path == NULL) {
304 return NULL;
305 }
306
307 entry = find_sldb_entry_txt(path);
308 free(path);
309 if (entry != NULL) {
310 hvsc_dbg("Got it: %s\n", entry);
311 }
312 return entry;
313 }
314
315
316
317 /** \brief Get a list of song lengths for PSID file \a psid
318 *
319 * \param[in] psid path to PSID file
320 * \param[out] lengths object to store pointer to array of song lengths
321 *
322 * \return number of songs or -1 on error
323 */
hvsc_sldb_get_lengths(const char * psid,long ** lengths)324 int hvsc_sldb_get_lengths(const char *psid, long **lengths)
325 {
326 char *entry;
327 int result;
328
329 *lengths = NULL;
330
331 #ifdef HVSC_USE_MD5
332 entry = hvsc_sldb_get_entry_md5(psid);
333 #else
334 entry = hvsc_sldb_get_entry_txt(psid);
335 #endif
336 if (entry == NULL) {
337 return -1;
338 }
339
340 result = parse_sldb_entry(entry, lengths);
341 if (result < 0) {
342 free(*lengths);
343 return -1;
344 }
345 free(entry);
346 return result;
347 }
348