javascript is Ready for IPv6

This commit is contained in:
lion 2025-10-13 12:05:20 +08:00
parent ad78dbe547
commit 4507c2f706
9 changed files with 224 additions and 125 deletions

View File

View File

@ -0,0 +1,7 @@
// Copyright 2022 The Ip2Region Authors. All rights reserved.
// Use of this source code is governed by a Apache2.0-style
// license that can be found in the LICENSE file.
// ip2region.js
// @Author Lion <chenxin619315@gmail.com>

View File

@ -1,8 +1,9 @@
{
"name": "ip2region",
"version": "3.0.0",
"description": "javascript (ES6) binding for ip2region with both IPv4 and IPv6 supported ",
"main": "index.js",
"description": "javascript binding for ip2region with both IPv4 and IPv6 supported ",
"type": "module",
"main": "ip2region.js",
"scripts": {
"test": "jest"
},
@ -13,10 +14,20 @@
"keywords": [
"ip2region",
"ip-address",
"ip-lookup",
"ip-region",
"ip-location",
"ip-lookup",
"ip-search",
"ipv4-address",
"ipv4-region",
"ipv4-location",
"ipv4-lookup",
"ipv6-lookup"
"ipv4-search",
"ipv6-address",
"ipv6-region",
"ipv6-location",
"ipv6-lookup",
"ipv6-search"
],
"author": "lionsoul2014",
"license": "ISC",

View File

@ -8,7 +8,8 @@
const fs = require('fs');
const {
parseIP,
HeaderInfoLength, VectorIndexCols, VectorIndexSize
HeaderInfoLength, VectorIndexCols, VectorIndexSize,
ipToString
} = require('./util');
class Searcher {
@ -30,17 +31,13 @@ class Searcher {
return this.ioCount;
}
async search(ip) {
let ipBytes;
if (Buffer.isBuffer(ip)) {
ipBytes = ip;
} else {
ipBytes = parseIP(ip);
}
search(ip) {
// check and parse the string ip
const ipBytes = Buffer.isBuffer(ip) ? ip : parseIP(ip);
// ip version check
if (ipBytes.length != this.version.bytes) {
throw new Error(`invalid ip address (${this.version.name} expected)`);
throw new Error(`invalid ip address '${ipToString(ipBytes)}' (${this.version.name} expected)`);
}
// reset the global counter
@ -48,14 +45,14 @@ class Searcher {
// located the segment index block based on the vector index
let sPtr = 0, ePtr = 0;
let il0 = ipBytes[0] & 0xFF, il1 = ipBytes[1] & 0xFF;
let il0 = ipBytes[0], il1 = ipBytes[1];
let idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize;
if (this.vectorIndex != null) {
sPtr = this.vectorIndex.readUint32LE(idx);
ePtr = this.vectorIndex.readUint32LE(idx + 4);
} else if (this.cBuffer != null) {
sPtr = this.vectorIndex.readUint32LE(HeaderInfoLength + idx);
ePtr = this.vectorIndex.readUint32LE(HeaderInfoLength + idx + 4);
sPtr = this.cBuffer.readUint32LE(HeaderInfoLength + idx);
ePtr = this.cBuffer.readUint32LE(HeaderInfoLength + idx + 4);
} else {
const buff = Buffer.alloc(VectorIndexSize);
this.read(HeaderInfoLength + idx, buff);
@ -67,7 +64,7 @@ class Searcher {
// binary search the segment index block to get the region info
const bytes = ipBytes.length, dBytes = ipBytes.length << 1;
const indexSize = this.version.indexSize;
let buff = Buffer.alloc(indexSize);
const buff = Buffer.alloc(indexSize);
let dLen = -1, dPtr = -1, l = 0, h = (ePtr - sPtr) / indexSize;
while (l <= h) {
const m = (l + h) >> 1;
@ -77,7 +74,7 @@ class Searcher {
this.read(p, buff);
if (this.version.ipSubCompare(ipBytes, buff, 0) < 0) {
h = m - 1;
} else if (this.version.ipSubCompare(ip, buff, bytes) > 0) {
} else if (this.version.ipSubCompare(ipBytes, buff, bytes) > 0) {
l = m + 1;
} else {
dLen = buff.readUint16LE(dBytes);
@ -87,12 +84,6 @@ class Searcher {
}
// console.log(`dLen: ${dLen}, dPtr: ${dPtr}`);
// empty match interception
// @Note: could this even be a case ?
if (dPtr < 0) {
return null;
}
const region = Buffer.alloc(dLen);
this.read(dPtr, region);
return region.toString('utf-8');

View File

@ -5,7 +5,7 @@
// util test script
// @Author Lion <chenxin619315@gmail.com>
const util = require('../util');
const util = require('../util.js');
const path = require('path');
const dbPath = path.join(__dirname, '..', '..', '..', 'data', 'ip2region_v4.xdb')

View File

@ -5,7 +5,7 @@
// util test script
// @Author Lion <chenxin619315@gmail.com>
const util = require('../util');
const util = require('../util.js');
test('parse ip address', () => {
let ip_list = [

View File

@ -0,0 +1,56 @@
// Copyright 2022 The Ip2Region Authors. All rights reserved.
// Use of this source code is governed by a Apache2.0-style
// license that can be found in the LICENSE file.
// searcher search tester
// @Author Lion <chenxin619315@gmail.com>
const {IPv4, IPv6, parseIP, ipToString} = require('../util');
const {newWithFileOnly} = require('../searcher');
const path = require('path');
const dbPath = {
v4: path.join(__dirname, '..', '..', '..', 'data', 'ip2region_v4.xdb'),
v6: path.join(__dirname, '..', '..', '..', 'data', 'ip2region_v6.xdb')
}
test('ipv4 search test', () => {
try {
let searcher = newWithFileOnly(IPv4, dbPath.v4);
let ip_list = [
'1.0.0.0',
parseIP('113.118.112.93'),
'240e:3b7::'
];
for (var i = 0; i < ip_list.length; i++) {
let ip = ip_list[i];
let region = searcher.search(ip);
let ipStr = Buffer.isBuffer(ip) ? ipToString(ip) : ip;
console.log(`search(${ipStr}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);
}
} catch (e) {
console.log(`${e.message}`);
}
});
test('ipv6 search test', async () => {
try {
let searcher = newWithFileOnly(IPv6, dbPath.v6);
let ip_list = [
'2a02:26f7:c409:4001::',
parseIP('2a11:8080:200::a:a05c'),
'240e:3b7::',
'120.229.45.92'
];
for (var i = 0; i < ip_list.length; i++) {
let ip = ip_list[i];
let region = searcher.search(ip);
let ipStr = Buffer.isBuffer(ip) ? ipToString(ip) : ip;
console.log(`search(${ipStr}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);
}
} catch (e) {
console.log(`${e.message}`);
}
});

View File

@ -2,11 +2,11 @@
// Use of this source code is governed by a Apache2.0-style
// license that can be found in the LICENSE file.
// searcher tester
// searcher new tester
// @Author Lion <chenxin619315@gmail.com>
const {IPv4, parseIP, ipToString} = require('../util');
const {newWithFileOnly} = require('../searcher');
const {IPv4, IPv6, loadVectorIndexFromFile, XdbIPv4Id, loadContentFromFile} = require('../util');
const {newWithFileOnly, newWithVectorIndex, newWithBuffer} = require('../searcher');
const path = require('path');
const dbPath = {
@ -14,22 +14,53 @@ const dbPath = {
v6: path.join(__dirname, '..', '..', '..', 'data', 'ip2region_v6.xdb')
}
test('ipv4 searcher test', async () => {
try {
let searcher = newWithFileOnly(IPv4, dbPath.v4);
let ip_list = [
'1.0.0.0',
parseIP('113.118.112.93'),
'240e:3b7::'
];
function _get_creater_list(version) {
let dbFile = version.id == XdbIPv4Id ? dbPath.v4 : dbPath.v6;
return [function(){
return ["newWithFileOnly", newWithFileOnly(version, dbFile)];
}, function(){
const vIndex = loadVectorIndexFromFile(dbFile);
return ["newWithVectorIndex", newWithVectorIndex(version, dbFile, vIndex)];
}, function(){
const cBuffer = loadContentFromFile(dbFile);
return ["newWithBuffer", newWithBuffer(version, cBuffer)];
}];
}
for (var i = 0; i < ip_list.length; i++) {
let ip = ip_list[i];
let region = await searcher.search(ip);
let ipStr = Buffer.isBuffer(ip) ? ipToString(ip) : ip;
console.log(`search(${ipStr}): {region: ${region}, ioCount: ${searcher.getIOCount()}}`);
test('ipv4 searcher test', () => {
const ip_Str = '120.229.45.92';
const c_list = _get_creater_list(IPv4);
try {
let bRegion = null;
for (var i = 0; i < c_list.length; i++) {
const meta = c_list[i]();
const region = meta[1].search(ip_Str);
if (bRegion != null) {
expect(region).toBe(region);
}
bRegion = region;
console.log(`${meta[0]}.search(${ip_Str}): ${region}`);
}
} catch (e) {
console.log(`${e.message}`);
console.error(`${e.message}`);
}
});
test('ipv6 searcher test', async () => {
const ip_Str = '240e:57f:32ff:ffff:ffff:ffff:ffff:ffff';
const c_list = _get_creater_list(IPv6);
try {
let bRegion = null;
for (var i = 0; i < c_list.length; i++) {
const meta = c_list[i]();
const region = meta[1].search(ip_Str);
if (bRegion != null) {
expect(region).toBe(region);
}
bRegion = region;
console.log(`${meta[0]}.search(${ip_Str}): ${region}`);
}
} catch (e) {
console.error(`${e.message}`);
}
});

View File

@ -47,87 +47,6 @@ class Header {
// ---
class Version {
constructor(id, name, bytes, indexSize, ipCompareFunc) {
this.id = id;
this.name = name;
this.bytes = bytes;
this.indexSize = indexSize;
this.ipCompareFunc = ipCompareFunc;
}
ipCompare(ip1, ip2) {
return this.ipCompareFunc(ip1, ip2, 0);
}
ipSubCompare(ip1, ip2, offset) {
return this.ipCompareFunc(ip1, ip2, offset);
}
toString() {
return `{"id": ${this.id}, "name": "${this.name}", "bytes":${this.bytes}, "index_size": ${this.indexSize}}`;
}
}
// 14 = 4 + 4 + 2 + 4
const IPv4 = new Version(XdbIPv4Id, "IPv4", 4, 14, function(ip1, buff, offset){
// ip1: Big endian byte order parsed from input
// ip2: Little endian byte order read from xdb index.
// @Note: to compatible with the old Litten endian index encode implementation.
let i, j = offset + ip1.length - 1;
for (i = 0; i < ip1.length; i++, j--) {
const i1 = ip1[i] & 0xFF;
const i2 = buff[j] & 0xFF;
if (i1 < i2) {
return -1;
}
if (i1 > i2) {
return 1;
}
}
return 0;
});
// 38 = 16 + 16 + 2 + 4
const IPv6 = new Version(XdbIPv6Id, "IPv6", 6, 38, ipSubCompare);
function versionFromName(name) {
let n = name.toUpperCase();
if (n == "V4" || n == "IPV4") {
return IPv4;
} else if (n == "V6" || n == "IPV6") {
return IPv6;
} else {
return null;
}
}
function versionFromHeader(h) {
let v = h.version();
// old structure with ONLY IPv4 supporting
if (v == XdbStructure20) {
return IPv4;
}
// structure 3.0 with IPv6 supporting
if (v != XdbStructure30) {
return null;
}
let ipVer = h.ipVersion();
if (ipVer == XdbIPv4Id) {
return IPv4;
} else if (ipVer == XdbIPv6Id) {
return IPv6;
} else {
return null;
}
}
// ---
// parse ipv4 address
function _parse_ipv4_addr(v4String) {
let ps = v4String.split('.', 4);
@ -223,7 +142,7 @@ function parseIP(ipString) {
} else if (cDot > -1) {
return _parse_ipv6_addr(ipString);
} else {
return null;
throw new Error(`invalid ip address '${ipString}'`);
}
}
@ -324,6 +243,90 @@ function ipCompare(ip1, ip2) {
return ipSubCompare(ip1, ip2, 0);
}
// ---
class Version {
constructor(id, name, bytes, indexSize, ipCompareFunc) {
this.id = id;
this.name = name;
this.bytes = bytes;
this.indexSize = indexSize;
this.ipCompareFunc = ipCompareFunc;
}
ipCompare(ip1, ip2) {
return this.ipCompareFunc(ip1, ip2, 0);
}
ipSubCompare(ip1, ip2, offset) {
return this.ipCompareFunc(ip1, ip2, offset);
}
toString() {
return `{"id": ${this.id}, "name": "${this.name}", "bytes":${this.bytes}, "index_size": ${this.indexSize}}`;
}
}
// 14 = 4 + 4 + 2 + 4
const IPv4 = new Version(XdbIPv4Id, "IPv4", 4, 14, function(ip1, buff, offset){
// ip1: Big endian byte order parsed from input
// ip2: Little endian byte order read from xdb index.
// @Note: to compatible with the old Litten endian index encode implementation.
let i, j = offset + ip1.length - 1;
for (i = 0; i < ip1.length; i++, j--) {
const i1 = ip1[i] & 0xFF;
const i2 = buff[j] & 0xFF;
if (i1 < i2) {
return -1;
}
if (i1 > i2) {
return 1;
}
}
return 0;
});
// 38 = 16 + 16 + 2 + 4
const IPv6 = new Version(XdbIPv6Id, "IPv6", 16, 38, ipSubCompare);
function versionFromName(name) {
let n = name.toUpperCase();
if (n == "V4" || n == "IPV4") {
return IPv4;
} else if (n == "V6" || n == "IPV6") {
return IPv6;
} else {
return null;
}
}
function versionFromHeader(h) {
let v = h.version();
// old structure with ONLY IPv4 supporting
if (v == XdbStructure20) {
return IPv4;
}
// structure 3.0 with IPv6 supporting
if (v != XdbStructure30) {
return null;
}
let ipVer = h.ipVersion();
if (ipVer == XdbIPv4Id) {
return IPv4;
} else if (ipVer == XdbIPv6Id) {
return IPv6;
} else {
return null;
}
}
// ---
function loadHeader(fd) {
const buffer = Buffer.alloc(HeaderInfoLength);
const rBytes = fs.readSync(fd, buffer, 0, HeaderInfoLength, 0);