ip2ldb-reader
Version:
Reader for IP2Location databases
975 lines (964 loc) • 31.6 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
Ip2lReader: () => Ip2lReader,
default: () => Ip2lReader
});
module.exports = __toCommonJS(index_exports);
// src/db-reader.ts
var import_node_fs = __toESM(require("fs"), 1);
// src/ip-utils.ts
var import_node_net = __toESM(require("net"), 1);
var FROM_6TO4 = BigInt("42545680458834377588178886921629466624");
var TO_6TO4 = BigInt("42550872755692912415807417417958686719");
var FROM_TEREDO = BigInt("42540488161975842760550356425300246528");
var TO_TEREDO = BigInt("42540488241204005274814694018844196863");
var LAST_32BITS = BigInt("4294967295");
function parseIp(ip) {
let ipVersion = import_node_net.default.isIP(ip);
let ipNum = BigInt(0);
if (ipVersion === 6) {
if (/^[:0]+:F{4}:(\d+\.){3}\d+$/i.test(ip)) {
ip = ip.replace(/^[:0]+:F{4}:/i, "");
ipVersion = import_node_net.default.isIP(ip);
} else if (/^[:0]+F{4}(:[\dA-Z]{4}){2}$/i.test(ip)) {
const tmp = ip.replace(/^[:0]+F{4}:/i, "").replace(/:/, "");
ip = (tmp.match(/../g) ?? []).map((b) => parseInt("0x" + b)).join(".");
ipVersion = import_node_net.default.isIP(ip);
}
}
if (ipVersion) {
({ ipNum, ipVersion } = getIpNum(ip, ipVersion));
}
return { ip, ipVersion, ipNum };
}
function getIpNum(ip, ipVersion) {
let ipNum = BigInt(0);
if (ipVersion === 4) {
const d = ip.split(".");
ipNum = BigInt(((+d[0] * 256 + +d[1]) * 256 + +d[2]) * 256 + +d[3]);
} else if (ipVersion === 6) {
const maxsections = 8;
const sectionbits = 16;
const m = ip.split("::");
let total = BigInt(0);
if (m.length === 2) {
const arrLeft = m[0] !== "" ? m[0].split(":") : [];
const arrRight = m[1] !== "" ? m[1].split(":") : [];
for (let x = 0; x < arrLeft.length; x++) {
total += BigInt(parseInt("0x" + arrLeft[x])) << BigInt((maxsections - (x + 1)) * sectionbits);
}
for (let x = 0; x < arrRight.length; x++) {
total += BigInt(parseInt("0x" + arrRight[x])) << BigInt((arrRight.length - (x + 1)) * sectionbits);
}
} else if (m.length === 1) {
const arr = m[0].split(":");
for (let x = 0; x < arr.length; x++) {
total += BigInt(parseInt("0x" + arr[x])) << BigInt((maxsections - (x + 1)) * sectionbits);
}
}
ipNum = total;
if (ipNum >= FROM_6TO4 && ipNum <= TO_6TO4) {
ipNum = ipNum >> BigInt(80) & LAST_32BITS;
ipVersion = 4;
} else if (ipNum >= FROM_TEREDO && ipNum <= TO_TEREDO) {
ipNum = ~ipNum & LAST_32BITS;
ipVersion = 4;
}
}
return { ipNum, ipVersion };
}
// src/db-reader.ts
var Position = {
country: [0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2],
region: [0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3],
city: [0, 0, 0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4],
isp: [0, 0, 3, 0, 5, 0, 7, 5, 7, 0, 8, 0, 9, 0, 9, 0, 9, 0, 9, 7, 9, 0, 9, 7, 9, 9, 9],
latitude: [0, 0, 0, 0, 0, 5, 5, 0, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5],
longitude: [0, 0, 0, 0, 0, 6, 6, 0, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6],
domain: [0, 0, 0, 0, 0, 0, 0, 6, 8, 0, 9, 0, 10, 0, 10, 0, 10, 0, 10, 8, 10, 0, 10, 8, 10, 10, 10],
zipcode: [0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 7, 0, 7, 7, 7, 0, 7, 0, 7, 7, 7, 0, 7, 7, 7],
timezone: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 7, 8, 8, 8, 7, 8, 0, 8, 8, 8, 0, 8, 8, 8],
netspeed: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 11, 0, 11, 8, 11, 0, 11, 0, 11, 0, 11, 11, 11],
iddcode: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 12, 0, 12, 0, 12, 9, 12, 0, 12, 12, 12],
areacode: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 13, 0, 13, 0, 13, 10, 13, 0, 13, 13, 13],
weatherstationcode: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 14, 0, 14, 0, 14, 0, 14, 14, 14],
weatherstationname: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 15, 0, 15, 0, 15, 0, 15, 15, 15],
mcc: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 16, 0, 16, 9, 16, 16, 16],
mnc: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 17, 0, 17, 10, 17, 17, 17],
mobilebrand: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 18, 0, 18, 11, 18, 18, 18],
elevation: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 19, 0, 19, 19, 19],
usagetype: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 20, 20, 20],
addresstype: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 21],
category: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22, 22],
district: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 23],
asn: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24],
as: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 25]
};
var MAX_SIZE = 65536;
var MAX_IPV4_RANGE = BigInt("4294967295");
var MAX_IPV6_RANGE = BigInt("340282366920938463463374607431768211455");
var DbReader = class {
readerStatus_;
dbPath_;
cacheInMemory_;
fd_;
dbCache_;
fsWatcher_;
indiciesIPv4_;
indiciesIPv6_;
offset_;
enabled_;
dbStats_;
constructor() {
this.readerStatus_ = 0 /* NotInitialized */;
this.dbPath_ = null;
this.cacheInMemory_ = false;
this.fd_ = null;
this.dbCache_ = null;
this.fsWatcher_ = null;
this.indiciesIPv4_ = [];
this.indiciesIPv6_ = [];
this.offset_ = {};
this.enabled_ = {};
this.dbStats_ = {
DBType: 0,
DBColumn: 0,
DBYear: 0,
DBMonth: 0,
DBDay: 0,
DBCount: 0,
DBCountIPv6: 0,
BaseAddr: 0,
BaseAddrIPv6: 0,
IndexBaseAddr: 0,
IndexBaseAddrIPv6: 0,
ColumnSize: 0,
ColumnSizeIPv6: 0,
ProductCode: 0,
ProductType: 0,
FileSize: 0,
Indexed: false,
IndexedIPv6: false,
OldBIN: false
};
}
/**
* Read data from database into a buffer
* @param readbytes Number of bytes to read
* @param pos Offset from beginning of database
*/
readToBuffer(readbytes, pos) {
if (this.dbCache_) {
const buff2 = this.dbCache_.subarray(pos, pos + readbytes);
return buff2.length === readbytes ? buff2 : void 0;
}
if (!this.fd_) {
throw new Error("Missing file descriptor, cannot read data");
}
const buff = Buffer.alloc(readbytes);
const totalread = import_node_fs.default.readSync(this.fd_, buff, 0, readbytes, pos);
return totalread === readbytes ? buff : void 0;
}
/**
* Read 8-bit integer from the database
* @param pos Offset from beginning of database
*/
readInt8(pos) {
const buff = this.readToBuffer(1, pos - 1);
return buff ? buff.readUInt8(0) : void 0;
}
/**
* Read 32-bit integer from the database
* @param pos Offset from beginning of database
*/
readInt32(pos) {
const buff = this.readToBuffer(4, pos - 1);
return buff ? buff.readUInt32LE(0) : void 0;
}
/**
* Read 32-bit integer from the database as a BigInt
* @param pos Offset from beginning of database
*/
readInt32Big(pos) {
const buff = this.readToBuffer(4, pos - 1);
return buff ? BigInt(buff.readUInt32LE(0)) : void 0;
}
/**
* Read 32-bit float from the database
* @param pos Offset from beginning of database
*/
readFloat(pos) {
const buff = this.readToBuffer(4, pos - 1);
return buff ? buff.readFloatLE(0) : void 0;
}
/**
* Read 128-bit integer from the database as a BigInt
* @param pos Offset from beginning of database
*/
readInt128Big(pos) {
const buff = this.readToBuffer(16, pos - 1);
if (!buff) {
return;
}
let ret = BigInt(0);
for (let x = 0; x < 16; x++) {
ret += BigInt(buff.readUInt8(x)) << BigInt(8 * x);
}
return ret;
}
/**
* Read string from the database
* @param pos Offset from beginning of database
*/
readString(pos) {
const strBytes = this.readInt8(pos + 1);
if (!strBytes) {
return;
}
const buff = this.readToBuffer(strBytes, pos + 1);
const str = buff ? buff.toString("utf8") : void 0;
return str === "-" ? "" : str;
}
/**
* Read 32-bit integer from a Buffer
* @param pos Offset from beginning of buffer
* @param buff Buffer
*/
readBufferInt32(pos, buff) {
return buff.readUInt32LE(pos);
}
/**
* Read 32-bit float from a buffer
* @param pos Offset from beginning of buffer
* @param buff Buffer
*/
readBufferFloat(pos, buff) {
return buff.readFloatLE(pos);
}
/**
* (Re)load database
*/
loadDatabase() {
if (!this.dbPath_) {
throw new Error("Path to database not available");
}
this.readerStatus_ = 1 /* Initializing */;
if (this.fd_ !== null) {
try {
import_node_fs.default.closeSync(this.fd_);
} catch {
}
}
if (this.cacheInMemory_) {
this.fd_ = null;
this.dbCache_ = import_node_fs.default.readFileSync(this.dbPath_);
} else {
this.fd_ = import_node_fs.default.openSync(this.dbPath_, "r");
this.dbCache_ = null;
}
this.dbStats_.DBType = this.readInt8(1) ?? 0;
this.dbStats_.DBColumn = this.readInt8(2) ?? 0;
this.dbStats_.DBYear = this.readInt8(3) ?? 0;
this.dbStats_.DBMonth = this.readInt8(4) ?? 0;
this.dbStats_.DBDay = this.readInt8(5) ?? 0;
this.dbStats_.DBCount = this.readInt32(6) ?? 0;
this.dbStats_.BaseAddr = this.readInt32(10) ?? 0;
this.dbStats_.DBCountIPv6 = this.readInt32(14) ?? 0;
this.dbStats_.BaseAddrIPv6 = this.readInt32(18) ?? 0;
this.dbStats_.IndexBaseAddr = this.readInt32(22) ?? 0;
this.dbStats_.IndexBaseAddrIPv6 = this.readInt32(26) ?? 0;
this.dbStats_.ProductCode = this.readInt8(30) ?? 0;
this.dbStats_.ProductType = this.readInt8(31) ?? 0;
this.dbStats_.FileSize = this.readInt32(32) ?? 0;
if (this.dbStats_.ProductCode !== 1 && this.dbStats_.DBYear >= 21) {
throw new Error(
"Incorrect IP2Location BIN file format. Please make sure that you are using the latest IP2Location BIN file."
);
}
if (this.dbStats_.DBType == "P".charCodeAt(0) && this.dbStats_.DBColumn === "K".charCodeAt(0)) {
throw new Error("Incorrect IP2Location BIN file format. Please uncompress zip file.");
}
this.dbStats_.Indexed = this.dbStats_.IndexBaseAddr > 0;
this.dbStats_.OldBIN = !this.dbStats_.DBCountIPv6;
this.dbStats_.IndexedIPv6 = !this.dbStats_.OldBIN && this.dbStats_.IndexBaseAddrIPv6 > 0;
this.dbStats_.ColumnSize = this.dbStats_.DBColumn << 2;
this.dbStats_.ColumnSizeIPv6 = 16 + (this.dbStats_.DBColumn - 1 << 2);
const dbType = this.dbStats_.DBType;
for (const key of Object.keys(Position)) {
this.offset_[key] = Position[key][dbType] ? Position[key][dbType] - 2 << 2 : 0;
this.enabled_[key] = Boolean(Position[key][dbType]);
}
this.indiciesIPv4_ = new Array(MAX_SIZE);
this.indiciesIPv6_ = new Array(MAX_SIZE);
if (this.dbStats_.Indexed) {
let pointer = this.dbStats_.IndexBaseAddr;
for (let x = 0; x < MAX_SIZE; x++) {
this.indiciesIPv4_[x] = [this.readInt32(pointer) ?? 0, this.readInt32(pointer + 4) ?? 0];
pointer += 8;
}
if (this.dbStats_.IndexedIPv6) {
for (let x = 0; x < MAX_SIZE; x++) {
this.indiciesIPv6_[x] = [this.readInt32(pointer) ?? 0, this.readInt32(pointer + 4) ?? 0];
pointer += 8;
}
}
}
this.readerStatus_ = 2 /* Ready */;
}
/**
* Get reader status
*/
get readerStatus() {
return this.readerStatus_;
}
/**
* Close database and uninitialize reader
*/
close() {
this.readerStatus_ = 0 /* NotInitialized */;
if (this.fd_ !== null) {
try {
import_node_fs.default.closeSync(this.fd_);
} catch {
}
}
if (this.fsWatcher_ !== null) {
this.fsWatcher_.close();
}
this.dbPath_ = null;
this.cacheInMemory_ = false;
this.fd_ = null;
this.dbCache_ = null;
this.fsWatcher_ = null;
this.indiciesIPv4_ = [];
this.indiciesIPv6_ = [];
this.offset_ = {};
this.enabled_ = {};
this.dbStats_ = {
DBType: 0,
DBColumn: 0,
DBYear: 0,
DBMonth: 0,
DBDay: 0,
DBCount: 0,
DBCountIPv6: 0,
BaseAddr: 0,
BaseAddrIPv6: 0,
IndexBaseAddr: 0,
IndexBaseAddrIPv6: 0,
ColumnSize: 0,
ColumnSizeIPv6: 0,
ProductCode: 0,
ProductType: 0,
FileSize: 0,
Indexed: false,
IndexedIPv6: false,
OldBIN: false
};
}
/**
* Initialize IP2Location database reader
* @param dbPath IP2Location BIN database
* @param options Options for database reader
*/
init(dbPath, options) {
if (!dbPath) {
throw new Error("Must specify path to database");
}
this.dbPath_ = dbPath;
this.cacheInMemory_ = options?.cacheDatabaseInMemory ?? false;
this.loadDatabase();
if (options?.reloadOnDbUpdate) {
this.watchDbFile();
}
}
/**
* Watch database file for changes and re-init if a change is detected
*/
watchDbFile() {
let timeout = null;
const originalState = this.readerStatus_;
const dbChangeHandler = (filename) => {
if (filename && this.dbPath_ && import_node_fs.default.existsSync(this.dbPath_)) {
this.fsWatcher_?.close();
this.loadDatabase();
this.fsWatcher_ = getFsWatch();
} else {
this.readerStatus_ = originalState;
}
};
const getFsWatch = () => {
if (!this.dbPath_) {
throw new Error("Path to database not available");
}
return import_node_fs.default.watch(this.dbPath_, (eventType, filename) => {
if (!filename) {
return;
}
if (this.fd_) {
this.readerStatus_ = 1 /* Initializing */;
}
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
timeout = null;
dbChangeHandler(filename);
}, 500);
});
};
this.fsWatcher_ = getFsWatch();
}
/**
* Populate data object with database query results for an IP address
* @param ipNum IP number
* @param ipVersion IP version
* @param data Output data object
*/
query(ipNum, ipVersion, data) {
let low = 0;
let high;
let maxIpRange;
let baseAddr;
let columnSize;
if (ipVersion === 6) {
maxIpRange = MAX_IPV6_RANGE;
high = this.dbStats_.DBCountIPv6;
baseAddr = this.dbStats_.BaseAddrIPv6;
columnSize = this.dbStats_.ColumnSizeIPv6;
if (this.dbStats_.IndexedIPv6) {
const indexaddr = Number(ipNum >> BigInt(112));
low = this.indiciesIPv6_[indexaddr][0];
high = this.indiciesIPv6_[indexaddr][1];
}
} else {
maxIpRange = MAX_IPV4_RANGE;
high = this.dbStats_.DBCount;
baseAddr = this.dbStats_.BaseAddr;
columnSize = this.dbStats_.ColumnSize;
if (this.dbStats_.Indexed) {
const indexaddr = Number(ipNum) >>> 16;
low = this.indiciesIPv4_[indexaddr][0];
high = this.indiciesIPv4_[indexaddr][1];
}
}
if (ipNum >= maxIpRange) {
ipNum = maxIpRange - BigInt(1);
}
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const rowoffset = baseAddr + mid * columnSize;
const rowoffset2 = rowoffset + columnSize;
const ipfrom = ipVersion === 6 ? this.readInt128Big(rowoffset) : this.readInt32Big(rowoffset);
const ipto = ipVersion === 6 ? this.readInt128Big(rowoffset2) : this.readInt32Big(rowoffset2);
if (ipfrom === void 0 || ipto === void 0) {
break;
}
if (ipfrom <= ipNum && ipto > ipNum) {
const firstcol = ipVersion === 6 ? 16 : 4;
const buff = this.readToBuffer(columnSize - firstcol, rowoffset + firstcol - 1);
if (!buff) {
break;
}
const enabledKeys = Object.keys(this.enabled_).filter(
(key) => this.enabled_[key]
);
for (const key of enabledKeys) {
if (key === "country") {
const countrypos = this.readBufferInt32(this.offset_[key], buff);
data.country_short = this.readString(countrypos) ?? "";
data.country_long = this.readString(countrypos + 3) ?? "";
} else if (key === "longitude" || key === "latitude") {
const num = this.readBufferFloat(this.offset_[key], buff);
data[key] = num !== 0 ? Math.round(num * 1e6) / 1e6 : null;
} else {
data[key] = this.readString(this.readBufferInt32(this.offset_[key], buff)) ?? "";
}
}
data.status = "OK";
return;
} else {
if (ipfrom > ipNum) {
high = mid - 1;
} else {
low = mid + 1;
}
}
}
data.status = "IP_ADDRESS_NOT_FOUND";
}
/**
* Query IP2Location database with an IP and get location information
* @param ip IP address
*/
get(ip) {
const data = {
ip,
ip_no: "",
status: ""
};
if (this.readerStatus_ === 0 /* NotInitialized */) {
data.status = "NOT_INITIALIZED";
} else if (this.readerStatus_ === 1 /* Initializing */) {
data.status = "INITIALIZING";
} else if (!this.dbPath_ || !this.dbCache_ && !import_node_fs.default.existsSync(this.dbPath_)) {
data.status = "DATABASE_NOT_FOUND";
} else if (!this.dbStats_.DBType) {
data.status = "NOT_INITIALIZED";
}
if (data.status) {
return data;
}
let ipVersion;
let ipNum;
({ ip, ipVersion, ipNum } = parseIp(ip));
if (!ipVersion) {
data.status = "INVALID_IP_ADDRESS";
} else if (ipVersion === 6 && this.dbStats_.OldBIN) {
data.status = "IPV6_NOT_SUPPORTED";
}
if (data.status) {
return data;
}
data.ip_no = ipNum.toString();
this.query(ipNum, ipVersion, data);
return data;
}
};
// src/csv-reader.ts
var import_node_fs2 = __toESM(require("fs"), 1);
var import_csv_parser = __toESM(require("csv-parser"), 1);
var CsvReader = class {
readerStatus_;
fsWatcher_;
reloadPromise_;
requiredCsvHeaders_;
constructor() {
this.readerStatus_ = 0 /* NotInitialized */;
this.fsWatcher_ = null;
this.reloadPromise_ = Promise.resolve();
this.requiredCsvHeaders_ = [];
}
/**
* Load IP2Location CSV databas
* @param csvPath Filesystem path to IP2Location CSV database
*/
async loadCsv(csvPath) {
const parser = import_node_fs2.default.createReadStream(csvPath).pipe((0, import_csv_parser.default)());
let firstRecord = true;
for await (const record of parser) {
const inputData = record;
if (firstRecord && Object.keys(inputData).filter((key) => this.requiredCsvHeaders_.includes(key)).length !== this.requiredCsvHeaders_.length) {
throw new Error("CSV database does not have expected headings");
}
firstRecord = false;
this.processRecord(inputData);
}
}
/**
* Get reader status
*/
get readerStatus() {
return this.readerStatus_;
}
/**
* Get DB reload promise
*/
get reloadPromise() {
return this.reloadPromise_;
}
/**
* Close database and uninitialize reader
*/
close() {
this.readerStatus_ = 0 /* NotInitialized */;
if (this.fsWatcher_ !== null) {
this.fsWatcher_.close();
}
this.fsWatcher_ = null;
}
/**
* Initialize reader
* @param dbPath IP2Location CSV database
* @param reloadOnDbUpdate Options for database reader
*/
async init(dbPath, reloadOnDbUpdate) {
if (!dbPath) {
throw new Error("Must specify path to CSV database");
}
this.readerStatus_ = 1 /* Initializing */;
await this.loadCsv(dbPath);
if (reloadOnDbUpdate) {
this.watchDbFile(dbPath);
}
this.readerStatus_ = 2 /* Ready */;
}
/**
* Watch database file for changes and re-init if a change is detected
* @param dbPath Path to watch
*/
watchDbFile(dbPath) {
let timeout = null;
const dbChangeHandler = (filename) => {
if (filename && import_node_fs2.default.existsSync(dbPath)) {
if (this.fsWatcher_ !== null) {
this.fsWatcher_.close();
this.fsWatcher_ = null;
}
this.reloadPromise_ = this.init(dbPath, true).catch(() => void 0);
}
};
this.fsWatcher_ = import_node_fs2.default.watch(dbPath, (eventType, filename) => {
if (!filename) {
return;
}
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
timeout = null;
dbChangeHandler(filename);
}, 500);
});
}
};
// src/subdiv-reader.ts
var SubdivReader = class extends CsvReader {
subdivisionMap_;
constructor() {
super();
this.subdivisionMap_ = {};
this.requiredCsvHeaders_ = ["country_code", "subdivision_name", "code"];
}
/**
* Process line from IP2Location subdivision database
* @param record Individual row from CSV database, broken into key/value pairs based on CSV headers
*/
processRecord(record) {
const { country_code, subdivision_name, code } = record;
const subdivisionCode = code?.length > 3 ? code.substring(3) : void 0;
if (!subdivisionCode) {
return;
}
const countryMap = this.subdivisionMap_[country_code] ?? {};
countryMap[subdivision_name] = subdivisionCode;
this.subdivisionMap_[country_code] = countryMap;
}
/**
* Close database and uninitialize reader
*/
close() {
super.close();
this.subdivisionMap_ = {};
}
/**
* Get the ISO 3166-2 subdivision part from a country code and region
* @param country ISO 3166-1 country code from IP2Location database
* @param region Region from from IP2Location database
*/
get(country, region) {
if (this.readerStatus !== 2 /* Ready */) {
return null;
}
if (!country || !region) {
return null;
}
return this.subdivisionMap_[country]?.[region] ?? null;
}
};
// src/geonameid-reader.ts
var GeoNameIdReader = class extends CsvReader {
geoNameIdMap_;
constructor() {
super();
this.geoNameIdMap_ = {};
this.requiredCsvHeaders_ = ["country_code", "region_name", "city_name", "geonameid"];
}
/**
* Process line from IP2Location GeoName ID database
* @param record Individual row from CSV database, broken into key/value pairs based on CSV headers
*/
processRecord(record) {
const { country_code, region_name, city_name, geonameid } = record;
if (!/^\d+$/.test(geonameid)) {
return;
}
const countryMap = this.geoNameIdMap_[country_code] ?? {};
const regionMap = countryMap[region_name] ?? {};
regionMap[city_name] = parseInt(geonameid);
countryMap[region_name] = regionMap;
this.geoNameIdMap_[country_code] = countryMap;
}
/**
* Close database and uninitialize reader
*/
close() {
super.close();
this.geoNameIdMap_ = {};
}
/**
* Get the GeoName ID from a country code, region, and city
* @param country ISO 3166-1 country code from IP2Location database
* @param region Region from from IP2Location database
* @param city City from IP2Location database
*/
get(country, region, city) {
if (this.readerStatus !== 2 /* Ready */) {
return null;
}
if (!country || !region || !city) {
return null;
}
return this.geoNameIdMap_[country]?.[region]?.[city] ?? null;
}
};
// src/country-info-reader.ts
var CountryInfoReader = class extends CsvReader {
countryInfoMap_;
constructor() {
super();
this.countryInfoMap_ = {};
this.requiredCsvHeaders_ = ["country_code", "capital", "total_area"];
}
/**
* Process line from IP2Location country info database
* @param record Individual row from CSV database, broken into key/value pairs based on CSV headers
*/
processRecord(record) {
const normalizeStr = (val) => {
return val.replace(/^-$/, "");
};
const normalizeNum = (val) => {
const numStr = normalizeStr(val).trim();
const num = numStr ? Number(numStr) : null;
return num != null && !Number.isNaN(num) ? num : null;
};
const countryInfoData = {
country_code: "",
capital: "",
total_area: null
};
for (const key in record) {
countryInfoData[key] = [
"country_numeric_code",
"total_area",
"population",
"idd_code"
].includes(key) ? normalizeNum(record[key]) : normalizeStr(record[key]);
}
if (countryInfoData.country_code) {
this.countryInfoMap_[countryInfoData.country_code] = countryInfoData;
}
}
/**
* Close database and uninitialize reader
*/
close() {
super.close();
this.countryInfoMap_ = {};
}
/**
* Get country info from a country code
* @param country ISO 3166-1 country code from IP2Location database
*/
get(country) {
if (this.readerStatus !== 2 /* Ready */) {
return null;
}
if (!country) {
return null;
}
return this.countryInfoMap_[country] ?? null;
}
};
// src/iata-icao-reader.ts
var IataIcaoReader = class extends CsvReader {
iataIcaoMap_;
constructor() {
super();
this.iataIcaoMap_ = {};
this.requiredCsvHeaders_ = [
"country_code",
"region_name",
"iata",
"icao",
"latitude",
"longitude"
];
}
/**
* Process line from IP2Location IATA/ICAO airport database
* @param record Individual row from CSV database, broken into key/value pairs based on CSV headers
*/
processRecord(record) {
const normalizeStr = (val) => {
return val.replace(/^-$/, "");
};
const normalizeNum = (val) => {
const numStr = normalizeStr(val).trim();
const num = numStr ? Number(numStr) : null;
return num != null && !Number.isNaN(num) ? num : null;
};
const airportOutputData = {
iata: "",
icao: "",
airport: "",
latitude: null,
longitude: null
};
const { country_code, region_name } = record;
for (const key in record) {
if (["country_code", "region_name"].includes(key)) {
continue;
}
airportOutputData[key] = ["latitude", "longitude"].includes(key) ? normalizeNum(record[key]) : normalizeStr(record[key]);
}
const countryMap = this.iataIcaoMap_[country_code] ?? {};
const regionArray = countryMap[region_name] ?? [];
regionArray.push(airportOutputData);
countryMap[region_name] = regionArray;
this.iataIcaoMap_[country_code] = countryMap;
}
/**
* Close database and uninitialize reader
*/
close() {
super.close();
this.iataIcaoMap_ = {};
}
/**
* Get IATA/ICAO airport info from country code and region name
* @param country ISO 3166-1 country code from IP2Location database
* @param region Region from from IP2Location database
*/
get(country, region) {
if (this.readerStatus !== 2 /* Ready */) {
return [];
}
if (!country || !region) {
return [];
}
return this.iataIcaoMap_[country]?.[region] ?? [];
}
};
// src/index.ts
var Ip2lReader = class {
dbReader_;
subdivReader_;
geoNameIdReader_;
countryInfoReader_;
iataIcaoReader_;
constructor() {
this.dbReader_ = new DbReader();
}
/**
* Initialize IP2Location database reader(s)
* @param dbPath IP2Location BIN database
* @param options Options for database reader
*/
async init(dbPath, options) {
this.dbReader_.init(dbPath, options);
if (!options) {
return;
}
if (options.subdivisionCsvPath) {
this.subdivReader_ = new SubdivReader();
await this.subdivReader_.init(options.subdivisionCsvPath, options.reloadOnDbUpdate);
}
if (options.geoNameIdCsvPath) {
this.geoNameIdReader_ = new GeoNameIdReader();
await this.geoNameIdReader_.init(options.geoNameIdCsvPath, options.reloadOnDbUpdate);
}
if (options.countryInfoCsvPath) {
this.countryInfoReader_ = new CountryInfoReader();
await this.countryInfoReader_.init(options.countryInfoCsvPath, options.reloadOnDbUpdate);
}
if (options.iataIcaoCsvPath) {
this.iataIcaoReader_ = new IataIcaoReader();
await this.iataIcaoReader_.init(options.iataIcaoCsvPath, options.reloadOnDbUpdate);
}
}
/**
* Query IP2Location database with an IP and get location information
* @param ip IP address
*/
get(ip) {
const ip2lData = this.dbReader_.get(ip);
if (this.subdivReader_) {
if (typeof ip2lData.country_short === "string" && typeof ip2lData.region === "string") {
const subdivision = this.subdivReader_.get(ip2lData.country_short, ip2lData.region);
if (subdivision !== null) {
ip2lData.subdivision = subdivision;
}
}
}
if (this.geoNameIdReader_) {
if (typeof ip2lData.country_short === "string" && typeof ip2lData.region === "string" && typeof ip2lData.city === "string") {
const geoNameId = this.geoNameIdReader_.get(
ip2lData.country_short,
ip2lData.region,
ip2lData.city
);
if (geoNameId !== null) {
ip2lData.geoname_id = geoNameId;
}
}
}
if (this.countryInfoReader_) {
if (typeof ip2lData.country_short === "string") {
const countryInfo = this.countryInfoReader_.get(ip2lData.country_short);
ip2lData.country_info = countryInfo;
}
}
if (this.iataIcaoReader_) {
if (typeof ip2lData.country_short === "string" && typeof ip2lData.region === "string") {
const airports = this.iataIcaoReader_.get(ip2lData.country_short, ip2lData.region);
ip2lData.airports = airports;
}
}
return ip2lData;
}
/**
* Close IP2Location database(s) and uninitialize reader(s)
*/
close() {
this.dbReader_.close();
this.subdivReader_?.close();
this.geoNameIdReader_?.close();
this.countryInfoReader_?.close();
this.iataIcaoReader_?.close();
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
Ip2lReader
});