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