Split database into a writer and reader
[people/ms/libloc.git] / src / database.c
1 /*
2         libloc - A library to determine the location of someone on the Internet
3
4         Copyright (C) 2017 IPFire Development Team <info@ipfire.org>
5
6         This library is free software; you can redistribute it and/or
7         modify it under the terms of the GNU Lesser General Public
8         License as published by the Free Software Foundation; either
9         version 2.1 of the License, or (at your option) any later version.
10
11         This library is distributed in the hope that it will be useful,
12         but WITHOUT ANY WARRANTY; without even the implied warranty of
13         MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14         Lesser General Public License for more details.
15 */
16
17 #include <arpa/inet.h>
18 #include <errno.h>
19 #include <stddef.h>
20 #include <stdint.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <sys/mman.h>
25 #include <sys/types.h>
26 #include <time.h>
27 #include <unistd.h>
28
29 #include <loc/libloc.h>
30 #include <loc/format.h>
31
32 #include "libloc-private.h"
33 #include "as.h"
34 #include "database.h"
35 #include "stringpool.h"
36
37 struct loc_database {
38         struct loc_ctx* ctx;
39         int refcount;
40
41         FILE* file;
42         unsigned int version;
43         time_t created_at;
44         off_t vendor;
45         off_t description;
46
47         // ASes in the database
48         struct loc_database_as_v0* as_v0;
49         size_t as_count;
50
51         struct loc_stringpool* pool;
52 };
53
54 static int loc_database_read_magic(struct loc_database* db) {
55         struct loc_database_magic magic;
56
57         // Read from file
58         size_t bytes_read = fread(&magic, 1, sizeof(magic), db->file);
59
60         // Check if we have been able to read enough data
61         if (bytes_read < sizeof(magic)) {
62                 ERROR(db->ctx, "Could not read enough data to validate magic bytes\n");
63                 DEBUG(db->ctx, "Read %zu bytes, but needed %zu\n", bytes_read, sizeof(magic));
64                 return -ENOMSG;
65         }
66
67         // Compare magic bytes
68         if (memcmp(LOC_DATABASE_MAGIC, magic.magic, strlen(LOC_DATABASE_MAGIC)) == 0) {
69                 DEBUG(db->ctx, "Magic value matches\n");
70
71                 // Parse version
72                 db->version = ntohs(magic.version);
73                 DEBUG(db->ctx, "Database version is %u\n", db->version);
74
75                 return 0;
76         }
77
78         ERROR(db->ctx, "Database format is not compatible\n");
79
80         // Return an error
81         return 1;
82 }
83
84 static int loc_database_read_as_section_v0(struct loc_database* db,
85                 off_t as_offset, size_t as_length) {
86         DEBUG(db->ctx, "Reading AS section from %jd (%zu bytes)\n", as_offset, as_length);
87
88         if (as_length > 0) {
89                 db->as_v0 = mmap(NULL, as_length, PROT_READ,
90                         MAP_SHARED, fileno(db->file), as_offset);
91
92                 if (db->as_v0 == MAP_FAILED)
93                         return -errno;
94         }
95
96         db->as_count = as_length / sizeof(*db->as_v0);
97
98         INFO(db->ctx, "Read %zu ASes from the database\n", db->as_count);
99
100         return 0;
101 }
102
103 static int loc_database_read_header_v0(struct loc_database* db) {
104         struct loc_database_header_v0 header;
105
106         // Read from file
107         size_t size = fread(&header, 1, sizeof(header), db->file);
108
109         if (size < sizeof(header)) {
110                 ERROR(db->ctx, "Could not read enough data for header\n");
111                 return -ENOMSG;
112         }
113
114         // Copy over data
115         db->created_at  = be64toh(header.created_at);
116         db->vendor      = ntohl(header.vendor);
117         db->description = ntohl(header.description);
118
119         // Open pool
120         off_t pool_offset  = ntohl(header.pool_offset);
121         size_t pool_length = ntohl(header.pool_length);
122
123         int r = loc_stringpool_new(db->ctx, &db->pool, 0);
124         if (r)
125                 return r;
126
127         r = loc_stringpool_read(db->pool, db->file, pool_offset, pool_length);
128         if (r)
129                 return r;
130
131         // AS section
132         off_t as_offset  = ntohl(header.as_offset);
133         size_t as_length = ntohl(header.as_length);
134
135         r = loc_database_read_as_section_v0(db, as_offset, as_length);
136         if (r)
137                 return r;
138
139         return 0;
140 }
141
142 static int loc_database_read_header(struct loc_database* db) {
143         switch (db->version) {
144                 case 0:
145                         return loc_database_read_header_v0(db);
146
147                 default:
148                         ERROR(db->ctx, "Incompatible database version: %u\n", db->version);
149                         return 1;
150         }
151 }
152
153 static FILE* copy_file_pointer(FILE* f) {
154         int fd = fileno(f);
155
156         // Make a copy
157         fd = dup(fd);
158
159         return fdopen(fd, "r");
160 }
161
162 LOC_EXPORT int loc_database_new(struct loc_ctx* ctx, struct loc_database** database, FILE* f) {
163         struct loc_database* db = calloc(1, sizeof(*db));
164         if (!db)
165                 return -ENOMEM;
166
167         // Reference context
168         db->ctx = loc_ref(ctx);
169         db->refcount = 1;
170
171         DEBUG(db->ctx, "Database object allocated at %p\n", db);
172
173         // Copy the file pointer and work on that so we don't care if
174         // the calling function closes the file
175         db->file = copy_file_pointer(f);
176         if (!db->file)
177                 goto FAIL;
178
179         // Read magic bytes
180         int r = loc_database_read_magic(db);
181         if (r)
182                 return r;
183
184         // Read the header
185         r = loc_database_read_header(db);
186         if (r)
187                 return r;
188
189         *database = db;
190
191         return 0;
192
193 FAIL:
194         loc_database_unref(db);
195
196         return -errno;
197 }
198
199 LOC_EXPORT struct loc_database* loc_database_ref(struct loc_database* db) {
200         db->refcount++;
201
202         return db;
203 }
204
205 static void loc_database_free(struct loc_database* db) {
206         DEBUG(db->ctx, "Releasing database %p\n", db);
207
208         // Removing all ASes
209         if (db->as_v0) {
210                 int r = munmap(db->as_v0, db->as_count * sizeof(*db->as_v0));
211                 if (r)
212                         ERROR(db->ctx, "Could not unmap AS section: %s\n", strerror(errno));
213         }
214
215         loc_stringpool_unref(db->pool);
216
217         // Close file
218         if (db->file)
219                 fclose(db->file);
220
221         loc_unref(db->ctx);
222         free(db);
223 }
224
225 LOC_EXPORT struct loc_database* loc_database_unref(struct loc_database* db) {
226         if (--db->refcount > 0)
227                 return NULL;
228
229         loc_database_free(db);
230         return NULL;
231 }
232
233 LOC_EXPORT time_t loc_database_created_at(struct loc_database* db) {
234         return db->created_at;
235 }
236
237 LOC_EXPORT const char* loc_database_get_vendor(struct loc_database* db) {
238         return loc_stringpool_get(db->pool, db->vendor);
239 }
240
241 LOC_EXPORT const char* loc_database_get_description(struct loc_database* db) {
242         return loc_stringpool_get(db->pool, db->description);
243 }
244
245 LOC_EXPORT size_t loc_database_count_as(struct loc_database* db) {
246         return db->as_count;
247 }
248
249 // Returns the AS at position pos
250 static int loc_database_fetch_as(struct loc_database* db, struct loc_as** as, off_t pos) {
251         if ((size_t)pos >= db->as_count)
252                 return -EINVAL;
253
254         DEBUG(db->ctx, "Fetching AS at position %jd\n", pos);
255
256         int r;
257         switch (db->version) {
258                 case 0:
259                         r = loc_as_new_from_database_v0(db->ctx, db->pool, as, db->as_v0 + pos);
260                         break;
261
262                 default:
263                         return -1;
264         }
265
266         if (r == 0) {
267                 DEBUG(db->ctx, "Got AS%u\n", loc_as_get_number(*as));
268         }
269
270         return r;
271 }
272
273 // Performs a binary search to find the AS in the list
274 LOC_EXPORT int loc_database_get_as(struct loc_database* db, struct loc_as** as, uint32_t number) {
275         off_t lo = 0;
276         off_t hi = db->as_count - 1;
277
278         while (lo <= hi) {
279                 off_t i = (lo + hi) / 2;
280
281                 // Fetch AS in the middle between lo and hi
282                 int r = loc_database_fetch_as(db, as, i);
283                 if (r)
284                         return r;
285
286                 // Check if this is a match
287                 uint32_t as_number = loc_as_get_number(*as);
288                 if (as_number == number)
289                         return 0;
290
291                 // If it wasn't, we release the AS and
292                 // adjust our search pointers
293                 loc_as_unref(*as);
294
295                 if (as_number < number) {
296                         lo = i + 1;
297                 } else
298                         hi = i - 1;
299         }
300
301         // Nothing found
302         *as = NULL;
303
304         return 0;
305 }