diff --git a/binding/javascript/searcher.js b/binding/javascript/searcher.js new file mode 100644 index 0000000..f5435da --- /dev/null +++ b/binding/javascript/searcher.js @@ -0,0 +1,142 @@ +// 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 implementation +// @Author Lion + +const fs = require('fs'); +const { + parseIP, + HeaderInfoLength, VectorIndexCols, VectorIndexSize +} = require('./util'); + +class Searcher { + constructor(version, dbPath, vectorIndex, cBuffer) { + this.ioCount = 0; + this.version = version; + if (cBuffer != null) { + this.handle = null; + this.vectorIndex = null; + this.cBuffer = cBuffer; + } else { + this.handle = fs.openSync(dbPath, 'r'); + this.vectorIndex = vectorIndex; + this.cBuffer = null; + } + } + + getIOCount() { + return this.ioCount; + } + + async search(ip) { + let ipBytes; + if (Buffer.isBuffer(ip)) { + ipBytes = ip; + } else { + ipBytes = parseIP(ip); + } + + // ip version check + if (ipBytes.length != this.version.bytes) { + throw new Error(`invalid ip address (${this.version.name} expected)`); + } + + // reset the global counter + this.ioCount = 0; + + // 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 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); + } else { + const buff = Buffer.alloc(VectorIndexSize); + this.read(HeaderInfoLength + idx, buff); + sPtr = buff.readUInt32LE(0); + ePtr = buff.readUInt32LE(4); + } + + // console.log(`sPtr: ${sPtr}, ePtr: ${ePtr}`); + // 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); + let dLen = -1, dPtr = -1, l = 0, h = (ePtr - sPtr) / indexSize; + while (l <= h) { + const m = (l + h) >> 1; + const p = sPtr + m * indexSize; + + // read the segment index block + this.read(p, buff); + if (this.version.ipSubCompare(ipBytes, buff, 0) < 0) { + h = m - 1; + } else if (this.version.ipSubCompare(ip, buff, bytes) > 0) { + l = m + 1; + } else { + dLen = buff.readUint16LE(dBytes); + dPtr = buff.readUint32LE(dBytes + 2); + break; + } + } + + // 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'); + } + + read(offset, buff, stats) { + // check the in-memory buffer first + if (this.cBuffer != null) { + this.cBuffer.copy(buff, 0, offset, offset + buff.length); + return; + } + + // increase the io counts + this.ioCount++; + + // read the data + let rBytes = fs.readSync(this.handle, buff, 0, buff.length, offset); + if (rBytes != buff.length) { + throw new Error(`incomplete read: read bytes should be ${buff.length}`); + } + } + + // close the searcher + close() { + if (this.handle != null) { + fs.close(this.handle); + } + } +} + +function newWithFileOnly(version, dbPath) { + return new Searcher(version, dbPath, null, null); +} + +function newWithVectorIndex(version, dbPath, vectorIndex) { + return new Searcher(version, dbPath, vectorIndex, null); +} + +function newWithBuffer(version, cBuffer) { + return new Searcher(version, null, null, cBuffer); +} + +module.exports = { + newWithFileOnly, + newWithVectorIndex, + newWithBuffer +} \ No newline at end of file diff --git a/binding/javascript/tests/searcher.test.js b/binding/javascript/tests/searcher.test.js new file mode 100644 index 0000000..869dba6 --- /dev/null +++ b/binding/javascript/tests/searcher.test.js @@ -0,0 +1,35 @@ +// 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 tester +// @Author Lion + +const {IPv4, 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 searcher test', async () => { + 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 = await 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/version.test.js b/binding/javascript/tests/version.test.js index 1ff37e7..9934581 100644 --- a/binding/javascript/tests/version.test.js +++ b/binding/javascript/tests/version.test.js @@ -16,7 +16,13 @@ test("test version from name", () => { let vs = ["v4", "ipv4", "v4x", "v6", "ipv6", "v6x"]; vs.forEach(ele => { let v = util.versionFromName(ele); - console.log(`versionFrom(${ele})`, v == null ? null : v.toString()); + if (v == null) { + console.log(`invalid version name ${ele}`); + return; + } + + console.log(`versionFrom(${ele})`, v.toString()); + console.log(`version.id=${v.id}, version.name=${v.name}`); }); }); diff --git a/binding/javascript/util.js b/binding/javascript/util.js index e15750a..4633ac0 100644 --- a/binding/javascript/util.js +++ b/binding/javascript/util.js @@ -33,38 +33,6 @@ class Header { this.buff = buff; } - version() { - return this.version; - } - - indexPolicy() { - return this.indexPolicy; - } - - createdAt() { - return this.createdAt; - } - - startIndexPtr() { - return this.startIndexPtr; - } - - endIndexPtr() { - return this.endIndexPtr; - } - - ipVersion() { - return this.ipVersion; - } - - runtimePtrBytes() { - return this.runtimePtrBytes; - } - - buff() { - return this.buff; - } - toString() { return `{ "version":${this.version}, @@ -88,26 +56,6 @@ class Version { this.ipCompareFunc = ipCompareFunc; } - id() { - return this.id; - } - - name() { - return this.name; - } - - bytes() { - return this.bytes; - } - - indexSize() { - return this.indexSize; - } - - compareFunc() { - return this.ipCompareFunc; - } - ipCompare(ip1, ip2) { return this.ipCompareFunc(ip1, ip2, 0); }