IPv6 supporting for Java searcher

This commit is contained in:
lion 2025-09-12 12:41:00 +08:00
parent c6c0ee954f
commit 6db9654c7d
18 changed files with 686 additions and 217 deletions

View File

@ -4,7 +4,7 @@
<groupId>org.lionsoul</groupId> <groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId> <artifactId>ip2region</artifactId>
<version>2.8.1</version> <version>3.1.0</version>
<packaging>jar</packaging> <packaging>jar</packaging>
<name>ip2region</name> <name>ip2region</name>
@ -41,8 +41,8 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.6</maven.compiler.source> <maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target> <maven.compiler.target>1.8</maven.compiler.target>
</properties> </properties>
<dependencies> <dependencies>
@ -105,7 +105,7 @@
<configuration> <configuration>
<transformers> <transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.lionsoul.ip2region.SearchTest</mainClass> <mainClass>org.lionsoul.ip2region.SearchApp</mainClass>
</transformer> </transformer>
</transformers> </transformers>
</configuration> </configuration>

View File

@ -6,15 +6,18 @@
package org.lionsoul.ip2region; package org.lionsoul.ip2region;
import org.lionsoul.ip2region.xdb.InetAddressException;
import org.lionsoul.ip2region.xdb.XdbException;
import org.lionsoul.ip2region.xdb.LongByteArray; import org.lionsoul.ip2region.xdb.LongByteArray;
import org.lionsoul.ip2region.xdb.Searcher; import org.lionsoul.ip2region.xdb.Searcher;
import org.lionsoul.ip2region.xdb.Util;
import org.lionsoul.ip2region.xdb.Version;
import java.io.*; import java.io.*;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class SearchTest { public class SearchApp {
public static void printHelp(String[] args) { public static void printHelp(String[] args) {
System.out.print("ip2region xdb searcher\n"); System.out.print("ip2region xdb searcher\n");
@ -24,21 +27,33 @@ public class SearchTest {
System.out.print(" bench search bench test\n"); System.out.print(" bench search bench test\n");
} }
public static Searcher createSearcher(String dbPath, String cachePolicy) throws IOException { public static Searcher createSearcher(String dbPath, String cachePolicy) throws IOException, XdbException {
final RandomAccessFile handle = new RandomAccessFile(dbPath, "r");
// verify the xdb file
// @Note: do NOT call it every time you create a searcher since this will slow
// down the search response.
// @see the util.Verify function for details.
Searcher.verify(handle);
// get the ip version from header
final Version version = Version.fromHeader(Searcher.loadHeader(handle));
// create the final searcher
if ("file".equals(cachePolicy)) { if ("file".equals(cachePolicy)) {
return Searcher.newWithFileOnly(dbPath); return Searcher.newWithFileOnly(version, dbPath);
} else if ("vectorIndex".equals(cachePolicy)) { } else if ("vectorIndex".equals(cachePolicy)) {
byte[] vIndex = Searcher.loadVectorIndexFromFile(dbPath); byte[] vIndex = Searcher.loadVectorIndexFromFile(dbPath);
return Searcher.newWithVectorIndex(dbPath, vIndex); return Searcher.newWithVectorIndex(version, dbPath, vIndex);
} else if ("content".equals(cachePolicy)) { } else if ("content".equals(cachePolicy)) {
LongByteArray cBuff = Searcher.loadContentFromFile(dbPath); LongByteArray cBuff = Searcher.loadContentFromFile(dbPath);
return Searcher.newWithBuffer(cBuff); return Searcher.newWithBuffer(version, cBuff);
} else { } else {
throw new IOException("invalid cache policy `" + cachePolicy + "`, options: file/vectorIndex/content"); throw new IOException("invalid cache policy `" + cachePolicy + "`, options: file/vectorIndex/content");
} }
} }
public static void searchTest(String[] args) throws IOException { public static void searchTest(String[] args) throws IOException, XdbException {
String dbPath = "", cachePolicy = "vectorIndex"; String dbPath = "", cachePolicy = "vectorIndex";
for (final String r : args) { for (final String r : args) {
if (r.length() < 5) { if (r.length() < 5) {
@ -105,7 +120,7 @@ public class SearchTest {
System.out.println("searcher test program exited, thanks for trying"); System.out.println("searcher test program exited, thanks for trying");
} }
public static void benchTest(String[] args) throws IOException { public static void benchTest(String[] args) throws IOException, XdbException, InetAddressException {
String dbPath = "", srcPath = "", cachePolicy = "vectorIndex"; String dbPath = "", srcPath = "", cachePolicy = "vectorIndex";
for (final String r : args) { for (final String r : args) {
if (r.length() < 5) { if (r.length() < 5) {
@ -155,40 +170,44 @@ public class SearchTest {
String l = line.trim(); String l = line.trim();
String[] ps = l.split("\\|", 3); String[] ps = l.split("\\|", 3);
if (ps.length != 3) { if (ps.length != 3) {
reader.close();
System.out.printf("invalid ip segment `%s`\n", l); System.out.printf("invalid ip segment `%s`\n", l);
return; return;
} }
long sip; byte[] sip;
try { try {
sip = Searcher.checkIP(ps[0]); sip = Util.parseIP(ps[0]);
} catch (Exception e) { } catch (Exception e) {
reader.close();
System.out.printf("check start ip `%s`: %s\n", ps[0], e); System.out.printf("check start ip `%s`: %s\n", ps[0], e);
return; return;
} }
long eip; byte[] eip;
try { try {
eip = Searcher.checkIP(ps[1]); eip = Util.parseIP(ps[1]);
} catch (Exception e) { } catch (Exception e) {
reader.close();
System.out.printf("check end ip `%s`: %s\n", ps[1], e); System.out.printf("check end ip `%s`: %s\n", ps[1], e);
return; return;
} }
if (sip > eip) { if (Util.ipCompare(sip, eip) > 0) {
reader.close();
System.out.printf("start ip(%s) should not be greater than end ip(%s)\n", ps[0], ps[1]); System.out.printf("start ip(%s) should not be greater than end ip(%s)\n", ps[0], ps[1]);
return; return;
} }
long mip = (sip + eip) >> 1; for (final byte[] ip : new byte[][]{sip, eip}) {
for (final long ip : new long[]{sip, (sip + mip) >> 1, mip, (mip + eip) >> 1, eip}) {
long sTime = System.nanoTime(); long sTime = System.nanoTime();
String region = searcher.search(ip); String region = searcher.search(ip);
costs += System.nanoTime() - sTime; costs += System.nanoTime() - sTime;
// check the region info // check the region info
if (!ps[2].equals(region)) { if (!ps[2].equals(region)) {
System.out.printf("failed search(%s) with (%s != %s)\n", Searcher.long2ip(ip), region, ps[2]); System.out.printf("failed search(%s) with (%s != %s)\n", Util.ipToString(ip), region, ps[2]);
reader.close();
return; return;
} }
@ -213,14 +232,14 @@ public class SearchTest {
if ("search".equals(args[0])) { if ("search".equals(args[0])) {
try { try {
searchTest(args); searchTest(args);
} catch (IOException e) { } catch (Exception e) {
System.out.printf("failed running search test: %s\n", e); System.out.printf("failed running search test: %s\n", e);
} }
} else if ("bench".equals(args[0])) { } else if ("bench".equals(args[0])) {
try { try {
benchTest(args); benchTest(args);
} catch (IOException e) { } catch (Exception e) {
System.out.printf("failed running bench test: %s\n", e); System.out.printf("fwailed running bench test: %s\n", e);
} }
} else { } else {
printHelp(args); printHelp(args);

View File

@ -1,85 +0,0 @@
// 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.
// @Author Lion <chenxin619315@gmail.com>
// @Date 2022/06/23
package org.lionsoul.ip2region;
import org.lionsoul.ip2region.xdb.LongByteArray;
import org.lionsoul.ip2region.xdb.Searcher;
import java.util.Arrays;
public class UtilTest {
public static void testIP2Long() {
String ip = "1.2.3.4";
long ipAddr = 0;
try {
ipAddr = Searcher.checkIP(ip);
} catch (Exception e) {
System.out.printf("failed to check ip: %s\n", e);
return;
}
if (ipAddr != 16909060) {
System.out.print("failed ip2long\n");
return;
}
String ip2 = Searcher.long2ip(ipAddr);
if (!ip.equals(ip2)) {
System.out.print("failed long2ip\n");
return;
}
System.out.printf("passed: ip=%s, ipAddr=%d, ip2=%s\n", ip, ipAddr, ip2);
}
public static void testLongByteArray() {
final LongByteArray byteArray = new LongByteArray();
byteArray.append(new byte[]{0,0,0,0,0});
byteArray.append(new byte[]{1,1,1,1,1});
int counter = 2;
for (int i = 0; i < 100; i++) {
final byte[] buff = new byte[10];
Arrays.fill(buff, (byte) counter);
byteArray.append(buff);
counter++;
}
System.out.printf("1, byteArray.length: %d\n", byteArray.length());
System.out.println("2, length copy test...");
int[] length = new int[]{5, 10, 15, 20, 21, 22, 23, 25, 28, 29, 30, 40, 42, 44, 50, 60};
for (int j : length) {
final byte[] destBuff = new byte[j];
byteArray.copy(0, destBuff, 0, destBuff.length);
System.out.printf("copy(0,%d): \n", destBuff.length);
for (byte b : destBuff) {
System.out.print(b + " ");
}
System.out.println();
}
System.out.println("3, offset copy test...");
int[] offset = new int[]{0, 5, 10, 15, 20, 21, 22, 23, 25, 28, 29, 30, 40, 42, 44, 50, 60};
for (int j : offset) {
final byte[] destBuff = new byte[11];
byteArray.copy(j, destBuff, 0, destBuff.length);
System.out.printf("copy(%d,%d): \n", j, destBuff.length);
for (byte b : destBuff) {
System.out.print(b + " ");
}
System.out.println();
}
}
public static void main(String[] args) {
System.out.print("testing IP2Long ... \n");
testIP2Long();
System.out.print("testing LongByteArray ... \n");
testLongByteArray();
}
}

View File

@ -9,18 +9,25 @@ package org.lionsoul.ip2region.xdb;
public class Header { public class Header {
public final int version; public final int version;
public final int indexPolicy; public final int indexPolicy;
public final int createdAt; public final long createdAt;
public final int startIndexPtr; public final long startIndexPtr;
public final int endIndexPtr; public final long endIndexPtr;
// since xdb 3.0 with IPv6 supporting
public final int ipVersion;
public final int runtimePtrBytes;
public final byte[] buffer; public final byte[] buffer;
public Header(byte[] buff) { public Header(byte[] buff) {
assert buff.length >= 16; assert buff.length >= 16;
version = Searcher.getInt2(buff, 0); version = LittleEndian.getInt2(buff, 0);
indexPolicy = Searcher.getInt2(buff, 2); indexPolicy = LittleEndian.getInt2(buff, 2);
createdAt = Searcher.getInt(buff, 4); createdAt = LittleEndian.getUint32(buff, 4);
startIndexPtr = Searcher.getInt(buff, 8); startIndexPtr = LittleEndian.getUint32(buff, 8);
endIndexPtr = Searcher.getInt(buff, 12); endIndexPtr = LittleEndian.getUint32(buff, 12);
ipVersion = LittleEndian.getInt2(buff, 16);
runtimePtrBytes = LittleEndian.getInt2(buff, 18);
buffer = buff; buffer = buff;
} }
@ -30,7 +37,9 @@ public class Header {
"IndexPolicy: " + indexPolicy + ',' + "IndexPolicy: " + indexPolicy + ',' +
"CreatedAt: " + createdAt + ',' + "CreatedAt: " + createdAt + ',' +
"StartIndexPtr: " + startIndexPtr + ',' + "StartIndexPtr: " + startIndexPtr + ',' +
"EndIndexPtr: " + endIndexPtr + "EndIndexPtr: " + endIndexPtr + ',' +
"IPVersion: " + ipVersion + ',' +
"RuntimePtrBytes: " + runtimePtrBytes +
'}'; '}';
} }
} }

View File

@ -0,0 +1,47 @@
// 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.
package org.lionsoul.ip2region.xdb;
// IPv4 version implementation
// @Author Lion <chenxin619315@gmail.com>
// @Date 2025/09/10
public class IPv4 extends Version {
public IPv4() {
// segmentIndex: 4 + 4 + 2 + 4
super(4, "IPv4", 4, 14);
}
@Override
public int putBytes(byte[] buff, int offset, byte[] ip) {
// use the Little endian byte order to compatible with the old searcher implementation
buff[offset++] = ip[3];
buff[offset++] = ip[2];
buff[offset++] = ip[1];
buff[offset ] = ip[0];
return ip.length;
}
@Override
public int ipSubCompare(byte[] ip1, byte[] buff, int 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.
int j = offset + ip1.length - 1;
for (int i = 0; i < ip1.length; i++, j--) {
final int i1 = (int) (ip1[i] & 0xFF);
final int i2 = (int) (buff[j] & 0xFF);
if (i1 < i2) {
return -1;
}
if (i1 > i2) {
return 1;
}
}
return 0;
}
}

View File

@ -0,0 +1,29 @@
// 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.
package org.lionsoul.ip2region.xdb;
// IPv4 version implementation
// @Author Lion <chenxin619315@gmail.com>
// @Date 2025/09/10
public class IPv6 extends Version {
public IPv6() {
// segmentIndex: 16 + 16 + 2 + 4
super(6, "IPv6", 16, 38);
}
@Override
public int putBytes(byte[] buff, int offset, byte[] ip) {
System.arraycopy(ip, 0, buff, offset, ip.length);
return ip.length;
}
@Override
public int ipSubCompare(byte[] ip1, byte[] buff, int offset) {
return Util.ipSubCompare(ip1, buff, offset);
}
}

View File

@ -0,0 +1,13 @@
// 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.
package org.lionsoul.ip2region.xdb;
public class InetAddressException extends Exception {
public InetAddressException(String str) {
super(str);
}
}

View File

@ -0,0 +1,58 @@
// 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.
package org.lionsoul.ip2region.xdb;
// Little Endian basic data type decode and encode.
// @Author Lion <chenxin619315@gmail.com>
// @Date 2025/09/10
public class LittleEndian {
public final static int[] shiftIndex = {0, 8, 16, 24, 32, 40, 48, 56};
// put specified bytes to the buffer started from the offset
public static void put(final byte[] buff, int offset, long value, int bytes) {
if (bytes > 8) {
throw new IndexOutOfBoundsException("bytes should be <= 8");
}
for (int i = 0; i < bytes; i++) {
buff[offset++] = (byte)((value >>> shiftIndex[i]) & 0xFF);
}
}
// put an uint32 (4 bytes long) to the buffer from the offset
public static void putUint32(final byte[] buff, int offset, long value) {
buff[offset++] = (byte) (value & 0xFF);
buff[offset++] = (byte) ((value >> 8) & 0xFF);
buff[offset++] = (byte) ((value >> 16) & 0xFF);
buff[offset ] = (byte) ((value >> 24) & 0xFF);
}
// put a 2-bytes int to the buffer from the specified offset
public static void putInt2(final byte[] buff, int offset, int value) {
buff[offset++] = (byte) (value & 0xFF);
buff[offset ] = (byte) ((value >> 8) & 0xFF);
}
// get an uint32 from a byte array from the specified offset
public static long getUint32(final byte[] buff, int offset) {
return (
((buff[offset++] & 0x000000FFL)) |
((buff[offset++] << 8) & 0x0000FF00L) |
((buff[offset++] << 16) & 0x00FF0000L) |
((buff[offset ] << 24) & 0xFF000000L)
);
}
// get an 2 bytes int from a byte array from the specified offset
public static int getInt2(final byte[] buff, int offset) {
return (
((buff[offset++]) & 0x000000FF) |
((buff[offset ] << 8) & 0x0000FF00)
);
}
}

View File

@ -0,0 +1,119 @@
// 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.
//
// @Author Lion <chenxin619315@gmail.com>
// @Date 2022/07/14
package org.lionsoul.ip2region.xdb;
import java.text.SimpleDateFormat;
import java.util.Date;
// simple log implementation
public class Log {
/* Log level constants define */
public static final int DEBUG = 0;
public static final int INFO = 1;
public static final int WARN = 2;
public static final int ERROR = 3;
// level name
public static final String[] level_string = new String[] {
"DEBUG",
"INFO",
"WARN",
"ERROR"
};
public final Class<?> baseClass;
private int level = INFO;
public Log(Class<?> baseClass) {
this.baseClass = baseClass;
}
public static Log getLogger(Class<?> baseClass) {
return new Log(baseClass);
}
public String format(int level, String format, Object... args) {
// append the datetime
final StringBuilder sb = new StringBuilder();
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sb.append(String.format("%s %-5s ", sdf.format(new Date()), level_string[level]));
// append the class name
sb.append(baseClass.getName()).append(' ');
sb.append(String.format(format, args));
return sb.toString();
}
public void printf(int level, String format, Object... args) {
if (level < DEBUG || level > ERROR) {
throw new IndexOutOfBoundsException("invalid level index " + level);
}
// level filter
if (level < this.level) {
return;
}
System.out.println(format(level, format, args));
System.out.flush();
}
public String getDebugf(String format, Object... args) {
return format(DEBUG, format, args);
}
public void debugf(String format, Object... args) {
printf(DEBUG, format, args);
}
public String getInfof(String format, Object... args) {
return format(INFO, format, args);
}
public void infof(String format, Object... args) {
printf(INFO, format, args);
}
public String getWarnf(String format, Object... args) {
return format(WARN, format, args);
}
public void warnf(String format, Object... args) {
printf(WARN, format, args);
}
public String getErrorf(String format, Object... args) {
return format(ERROR, format, args);
}
public void errorf(String format, Object... args) {
printf(ERROR, format, args);
}
public Log setLevel(int level) {
this.level = level;
return this;
}
public Log setLevel(String level) {
String v = level.toLowerCase();
if ("debug".equals(v)) {
this.level = DEBUG;
} else if ("info".equals(v)) {
this.level = INFO;
} else if ("warn".equals(v)) {
this.level = WARN;
} else if ("error".equals(v)) {
this.level = ERROR;
}
return this;
}
}

View File

@ -104,27 +104,17 @@ public class LongByteArray {
return copy(offset, buffer, 0, length); return copy(offset, buffer, 0, length);
} }
// get a 4-bytes long integer from the specified index // get a 4-bytes uint32 integer from the specified index
public long getIntLong(long offset) { public long getUint32(long offset) {
final byte[] b = new byte[4]; final byte[] b = new byte[4];
copy(offset, b, 0, 4); copy(offset, b, 0, 4);
return ( return LittleEndian.getUint32(b, 0);
((b[0] & 0x000000FFL)) |
((b[1] << 8) & 0x0000FF00L) |
((b[2] << 16) & 0x00FF0000L) |
((b[3] << 24) & 0xFF000000L)
);
} }
public int getInt(long offset) { public int getInt2(long offset) {
final byte[] b = new byte[4]; final byte[] b = new byte[4];
copy(offset, b, 0, 4); copy(offset, b, 0, 4);
return ( return LittleEndian.getInt2(b, 0);
((b[0] & 0x000000FF)) |
((b[1] << 8) & 0x0000FF00) |
((b[2] << 16) & 0x00FF0000) |
((b[3] << 24) & 0xFF000000)
);
} }
// position entry class // position entry class

View File

@ -8,21 +8,26 @@ package org.lionsoul.ip2region.xdb;
// @Author Lion <chenxin619315@gmail.com> // @Author Lion <chenxin619315@gmail.com>
// @Date 2022/06/23 // @Date 2022/06/23
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
public class Searcher { public class Searcher {
// xdb structure version no
public static final int STRUCTURE_20 = 2;
public static final int STRUCTURE_30 = 3;
// constant defined copied from the xdb maker // constant defined copied from the xdb maker
public static final int HeaderInfoLength = 256; public static final int HeaderInfoLength = 256;
public static final int VectorIndexRows = 256; public static final int VectorIndexRows = 256;
public static final int VectorIndexCols = 256; public static final int VectorIndexCols = 256;
public static final int VectorIndexSize = 8; public static final int VectorIndexSize = 8;
public static final int SegmentIndexSize = 14;
// Linux max write / read bytes // Linux max write / read bytes
public static final int MAX_WRITE_BYTES = 0x7ffff000; public static final int MAX_WRITE_BYTES = 0x7ffff000;
// ip version
private final Version version;
// random access file handle for file-based search // random access file handle for file-based search
private final RandomAccessFile handle; private final RandomAccessFile handle;
@ -40,21 +45,22 @@ public class Searcher {
// --- static method to create searchers // --- static method to create searchers
public static Searcher newWithFileOnly(String dbPath) throws IOException { public static Searcher newWithFileOnly(Version version, String dbPath) throws IOException {
return new Searcher(dbPath, null, null); return new Searcher(version, dbPath, null, null);
} }
public static Searcher newWithVectorIndex(String dbPath, byte[] vectorIndex) throws IOException { public static Searcher newWithVectorIndex(Version version, String dbPath, byte[] vectorIndex) throws IOException {
return new Searcher(dbPath, vectorIndex, null); return new Searcher(version, dbPath, vectorIndex, null);
} }
public static Searcher newWithBuffer(LongByteArray cBuff) throws IOException { public static Searcher newWithBuffer(Version version, LongByteArray cBuff) throws IOException {
return new Searcher(null, null, cBuff); return new Searcher(version, null, null, cBuff);
} }
// --- End of creator // --- End of creator
public Searcher(String dbFile, byte[] vectorIndex, LongByteArray cBuff) throws IOException { public Searcher(Version version, String dbFile, byte[] vectorIndex, LongByteArray cBuff) throws IOException {
this.version = version;
if (cBuff != null) { if (cBuff != null) {
this.handle = null; this.handle = null;
this.vectorIndex = null; this.vectorIndex = null;
@ -72,64 +78,70 @@ public class Searcher {
} }
} }
public Version getIPVersion() {
return version;
}
public int getIOCount() { public int getIOCount() {
return ioCount; return ioCount;
} }
public String search(String ipStr) throws Exception { public String search(String ipStr) throws Exception {
long ip = checkIP(ipStr); return search(Util.parseIP(ipStr));
return search(ip); }
public String search(byte[] ip) throws IOException, InetAddressException {
// ip version check
if (ip.length != version.bytes) {
throw new InetAddressException("invalid ip address ("+version.name+" expected)");
} }
public String search(long ip) throws IOException {
// reset the global counter // reset the global counter
this.ioCount = 0; this.ioCount = 0;
// locate the segment index block based on the vector index // locate the segment index block based on the vector index
long sPtr = 0, ePtr = 0; long sPtr = 0, ePtr = 0;
int il0 = (int) ((ip >> 24) & 0xFF); int il0 = (int) (ip[0] & 0xFF);
int il1 = (int) ((ip >> 16) & 0xFF); int il1 = (int) (ip[1] & 0xFF);
int idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize; int idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize;
// System.out.printf("il0: %d, il1: %d, idx: %d\n", il0, il1, idx); // System.out.printf("il0: %d, il1: %d, idx: %d\n", il0, il1, idx);
if (vectorIndex != null) { if (vectorIndex != null) {
sPtr = getIntLong(vectorIndex, idx); sPtr = LittleEndian.getUint32(vectorIndex, idx);
ePtr = getIntLong(vectorIndex, idx + 4); ePtr = LittleEndian.getUint32(vectorIndex, idx + 4);
} else if (contentBuff != null) { } else if (contentBuff != null) {
sPtr = contentBuff.getIntLong(HeaderInfoLength + idx); sPtr = contentBuff.getUint32(HeaderInfoLength + idx);
ePtr = contentBuff.getIntLong(HeaderInfoLength + idx + 4); ePtr = contentBuff.getUint32(HeaderInfoLength + idx + 4);
} else { } else {
final byte[] buff = new byte[VectorIndexSize]; final byte[] buff = new byte[VectorIndexSize];
read(HeaderInfoLength + idx, buff); read(HeaderInfoLength + idx, buff);
sPtr = getIntLong(buff, 0); sPtr = LittleEndian.getUint32(buff, 0);
ePtr = getIntLong(buff, 4); ePtr = LittleEndian.getUint32(buff, 4);
} }
// System.out.printf("sPtr: %d, ePtr: %d\n", sPtr, ePtr); // System.out.printf("sPtr: %d, ePtr: %d\n", sPtr, ePtr);
// binary search the segment index block to get the region info // binary search the segment index block to get the region info
final byte[] buff = new byte[SegmentIndexSize]; final int bytes = ip.length, dBytes = ip.length << 1;
final int segIndexSize = version.segmentIndexSize;
final byte[] buff = new byte[segIndexSize];
int dataLen = -1; int dataLen = -1;
long dataPtr = -1, l = 0, h = (ePtr - sPtr) / SegmentIndexSize; long dataPtr = -1, l = 0, h = (ePtr - sPtr) / segIndexSize;
while (l <= h) { while (l <= h) {
long m = (l + h) >> 1; long m = (l + h) >> 1;
long p = sPtr + m * SegmentIndexSize; long p = sPtr + m * segIndexSize;
// read the segment index // read the segment index
read(p, buff); read(p, buff);
long sip = getIntLong(buff, 0); if (version.ipSubCompare(ip, buff, 0) < 0) {
if (ip < sip) {
h = m - 1; h = m - 1;
} else { } else if (version.ipSubCompare(ip, buff, bytes) > 0) {
long eip = getIntLong(buff, 4);
if (ip > eip) {
l = m + 1; l = m + 1;
} else { } else {
dataLen = getInt2(buff, 8); dataLen = LittleEndian.getInt2(buff, dBytes);
dataPtr = getIntLong(buff, 10); dataPtr = LittleEndian.getUint32(buff, dBytes + 2);
break; break;
} }
} }
}
// empty match interception // empty match interception
// System.out.printf("dataLen: %d, dataPtr: %d\n", dataLen, dataPtr); // System.out.printf("dataLen: %d, dataPtr: %d\n", dataLen, dataPtr);
@ -161,7 +173,7 @@ public class Searcher {
} }
} }
// --- static cache util function // --- static util function
public static Header loadHeader(RandomAccessFile handle) throws IOException { public static Header loadHeader(RandomAccessFile handle) throws IOException {
handle.seek(0); handle.seek(0);
@ -222,63 +234,39 @@ public class Searcher {
return content; return content;
} }
// --- End cache load util function // --- verify util function
// --- static util method // Verify if the current Searcher could be used to search the specified xdb file.
// Why do we need this check ?
// The future features of the xdb impl may cause the current searcher not able to work properly.
//
// @Note: You Just need to check this ONCE when the service starts
// Or use another process (eg, A command) to check once Just to confirm the suitability.
public static void verify(RandomAccessFile handle) throws IOException, XdbException {
final Header header = loadHeader(handle);
/* get an int from a byte array start from the specified offset */ // get the runtime ptr bytes
public static long getIntLong(byte[] b, int offset) { int runtimePtrBytes = 0;
return ( if (header.version == STRUCTURE_20) {
((b[offset++] & 0x000000FFL)) | runtimePtrBytes = 4;
((b[offset++] << 8) & 0x0000FF00L) | } else if (header.version == STRUCTURE_30) {
((b[offset++] << 16) & 0x00FF0000L) | runtimePtrBytes = header.runtimePtrBytes;
((b[offset ] << 24) & 0xFF000000L) } else {
); throw new XdbException("invalid structure version `" + header.version + "`");
} }
public static int getInt(byte[] b, int offset) { // 1, confirm the xdb file size
return ( // to ensure that the maximum file pointer does not overflow
((b[offset++] & 0x000000FF)) | final long maxFilePtr = (1L << (runtimePtrBytes * 8)) - 1;
((b[offset++] << 8) & 0x0000FF00) | if (handle.length() > maxFilePtr) {
((b[offset++] << 16) & 0x00FF0000) | throw new XdbException("xdb file exceeds the maximum supported bytes: "+maxFilePtr+"");
((b[offset ] << 24) & 0xFF000000) }
);
} }
public static int getInt2(byte[] b, int offset) { public static void verifyFromFile(String dbFile) throws IOException, XdbException {
return ( final RandomAccessFile handle = new RandomAccessFile(dbFile, "r");
((b[offset++] & 0x000000FF)) | verify(handle);
((b[offset ] << 8) & 0x0000FF00) handle.close();
);
}
/* long int to ip string */
public static String long2ip( long ip )
{
return String.valueOf((ip >> 24) & 0xFF) + '.' +
((ip >> 16) & 0xFF) + '.' + ((ip >> 8) & 0xFF) + '.' + ((ip) & 0xFF);
}
public static final byte[] shiftIndex = {24, 16, 8, 0};
/* check the specified ip address */
public static long checkIP(String ip) throws Exception {
String[] ps = ip.split("\\.");
if (ps.length != 4) {
throw new Exception("invalid ip address `" + ip + "`");
}
long ipDst = 0;
for (int i = 0; i < ps.length; i++) {
int val = Integer.parseInt(ps[i]);
if (val > 255) {
throw new Exception("ip part `"+ps[i]+"` should be less then 256");
}
ipDst |= ((long) val << shiftIndex[i]);
}
return ipDst & 0xFFFFFFFFL;
} }
} }

View File

@ -0,0 +1,78 @@
// 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.
//
// @Author Lion <chenxin619315@gmail.com>
// @Date 2022/07/14
package org.lionsoul.ip2region.xdb;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class Util
{
// parse the specified IP address and return its bytes.
// returns: byte[4] for IPv4 and byte[16] for IPv6 and the bytes should be in Big endian order.
public static byte[] parseIP(String ip) throws InetAddressException {
try {
return InetAddress.getByName(ip).getAddress();
} catch (UnknownHostException e) {
throw new InetAddressException("invalid ip address `"+ip+"`");
}
}
// convert the byte[] ip to string ip address
public static String ipToString(final byte[] ip) {
if (ip.length != 4 && ip.length != 16) {
return String.format("invalid-ip-address-length: %d", ip.length);
}
try {
return InetAddress.getByAddress(ip).getHostAddress();
} catch (UnknownHostException e) {
return String.format("invalid-ip-address `%s`", ipArrayString(ip));
}
}
// implode the byte[] ip with its byte value.
public static String ipArrayString(byte[] ip) {
final StringBuffer sb = new StringBuffer();
sb.append("[");
for (int i = 0; i < ip.length; i++) {
if (i > 0) {
sb.append(',');
}
sb.append((ip[i] & 0xFF));
}
sb.append("]");
return sb.toString();
}
// compare two byte ip
// Returns: -1 if ip1 < ip2, 0 if ip1 == ip2, 1 if ip1 > ip2
public static int ipCompare(byte[] ip1, byte[] ip2) {
return ipSubCompare(ip1, ip2, 0);
}
// compare the ip with the ip in the buffer start from offset
// Returns: -1 if ip < buff[offset], 0 if ip == buff[offset], 1 if ip > buff[offset]
public static int ipSubCompare(byte[] ip, byte[] buff, int offset) {
for (int i = 0; i < ip.length; i++) {
// covert the byte to int to sure the uint8 attribute
final int i1 = (int)(ip[i] & 0xFF);
final int i2 = (int)(buff[offset+i] & 0xFF);
if (i1 < i2) {
return -1;
}
if (i1 > i2) {
return 1;
}
}
return 0;
}
}

View File

@ -0,0 +1,83 @@
// Copyright 2025 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.
package org.lionsoul.ip2region.xdb;
// IP version abstract manager (IPv4 & IPv6)
// @Author Lion <chenxin619315@gmail.com>
// @Date 2025/09/10
public abstract class Version {
public static final int IPv4VersionNo = 4;
public static final int IPv6VersionNo = 6;
public static final IPv4 IPv4 = new IPv4();
public static final IPv6 IPv6 = new IPv6();
// version id and name
public final int id;
public final String name;
// the numbers of bytes for one IP
public final int bytes;
// segment index size (bytes)
public final int segmentIndexSize;
public Version(int id, String name, int bytes, int segmentIndexSize) {
this.id = id;
this.name = name;
this.bytes = bytes;
this.segmentIndexSize = segmentIndexSize;
}
// encode the specified IP bytes to the specified buffer
public abstract int putBytes(byte[] buff, int offset, byte[] ip);
// compare the two IPs with the current version.
// Returns: -1 if ip1 < ip2, 0 if ip1 == ip2, 1 if ip1 > ip2
public int ipCompare(byte[] ip1, byte[] ip2) {
return ipSubCompare(ip1, ip2, 0);
}
// @see ipCompare
public abstract int ipSubCompare(byte[] ip1, byte[] buff, int offset);
// parse the version from an name
public static final Version fromName(String name) throws Exception {
final String n = name.toUpperCase();
if (n.equals("V4") || n.equals("IPV4")) {
return IPv4;
} else if (n.equals("V6") || n.equals("IPV6")) {
return IPv6;
} else {
throw new Exception("invalid version name `"+name+"`");
}
}
// parse the version from header
public static final Version fromHeader(Header header) throws XdbException {
// Old 2.0 structure with IPv4 supports ONLY.
if (header.version == Searcher.STRUCTURE_20) {
return IPv4;
}
// structure 3.0 after IPv6 supporting
if (header.version != Searcher.STRUCTURE_30) {
throw new XdbException("invalid xdb structure version `"+header.version+"`");
}
if (header.ipVersion == IPv4VersionNo) {
return IPv4;
} else if (header.ipVersion == IPv6VersionNo) {
return IPv6;
} else {
throw new XdbException("invalid ip version number `" + header.ipVersion + "`");
}
}
@Override public String toString() {
return String.format("{Id:%d, Name:%s, Bytes:%d, IndexSize: %d}", id, name, bytes, segmentIndexSize);
}
}

View File

@ -0,0 +1,13 @@
// 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.
package org.lionsoul.ip2region.xdb;
public class XdbException extends Exception {
public XdbException(String str) {
super(str);
}
}

View File

@ -0,0 +1,5 @@
package xdb;
public class Version {
}

View File

@ -0,0 +1,36 @@
package org.lionsoul.ip2region.xdb;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class LittleEndianTest {
private static final Log log = Log.getLogger(LittleEndianTest.class).setLevel(Log.DEBUG);
@Test
public void testAll() {
final byte[] buff = new byte[14];
// encode
// do the put
LittleEndian.put(buff, 0, 1L, 4);
LittleEndian.put(buff, 4, 2L, 4);
// putUint32
LittleEndian.putInt2(buff, 8, 24);
LittleEndian.putUint32(buff, 10, 1024L);
// decode
assertEquals(LittleEndian.getUint32(buff, 0), 1);
assertEquals(LittleEndian.getUint32(buff, 4), 2);
assertEquals(LittleEndian.getInt2(buff, 8), 24);
assertEquals(LittleEndian.getUint32(buff, 10), 1024);
log.debugf("uint32(buff, 0): %d", LittleEndian.getUint32(buff, 0));
log.debugf("uint32(buff, 4): %d", LittleEndian.getUint32(buff, 4));
log.debugf("int2(buff, 8): %d", LittleEndian.getInt2(buff, 8));
log.debugf("uint32(buff, 10): %d", LittleEndian.getUint32(buff, 10));
}
}

View File

@ -0,0 +1,45 @@
package org.lionsoul.ip2region.xdb;
import org.junit.Test;
public class UtilTest {
private static final Log log = Log.getLogger(UtilTest.class).setLevel(Log.DEBUG);
@Test
public void testCheckIP() throws InetAddressException {
final String[] ips = new String[]{
"192.168.1.102",
"219.133.111.87",
"::",
"3000::",
"::1001:ffff",
"2001:2:0:ffff:ffff:ffff:ffff:ffff",
"::ffff:114.114.114.114"
};
for (String ip : ips) {
final byte[] ipBytes = Util.parseIP(ip);
log.debugf("%s(v=%s) => %s", ip, Util.ipArrayString(ipBytes), Util.ipToString(ipBytes));
}
}
@Test
public void testIpCompare() throws InetAddressException {
final String[][] ipPairs = new String[][]{
{"1.0.0.0", "1.0.0.1"},
{"192.168.1.101", "192.168.1.90"},
{"219.133.111.87", "114.114.114.114"},
{"2000::", "2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff"},
{"2001:4:112::", "2001:4:112:ffff:ffff:ffff:ffff:ffff"},
{"ffff::", "2001:4:ffff:ffff:ffff:ffff:ffff:ffff"}
};
for (String[] ips : ipPairs) {
final byte[] ip1 = Util.parseIP(ips[0]);
final byte[] ip2 = Util.parseIP(ips[1]);
log.debugf("compare(%s, %s): %d", ips[0], ips[1], Util.ipCompare(ip1, ip2));
}
}
}

View File

@ -0,0 +1,22 @@
package org.lionsoul.ip2region.xdb;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class VersionTest {
private static final Log log = Log.getLogger(VersionTest.class).setLevel(Log.DEBUG);
@Test
public void testFromName() throws Exception {
final String[] vers = new String[]{"IPv4", "IPv6"};
final Version v4 = Version.fromName(vers[0]);
assertEquals(v4.name, vers[0]);
final Version v6 = Version.fromName(vers[1]);
assertEquals(v6.name, vers[1]);
log.debugf("v4: %s", v4);
log.debugf("v6: %s", v6);
}
}