File Coverage

MMDB.xs
Criterion Covered Total %
statement 241 272 88.6
branch 64 104 61.5
condition n/a
subroutine n/a
pod n/a
total 305 376 81.1


line stmt bran cond sub pod time code
1             /*
2             * Read MaxMind DB files
3             *
4             * Copyright (C) 2025 Andreas Vögele
5             *
6             * This module is free software; you can redistribute it and/or modify it
7             * under the same terms as Perl itself.
8             */
9              
10             /* SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later */
11              
12             #define PERL_NO_GET_CONTEXT
13             #include "EXTERN.h"
14             #include "perl.h"
15             #include "XSUB.h"
16              
17             #include
18              
19             #ifdef MULTIPLICITY
20             #define storeTHX(var) (var) = aTHX
21             #define dTHXfield(var) tTHX var;
22             #else
23             #define storeTHX(var) dNOOP
24             #define dTHXfield(var)
25             #endif
26              
27             #define is_ipv6_database(mmdb) (6 == (mmdb)->metadata.ip_version)
28              
29             typedef struct ip_geolocation_mmdb {
30             MMDB_s mmdb;
31             SV *file;
32             SV *selfrv;
33             dTHXfield(perl)
34             } *IP__Geolocation__MMDB;
35              
36             typedef struct {
37             IP__Geolocation__MMDB self;
38             SV *data_callback;
39             SV *node_callback;
40             int max_depth;
41             } iterate_data;
42              
43             static void
44 1           init_iterate_data(iterate_data *data, IP__Geolocation__MMDB self,
45             SV *data_callback, SV *node_callback)
46             {
47 1           data->self = self;
48 1           data->data_callback = data_callback;
49 1           data->node_callback = node_callback;
50 1 50         data->max_depth = is_ipv6_database(&self->mmdb) ? 128 : 32;
51 1           }
52              
53             static SV *
54 16           to_bigint(IP__Geolocation__MMDB self, const char *bytes, size_t size)
55             {
56             dTHXa(self->perl);
57              
58 16           dSP;
59             int count;
60             SV *err_tmp;
61             SV *retval;
62              
63 16           ENTER;
64 16           SAVETMPS;
65              
66 16 50         PUSHMARK(SP);
67 16 50         EXTEND(SP, 2);
68 16           mPUSHs(newRV_inc(self->selfrv));
69 16           mPUSHp(bytes, size);
70 16           PUTBACK;
71 16           count = call_method("_to_bigint", G_SCALAR | G_EVAL);
72 16           SPAGAIN;
73 16 50         err_tmp = ERRSV;
74 16 50         if (SvTRUE(err_tmp)) {
75 0           (void) POPs;
76 0           retval = newSVpvn(bytes, size);
77             }
78             else {
79 16 50         if (1 == count) {
80 16           retval = newSVsv(POPs);
81             }
82             else {
83 0           retval = newSVpvn(bytes, size);
84             }
85             }
86 16           PUTBACK;
87              
88 16 50         FREETMPS;
89 16           LEAVE;
90              
91 16           return retval;
92             }
93              
94             typedef struct {
95             char bytes[16];
96             } numeric_ip;
97              
98             static void
99 1           init_numeric_ip(numeric_ip *ipnum)
100             {
101 1           Zero(ipnum, sizeof(*ipnum), char);
102 1           }
103              
104             static void
105 832           numeric_ip_set_bit(numeric_ip *ipnum, int bit)
106             {
107 832           int quot = bit / (8 * sizeof(char));
108 832           int rem = bit % (8 * sizeof(char));
109 832           ipnum->bytes[15 - quot] |= (128 >> (7 - rem));
110 832           }
111              
112             static SV *
113 5           numeric_ip_to_bigint(IP__Geolocation__MMDB self, const numeric_ip *ipnum)
114             {
115 5           return to_bigint(self, ipnum->bytes, sizeof(ipnum->bytes));
116             }
117              
118             #if MMDB_UINT128_IS_BYTE_ARRAY
119             static SV *
120             createSVu128(IP__Geolocation__MMDB self, uint8_t u[16])
121             {
122             char bytes[16];
123             size_t n;
124             for (n = 0; n < 16; ++n) {
125             bytes[n] = (char) u[n];
126             }
127             return to_bigint(self, bytes, sizeof(bytes));
128             }
129             #else
130             static SV *
131 11           createSVu128(IP__Geolocation__MMDB self, mmdb_uint128_t u)
132             {
133             #ifdef WORDS_BIGENDIAN
134             return to_bigint(self, (const char *) &u, sizeof(u));
135             #else
136             char bytes[sizeof(u)];
137             size_t n;
138 187 100         for (n = 0; n < sizeof(u); ++n) {
139 176           bytes[n] = ((const char *) &u)[sizeof(u) - n - 1];
140             }
141 11           return to_bigint(self, bytes, sizeof(bytes));
142             #endif
143             }
144             #endif
145              
146             #if UVSIZE >= 8
147             #define createSVu64(self, u) newSVuv(u)
148             #else
149             static SV *
150             createSVu64(IP__Geolocation__MMDB self, uint64_t u)
151             {
152             #ifdef WORDS_BIGENDIAN
153             return to_bigint(self, (const char *) &u, sizeof(u));
154             #else
155             char bytes[sizeof(u)];
156             size_t n;
157             for (n = 0; n < sizeof(u); ++n) {
158             bytes[n] = ((const char *) &u)[sizeof(u) - n - 1];
159             }
160             return to_bigint(self, bytes, sizeof(bytes));
161             #endif
162             }
163             #endif
164              
165             static MMDB_entry_data_list_s *
166 993           decode_entry_data_list(IP__Geolocation__MMDB self,
167             MMDB_entry_data_list_s *list, SV **sv, int *mmdb_error)
168             {
169             dTHXa(self->perl);
170 993           MMDB_entry_data_s *data = &list->entry_data;
171 993           switch (data->type) {
172 211           case MMDB_DATA_TYPE_MAP: {
173 211           uint32_t size = data->data_size;
174 211           HV *hv = newHV();
175 211           hv_ksplit(hv, size);
176 1134 100         for (list = list->next; size > 0 && NULL != list; size--) {
    50          
177 923 50         if (MMDB_DATA_TYPE_UTF8_STRING != list->entry_data.type) {
178 0           *mmdb_error = MMDB_INVALID_DATA_ERROR;
179 0           return NULL;
180             }
181 923           const char *key = list->entry_data.utf8_string;
182 923           uint32_t key_size = list->entry_data.data_size;
183 923           list = list->next;
184 923 50         if (NULL == list) {
185 0           *mmdb_error = MMDB_INVALID_DATA_ERROR;
186 0           return NULL;
187             }
188 923           SV *val = &PL_sv_undef;
189 923           list = decode_entry_data_list(self, list, &val, mmdb_error);
190 923 50         if (MMDB_SUCCESS != *mmdb_error) {
191 0           return NULL;
192             }
193 923           (void) hv_store(hv, key, key_size, val, 0);
194             }
195 211           *sv = newRV_noinc((SV *) hv);
196             }
197 211           break;
198              
199 23           case MMDB_DATA_TYPE_ARRAY: {
200 23           uint32_t size = data->data_size;
201 23           AV *av = newAV();
202 23           av_extend(av, size);
203 81 100         for (list = list->next; size > 0 && NULL != list; size--) {
    50          
204 58           SV *val = &PL_sv_undef;
205 58           list = decode_entry_data_list(self, list, &val, mmdb_error);
206 58 50         if (MMDB_SUCCESS != *mmdb_error) {
207 0           return NULL;
208             }
209 58           av_push(av, val);
210             }
211 23           *sv = newRV_noinc((SV *) av);
212             }
213 23           break;
214              
215 346           case MMDB_DATA_TYPE_UTF8_STRING:
216 346           *sv = newSVpvn_utf8(data->utf8_string, data->data_size, 1);
217 346           list = list->next;
218 346           break;
219              
220 11           case MMDB_DATA_TYPE_BYTES:
221 11           *sv = newSVpvn((const char *) data->bytes, data->data_size);
222 11           list = list->next;
223 11           break;
224              
225 33           case MMDB_DATA_TYPE_DOUBLE:
226 33           *sv = newSVnv(data->double_value);
227 33           list = list->next;
228 33           break;
229              
230 11           case MMDB_DATA_TYPE_FLOAT:
231 11           *sv = newSVnv(data->float_value);
232 11           list = list->next;
233 11           break;
234              
235 136           case MMDB_DATA_TYPE_UINT16:
236 136           *sv = newSVuv(data->uint16);
237 136           list = list->next;
238 136           break;
239              
240 111           case MMDB_DATA_TYPE_UINT32:
241 111           *sv = newSVuv(data->uint32);
242 111           list = list->next;
243 111           break;
244              
245 44           case MMDB_DATA_TYPE_BOOLEAN:
246 44           *sv = newSViv(data->boolean);
247 44           list = list->next;
248 44           break;
249              
250 12           case MMDB_DATA_TYPE_UINT64:
251 12           *sv = createSVu64(self, data->uint64);
252 12           list = list->next;
253 12           break;
254              
255 11           case MMDB_DATA_TYPE_UINT128:
256 11           *sv = createSVu128(self, data->uint128);
257 11           list = list->next;
258 11           break;
259              
260 44           case MMDB_DATA_TYPE_INT32:
261 44           *sv = newSViv(data->int32);
262 44           list = list->next;
263 44           break;
264              
265 0           default:
266 0           *mmdb_error = MMDB_INVALID_DATA_ERROR;
267 0           return NULL;
268             }
269              
270 993           *mmdb_error = MMDB_SUCCESS;
271 993           return list;
272             }
273              
274             static void
275 832           call_node_callback(iterate_data *data, uint32_t node_num,
276             MMDB_search_node_s *node)
277             {
278 832           IP__Geolocation__MMDB self = data->self;
279             dTHXa(self->perl);
280              
281 832 50         if (!SvOK(data->node_callback)) {
282 0           return;
283             }
284              
285 832           dSP;
286 832           SV *left_record = createSVu64(self, node->left_record);
287 832           SV *right_record = createSVu64(self, node->right_record);
288              
289 832           ENTER;
290 832           SAVETMPS;
291              
292 832 50         PUSHMARK(SP);
293 832 50         EXTEND(SP, 3);
294 832           mPUSHu(node_num);
295 832           mPUSHs(left_record);
296 832           mPUSHs(right_record);
297 832           PUTBACK;
298 832           (void) call_sv(data->node_callback, G_VOID);
299              
300 832 50         FREETMPS;
301 832           LEAVE;
302             }
303              
304             static void
305 5           call_data_callback(iterate_data *data, numeric_ip ipnum, int depth,
306             MMDB_entry_s *record_entry)
307             {
308 5           IP__Geolocation__MMDB self = data->self;
309             dTHXa(self->perl);
310              
311 5 50         if (!SvOK(data->data_callback)) {
312 0           return;
313             }
314              
315 5           SV *decoded_entry = &PL_sv_undef;
316 5           MMDB_entry_data_list_s *list = NULL;
317 5           int mmdb_error = MMDB_get_entry_data_list(record_entry, &list);
318 5 50         if (MMDB_SUCCESS == mmdb_error) {
319 5           (void) decode_entry_data_list(self, list, &decoded_entry,
320             &mmdb_error);
321             }
322 5           MMDB_free_entry_data_list(list);
323 5 50         if (MMDB_SUCCESS != mmdb_error) {
324 0           const char *error = MMDB_strerror(mmdb_error);
325 0           croak("Entry data error looking at offset %u: %s",
326             (unsigned int) record_entry->offset, error);
327             }
328              
329 5           SV *ip = numeric_ip_to_bigint(self, &ipnum);
330              
331 5           dSP;
332              
333 5           ENTER;
334 5           SAVETMPS;
335              
336 5 50         PUSHMARK(SP);
337 5 50         EXTEND(SP, 3);
338 5           mPUSHs(ip);
339 5           mPUSHi(depth);
340 5           mPUSHs(decoded_entry);
341 5           PUTBACK;
342              
343 5           (void) call_sv(data->data_callback, G_VOID);
344              
345 5 50         FREETMPS;
346 5           LEAVE;
347             }
348              
349             static void iterate_search_nodes(iterate_data *, uint32_t, numeric_ip, int);
350              
351             static void
352 1664           iterate_record_entry(iterate_data *data, numeric_ip ipnum, int depth,
353             uint64_t record, uint8_t record_type,
354             MMDB_entry_s *record_entry)
355             {
356 1664           switch (record_type) {
357 0           case MMDB_RECORD_TYPE_INVALID:
358 0           croak("%s", "Invalid record when reading node");
359             break;
360 831           case MMDB_RECORD_TYPE_SEARCH_NODE:
361 831           iterate_search_nodes(data, (uint32_t) record, ipnum, depth + 1);
362 831           break;
363 828           case MMDB_RECORD_TYPE_EMPTY:
364             /* Empty branches are ignored. */
365 828           break;
366 5           case MMDB_RECORD_TYPE_DATA:
367 5           call_data_callback(data, ipnum, depth, record_entry);
368 5           break;
369 0           default:
370 0           croak("Unknown record type: %u", (unsigned int) record_type);
371             break;
372             }
373 1664           }
374              
375             static void
376 832           iterate_search_nodes(iterate_data *data, uint32_t node_num, numeric_ip ipnum,
377             int depth)
378             {
379             MMDB_search_node_s node;
380 832           int mmdb_error = MMDB_read_node(&data->self->mmdb, node_num, &node);
381 832 50         if (MMDB_SUCCESS != mmdb_error) {
382 0           const char *error = MMDB_strerror(mmdb_error);
383 0           croak("Error reading node %u: %s", (unsigned int) node_num, error);
384             }
385              
386 832 50         if (depth > data->max_depth) {
387 0           croak("Invalid depth when reading node %u: %d", (unsigned int)
388             node_num, depth);
389             }
390              
391 832           call_node_callback(data, node_num, &node);
392              
393 832           iterate_record_entry(data, ipnum, depth, node.left_record,
394 832           node.left_record_type, &node.left_record_entry);
395              
396 832           numeric_ip_set_bit(&ipnum, data->max_depth - depth);
397              
398 832           iterate_record_entry(data, ipnum, depth, node.right_record,
399 832           node.right_record_type, &node.right_record_entry);
400 832           }
401              
402             MODULE = IP::Geolocation::MMDB PACKAGE = IP::Geolocation::MMDB
403              
404             PROTOTYPES: DISABLE
405              
406             SV *
407             new(klass, ...)
408             SV *klass
409             INIT:
410             IP__Geolocation__MMDB self;
411 3           SV *file = NULL;
412 3           U32 flags = 0;
413             I32 i;
414             const char *key;
415             SV *value;
416             const char *filename;
417             int mmdb_error;
418             const char *error;
419             CODE:
420 3 50         if ((items - 1) % 2 != 0) {
421 0           warn("Odd-length list passed to %" SVf " constructor", SVfARG(klass));
422             }
423              
424 5 100         for (i = 1; i < items; i += 2) {
425 2           key = SvPV_nolen_const(ST(i));
426 2           value = ST(i + 1);
427 2 50         if (strEQ(key, "file")) {
428 2           file = value;
429             }
430             }
431              
432 3 100         if (NULL == file) {
433 1           croak("The \"file\" parameter is mandatory");
434             }
435              
436 2           filename = SvPVbyte_nolen(file);
437              
438 2           Newxz(self, 1, struct ip_geolocation_mmdb);
439             storeTHX(self->perl);
440 2           self->file = SvREFCNT_inc(file);
441              
442 2           mmdb_error = MMDB_open(filename, flags, &self->mmdb);
443 2 100         if (MMDB_SUCCESS != mmdb_error) {
444 1           Safefree(self);
445 1           error = MMDB_strerror(mmdb_error);
446 1           croak("Error opening database file \"%" SVf "\": %s", SVfARG(file),
447             error);
448             }
449              
450 1           RETVAL = sv_bless(newRV_noinc(newSViv(PTR2IV(self))),
451             gv_stashsv(klass, GV_ADD));
452 1           self->selfrv = SvRV(RETVAL); /* no inc */
453             OUTPUT:
454             RETVAL
455              
456             void
457             DESTROY(self)
458             IP::Geolocation::MMDB self
459             CODE:
460 1           MMDB_close(&self->mmdb);
461 1           SvREFCNT_dec(self->file);
462 1           Safefree(self);
463              
464             void
465             get(self, ...)
466             IP::Geolocation::MMDB self
467             ALIAS:
468             record_for_address = 1
469             INIT:
470             const char *ip_address;
471             int gai_error, mmdb_error;
472             const char *error;
473             MMDB_lookup_result_s result;
474             MMDB_entry_data_list_s *list;
475 8           SV *data = &PL_sv_undef;
476 8           int wants_prefix_length = 0;
477 8           uint16_t prefix_length = 0;
478 8           U8 gimme = GIMME_V;
479             PPCODE:
480 8           ip_address = NULL;
481 8 50         if (items > 1) {
482 8           ip_address = SvPVbyte_nolen(ST(1));
483             }
484 8 50         if (NULL == ip_address || '\0' == *ip_address) {
    50          
485 0           croak("%s", "You must provide an IP address to look up");
486             }
487             result =
488 8           MMDB_lookup_string(&self->mmdb, ip_address, &gai_error, &mmdb_error);
489 8 100         if (0 != gai_error) {
490 1           croak("The IP address you provided (%s) is not a valid IPv4 or IPv6 "
491             "address", ip_address);
492             }
493 7 50         if (MMDB_SUCCESS != mmdb_error) {
494 0           error = MMDB_strerror(mmdb_error);
495 0           croak("Error looking up IP address \"%s\": %s", ip_address, error);
496             }
497 7 100         if (result.found_entry) {
498 6           list = NULL;
499 6           mmdb_error = MMDB_get_entry_data_list(&result.entry, &list);
500 6 50         if (MMDB_SUCCESS == mmdb_error) {
501 6           (void) decode_entry_data_list(self, list, &data, &mmdb_error);
502             }
503 6           MMDB_free_entry_data_list(list);
504 6 50         if (MMDB_SUCCESS != mmdb_error) {
505 0           error = MMDB_strerror(mmdb_error);
506 0           croak("Entry data error looking up \"%s\": %s", ip_address,
507             error);
508             }
509 6 100         if (0 == ix && G_SCALAR != gimme) {
    100          
510 2           wants_prefix_length = 1;
511 2 100         if (instr(ip_address, ".") && is_ipv6_database(&self->mmdb)) {
    50          
512 1           prefix_length = result.netmask - 96;
513             }
514             else {
515 1           prefix_length = result.netmask;
516             }
517             }
518             }
519 7 50         XPUSHs(sv_2mortal(data));
520 7 100         if (wants_prefix_length) {
521 2 50         XPUSHs(sv_2mortal(newSVuv(prefix_length)));
522             }
523              
524             void
525             iterate_search_tree(self, ...)
526             IP::Geolocation::MMDB self
527             INIT:
528             SV *data_callback;
529             SV *node_callback;
530             iterate_data data;
531             numeric_ip ipnum;
532             CODE:
533 1           data_callback = &PL_sv_undef;
534 1           node_callback = &PL_sv_undef;
535 1 50         if (items > 1) {
536 1           data_callback = ST(1);
537 1 50         if (items > 2) {
538 1           node_callback = ST(2);
539             }
540             }
541 1           init_iterate_data(&data, self, data_callback, node_callback);
542 1           init_numeric_ip(&ipnum);
543 1           iterate_search_nodes(&data, 0, ipnum, 1);
544              
545             SV *
546             _metadata(self)
547             IP::Geolocation::MMDB self
548             INIT:
549             int mmdb_error;
550             const char *error;
551             MMDB_entry_data_list_s *list;
552             CODE:
553 1           RETVAL = &PL_sv_undef;
554 1           list = NULL;
555 1           mmdb_error = MMDB_get_metadata_as_entry_data_list(&self->mmdb, &list);
556 1 50         if (MMDB_SUCCESS == mmdb_error) {
557 1           (void) decode_entry_data_list(self, list, &RETVAL, &mmdb_error);
558             }
559 1           MMDB_free_entry_data_list(list);
560 1 50         if (MMDB_SUCCESS != mmdb_error) {
561 0           error = MMDB_strerror(mmdb_error);
562 0           croak("Error getting metadata: %s", error);
563             }
564             OUTPUT:
565             RETVAL
566              
567             SV *
568             file(self)
569             IP::Geolocation::MMDB self
570             CODE:
571 1           RETVAL = SvREFCNT_inc(self->file);
572             OUTPUT:
573             RETVAL
574              
575             const char *
576             libmaxminddb_version()
577             CODE:
578 1           RETVAL = MMDB_lib_version();
579             OUTPUT:
580             RETVAL