diff --git a/binding/javascript/ReadMe.md b/binding/javascript/ReadMe.md new file mode 100644 index 0000000..e69de29 diff --git a/binding/javascript/ip2region.js b/binding/javascript/ip2region.js new file mode 100644 index 0000000..6d9d115 --- /dev/null +++ b/binding/javascript/ip2region.js @@ -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 + diff --git a/binding/javascript/package.json b/binding/javascript/package.json index e665ea8..e1335f7 100644 --- a/binding/javascript/package.json +++ b/binding/javascript/package.json @@ -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", diff --git a/binding/javascript/searcher.js b/binding/javascript/searcher.js index f5435da..601ec1a 100644 --- a/binding/javascript/searcher.js +++ b/binding/javascript/searcher.js @@ -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'); diff --git a/binding/javascript/tests/buffer.test.js b/binding/javascript/tests/buffer.test.js index f63a1ac..ac8aab0 100644 --- a/binding/javascript/tests/buffer.test.js +++ b/binding/javascript/tests/buffer.test.js @@ -5,7 +5,7 @@ // util test script // @Author Lion -const util = require('../util'); +const util = require('../util.js'); const path = require('path'); const dbPath = path.join(__dirname, '..', '..', '..', 'data', 'ip2region_v4.xdb') diff --git a/binding/javascript/tests/ip.test.js b/binding/javascript/tests/ip.test.js index 2479e7f..2598ae8 100644 --- a/binding/javascript/tests/ip.test.js +++ b/binding/javascript/tests/ip.test.js @@ -5,7 +5,7 @@ // util test script // @Author Lion -const util = require('../util'); +const util = require('../util.js'); test('parse ip address', () => { let ip_list = [ diff --git a/binding/javascript/tests/search.test.js b/binding/javascript/tests/search.test.js new file mode 100644 index 0000000..4b0c84b --- /dev/null +++ b/binding/javascript/tests/search.test.js @@ -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 + +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}`); + } +}); \ No newline at end of file diff --git a/binding/javascript/tests/searcher.test.js b/binding/javascript/tests/searcher.test.js index 869dba6..b762190 100644 --- a/binding/javascript/tests/searcher.test.js +++ b/binding/javascript/tests/searcher.test.js @@ -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 -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}`); } }); \ No newline at end of file diff --git a/binding/javascript/util.js b/binding/javascript/util.js index 4633ac0..d4a4abb 100644 --- a/binding/javascript/util.js +++ b/binding/javascript/util.js @@ -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);