187 lines
5.4 KiB
Zig
187 lines
5.4 KiB
Zig
const std = @import("std");
|
|
|
|
pub const Coordinates = struct {
|
|
latitude: f64,
|
|
longitude: f64,
|
|
};
|
|
|
|
pub const MMDB = extern struct {
|
|
filename: [*:0]const u8,
|
|
flags: u32,
|
|
file_content: ?*anyopaque,
|
|
file_size: usize,
|
|
data_section: ?*anyopaque,
|
|
data_section_size: u32,
|
|
metadata_section: ?*anyopaque,
|
|
metadata_section_size: u32,
|
|
full_record_byte_size: u16,
|
|
depth: u16,
|
|
ipv4_start_node: extern struct {
|
|
node_value: u32,
|
|
netmask: u16,
|
|
},
|
|
metadata: extern struct {
|
|
node_count: u32,
|
|
record_size: u16,
|
|
ip_version: u16,
|
|
database_type: [*:0]const u8,
|
|
languages: extern struct {
|
|
count: usize,
|
|
names: [*][*:0]const u8,
|
|
},
|
|
binary_format_major_version: u16,
|
|
binary_format_minor_version: u16,
|
|
build_epoch: u64,
|
|
description: extern struct {
|
|
count: usize,
|
|
descriptions: [*]?*anyopaque,
|
|
},
|
|
},
|
|
};
|
|
|
|
pub const MMDBLookupResult = extern struct {
|
|
found_entry: bool,
|
|
entry: MMDBEntry,
|
|
netmask: u16,
|
|
};
|
|
|
|
pub const MMDBEntry = extern struct {
|
|
mmdb: *MMDB,
|
|
offset: u32,
|
|
};
|
|
|
|
pub const MMDBEntryData = extern struct {
|
|
has_data: bool,
|
|
data_type: u32,
|
|
offset: u32,
|
|
offset_to_next: u32,
|
|
data_size: u32,
|
|
utf8_string: [*:0]const u8,
|
|
double_value: f64,
|
|
bytes: [*]const u8,
|
|
uint16: u16,
|
|
uint32: u32,
|
|
int32: i32,
|
|
uint64: u64,
|
|
uint128: u128,
|
|
boolean: bool,
|
|
float_value: f32,
|
|
};
|
|
|
|
extern fn MMDB_open(filename: [*:0]const u8, flags: u32, mmdb: *MMDB) c_int;
|
|
extern fn MMDB_close(mmdb: *MMDB) void;
|
|
extern fn MMDB_lookup_string(mmdb: *MMDB, ipstr: [*:0]const u8, gai_error: *c_int, mmdb_error: *c_int) MMDBLookupResult;
|
|
extern fn MMDB_get_value(entry: *MMDBEntry, entry_data: *MMDBEntryData, ...) c_int;
|
|
extern fn MMDB_strerror(error_code: c_int) [*:0]const u8;
|
|
|
|
pub const GeoIP = struct {
|
|
mmdb: MMDB,
|
|
|
|
pub fn init(db_path: []const u8) !GeoIP {
|
|
var mmdb: MMDB = undefined;
|
|
const path_z = try std.heap.c_allocator.dupeZ(u8, db_path);
|
|
defer std.heap.c_allocator.free(path_z);
|
|
|
|
const status = MMDB_open(path_z.ptr, 0, &mmdb);
|
|
if (status != 0) {
|
|
return error.CannotOpenDatabase;
|
|
}
|
|
|
|
return GeoIP{ .mmdb = mmdb };
|
|
}
|
|
|
|
pub fn deinit(self: *GeoIP) void {
|
|
MMDB_close(&self.mmdb);
|
|
}
|
|
|
|
pub fn lookup(self: *GeoIP, ip: []const u8) !?Coordinates {
|
|
const ip_z = try std.heap.c_allocator.dupeZ(u8, ip);
|
|
defer std.heap.c_allocator.free(ip_z);
|
|
|
|
var gai_error: c_int = 0;
|
|
var mmdb_error: c_int = 0;
|
|
|
|
const result = MMDB_lookup_string(&self.mmdb, ip_z.ptr, &gai_error, &mmdb_error);
|
|
|
|
if (gai_error != 0 or mmdb_error != 0) {
|
|
return null;
|
|
}
|
|
|
|
if (!result.found_entry) {
|
|
return null;
|
|
}
|
|
|
|
return try self.extractCoordinates(result.entry);
|
|
}
|
|
|
|
pub fn isUSIP(self: *GeoIP, ip: []const u8) bool {
|
|
const ip_z = std.heap.c_allocator.dupeZ(u8, ip) catch return false;
|
|
defer std.heap.c_allocator.free(ip_z);
|
|
|
|
var gai_error: c_int = 0;
|
|
var mmdb_error: c_int = 0;
|
|
|
|
const result = MMDB_lookup_string(&self.mmdb, ip_z.ptr, &gai_error, &mmdb_error);
|
|
|
|
if (gai_error != 0 or mmdb_error != 0 or !result.found_entry) {
|
|
return false;
|
|
}
|
|
|
|
var entry_mut = result.entry;
|
|
var country_data: MMDBEntryData = undefined;
|
|
const null_term: [*:0]const u8 = @ptrCast(&[_]u8{0});
|
|
const status = MMDB_get_value(&entry_mut, &country_data, "country\x00", "iso_code\x00", null_term);
|
|
|
|
if (status != 0 or !country_data.has_data) {
|
|
return false;
|
|
}
|
|
|
|
const country_code = std.mem.span(country_data.utf8_string);
|
|
return std.mem.eql(u8, country_code, "US");
|
|
}
|
|
|
|
fn extractCoordinates(self: *GeoIP, entry: MMDBEntry) !Coordinates {
|
|
_ = self;
|
|
var entry_mut = entry;
|
|
var latitude_data: MMDBEntryData = undefined;
|
|
var longitude_data: MMDBEntryData = undefined;
|
|
|
|
const lat_status = MMDB_get_value(&entry_mut, &latitude_data, "location", "latitude", @as([*:0]const u8, @ptrCast(&[_]u8{0})));
|
|
const lon_status = MMDB_get_value(&entry_mut, &longitude_data, "location", "longitude", @as([*:0]const u8, @ptrCast(&[_]u8{0})));
|
|
|
|
if (lat_status != 0 or lon_status != 0 or !latitude_data.has_data or !longitude_data.has_data) {
|
|
return error.CoordinatesNotFound;
|
|
}
|
|
|
|
return Coordinates{
|
|
.latitude = latitude_data.double_value,
|
|
.longitude = longitude_data.double_value,
|
|
};
|
|
}
|
|
};
|
|
|
|
test "MMDB functions are callable" {
|
|
const mmdb_error = MMDB_strerror(0);
|
|
try std.testing.expect(mmdb_error[0] != 0);
|
|
}
|
|
|
|
test "GeoIP init with invalid path fails" {
|
|
const result = GeoIP.init("/nonexistent/path.mmdb");
|
|
try std.testing.expectError(error.CannotOpenDatabase, result);
|
|
}
|
|
|
|
test "isUSIP detects US IPs" {
|
|
var geoip = GeoIP.init("./GeoLite2-City.mmdb") catch {
|
|
std.debug.print("Skipping test - GeoLite2-City.mmdb not found\n", .{});
|
|
return error.SkipZigTest;
|
|
};
|
|
defer geoip.deinit();
|
|
|
|
// Test that the function doesn't crash with various IPs
|
|
_ = geoip.isUSIP("8.8.8.8");
|
|
_ = geoip.isUSIP("1.1.1.1");
|
|
|
|
// Test invalid IP returns false
|
|
const invalid = geoip.isUSIP("invalid");
|
|
try std.testing.expect(!invalid);
|
|
}
|