From 6db9654c7dc2fd42e13d1f9671d7d0e4048c409a Mon Sep 17 00:00:00 2001 From: lion Date: Fri, 12 Sep 2025 12:41:00 +0800 Subject: [PATCH] IPv6 supporting for Java searcher --- binding/java/pom.xml | 8 +- .../{SearchTest.java => SearchApp.java} | 57 ++++--- .../java/org/lionsoul/ip2region/UtilTest.java | 85 ---------- .../org/lionsoul/ip2region/xdb/Header.java | 27 ++- .../java/org/lionsoul/ip2region/xdb/IPv4.java | 47 ++++++ .../java/org/lionsoul/ip2region/xdb/IPv6.java | 29 ++++ .../ip2region/xdb/InetAddressException.java | 13 ++ .../lionsoul/ip2region/xdb/LittleEndian.java | 58 +++++++ .../java/org/lionsoul/ip2region/xdb/Log.java | 119 +++++++++++++ .../lionsoul/ip2region/xdb/LongByteArray.java | 20 +-- .../org/lionsoul/ip2region/xdb/Searcher.java | 158 ++++++++---------- .../java/org/lionsoul/ip2region/xdb/Util.java | 78 +++++++++ .../org/lionsoul/ip2region/xdb/Version.java | 83 +++++++++ .../lionsoul/ip2region/xdb/XdbException.java | 13 ++ binding/java/src/main/java/xdb/Version.java | 5 + .../ip2region/xdb/LittleEndianTest.java | 36 ++++ .../org/lionsoul/ip2region/xdb/UtilTest.java | 45 +++++ .../lionsoul/ip2region/xdb/VersionTest.java | 22 +++ 18 files changed, 686 insertions(+), 217 deletions(-) rename binding/java/src/main/java/org/lionsoul/ip2region/{SearchTest.java => SearchApp.java} (82%) delete mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/UtilTest.java create mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv4.java create mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv6.java create mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/xdb/InetAddressException.java create mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/xdb/LittleEndian.java create mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/xdb/Log.java create mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java create mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/xdb/Version.java create mode 100644 binding/java/src/main/java/org/lionsoul/ip2region/xdb/XdbException.java create mode 100644 binding/java/src/main/java/xdb/Version.java create mode 100644 binding/java/src/test/java/org/lionsoul/ip2region/xdb/LittleEndianTest.java create mode 100644 binding/java/src/test/java/org/lionsoul/ip2region/xdb/UtilTest.java create mode 100644 binding/java/src/test/java/org/lionsoul/ip2region/xdb/VersionTest.java diff --git a/binding/java/pom.xml b/binding/java/pom.xml index d531daf..b9027b3 100644 --- a/binding/java/pom.xml +++ b/binding/java/pom.xml @@ -4,7 +4,7 @@ org.lionsoul ip2region - 2.8.1 + 3.1.0 jar ip2region @@ -41,8 +41,8 @@ UTF-8 UTF-8 - 1.6 - 1.6 + 1.8 + 1.8 @@ -105,7 +105,7 @@ - org.lionsoul.ip2region.SearchTest + org.lionsoul.ip2region.SearchApp diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/SearchTest.java b/binding/java/src/main/java/org/lionsoul/ip2region/SearchApp.java similarity index 82% rename from binding/java/src/main/java/org/lionsoul/ip2region/SearchTest.java rename to binding/java/src/main/java/org/lionsoul/ip2region/SearchApp.java index bc6b53f..68a9f93 100644 --- a/binding/java/src/main/java/org/lionsoul/ip2region/SearchTest.java +++ b/binding/java/src/main/java/org/lionsoul/ip2region/SearchApp.java @@ -6,15 +6,18 @@ 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.Searcher; +import org.lionsoul.ip2region.xdb.Util; +import org.lionsoul.ip2region.xdb.Version; import java.io.*; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; -public class SearchTest { +public class SearchApp { public static void printHelp(String[] args) { System.out.print("ip2region xdb searcher\n"); @@ -24,21 +27,33 @@ public class SearchTest { 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)) { - return Searcher.newWithFileOnly(dbPath); + return Searcher.newWithFileOnly(version, dbPath); } else if ("vectorIndex".equals(cachePolicy)) { byte[] vIndex = Searcher.loadVectorIndexFromFile(dbPath); - return Searcher.newWithVectorIndex(dbPath, vIndex); + return Searcher.newWithVectorIndex(version, dbPath, vIndex); } else if ("content".equals(cachePolicy)) { LongByteArray cBuff = Searcher.loadContentFromFile(dbPath); - return Searcher.newWithBuffer(cBuff); + return Searcher.newWithBuffer(version, cBuff); } else { 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"; for (final String r : args) { if (r.length() < 5) { @@ -105,7 +120,7 @@ public class SearchTest { 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"; for (final String r : args) { if (r.length() < 5) { @@ -155,40 +170,44 @@ public class SearchTest { String l = line.trim(); String[] ps = l.split("\\|", 3); if (ps.length != 3) { + reader.close(); System.out.printf("invalid ip segment `%s`\n", l); return; } - long sip; + byte[] sip; try { - sip = Searcher.checkIP(ps[0]); + sip = Util.parseIP(ps[0]); } catch (Exception e) { + reader.close(); System.out.printf("check start ip `%s`: %s\n", ps[0], e); return; } - long eip; + byte[] eip; try { - eip = Searcher.checkIP(ps[1]); + eip = Util.parseIP(ps[1]); } catch (Exception e) { + reader.close(); System.out.printf("check end ip `%s`: %s\n", ps[1], e); 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]); return; } - long mip = (sip + eip) >> 1; - for (final long ip : new long[]{sip, (sip + mip) >> 1, mip, (mip + eip) >> 1, eip}) { + for (final byte[] ip : new byte[][]{sip, eip}) { long sTime = System.nanoTime(); String region = searcher.search(ip); costs += System.nanoTime() - sTime; // check the region info 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; } @@ -213,14 +232,14 @@ public class SearchTest { if ("search".equals(args[0])) { try { searchTest(args); - } catch (IOException e) { + } catch (Exception e) { System.out.printf("failed running search test: %s\n", e); } } else if ("bench".equals(args[0])) { try { benchTest(args); - } catch (IOException e) { - System.out.printf("failed running bench test: %s\n", e); + } catch (Exception e) { + System.out.printf("fwailed running bench test: %s\n", e); } } else { printHelp(args); diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/UtilTest.java b/binding/java/src/main/java/org/lionsoul/ip2region/UtilTest.java deleted file mode 100644 index db5cbbb..0000000 --- a/binding/java/src/main/java/org/lionsoul/ip2region/UtilTest.java +++ /dev/null @@ -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 -// @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(); - } - -} diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Header.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Header.java index a55cedc..a6305f1 100644 --- a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Header.java +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Header.java @@ -9,18 +9,25 @@ package org.lionsoul.ip2region.xdb; public class Header { public final int version; public final int indexPolicy; - public final int createdAt; - public final int startIndexPtr; - public final int endIndexPtr; + public final long createdAt; + public final long startIndexPtr; + public final long endIndexPtr; + + // since xdb 3.0 with IPv6 supporting + public final int ipVersion; + public final int runtimePtrBytes; + public final byte[] buffer; public Header(byte[] buff) { assert buff.length >= 16; - version = Searcher.getInt2(buff, 0); - indexPolicy = Searcher.getInt2(buff, 2); - createdAt = Searcher.getInt(buff, 4); - startIndexPtr = Searcher.getInt(buff, 8); - endIndexPtr = Searcher.getInt(buff, 12); + version = LittleEndian.getInt2(buff, 0); + indexPolicy = LittleEndian.getInt2(buff, 2); + createdAt = LittleEndian.getUint32(buff, 4); + startIndexPtr = LittleEndian.getUint32(buff, 8); + endIndexPtr = LittleEndian.getUint32(buff, 12); + ipVersion = LittleEndian.getInt2(buff, 16); + runtimePtrBytes = LittleEndian.getInt2(buff, 18); buffer = buff; } @@ -30,7 +37,9 @@ public class Header { "IndexPolicy: " + indexPolicy + ',' + "CreatedAt: " + createdAt + ',' + "StartIndexPtr: " + startIndexPtr + ',' + - "EndIndexPtr: " + endIndexPtr + + "EndIndexPtr: " + endIndexPtr + ',' + + "IPVersion: " + ipVersion + ',' + + "RuntimePtrBytes: " + runtimePtrBytes + '}'; } } diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv4.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv4.java new file mode 100644 index 0000000..bd07013 --- /dev/null +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv4.java @@ -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 +// @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; + } +} diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv6.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv6.java new file mode 100644 index 0000000..af7349d --- /dev/null +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/IPv6.java @@ -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 +// @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); + } + +} diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/InetAddressException.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/InetAddressException.java new file mode 100644 index 0000000..369c2b8 --- /dev/null +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/InetAddressException.java @@ -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); + } + +} diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/LittleEndian.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/LittleEndian.java new file mode 100644 index 0000000..2a8b7b0 --- /dev/null +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/LittleEndian.java @@ -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 +// @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) + ); + } + +} diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Log.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Log.java new file mode 100644 index 0000000..a5297fe --- /dev/null +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Log.java @@ -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 +// @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; + } + +} \ No newline at end of file diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/LongByteArray.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/LongByteArray.java index aa85b6b..3fb2a07 100644 --- a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/LongByteArray.java +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/LongByteArray.java @@ -104,27 +104,17 @@ public class LongByteArray { return copy(offset, buffer, 0, length); } - // get a 4-bytes long integer from the specified index - public long getIntLong(long offset) { + // get a 4-bytes uint32 integer from the specified index + public long getUint32(long offset) { final byte[] b = new byte[4]; copy(offset, b, 0, 4); - return ( - ((b[0] & 0x000000FFL)) | - ((b[1] << 8) & 0x0000FF00L) | - ((b[2] << 16) & 0x00FF0000L) | - ((b[3] << 24) & 0xFF000000L) - ); + return LittleEndian.getUint32(b, 0); } - public int getInt(long offset) { + public int getInt2(long offset) { final byte[] b = new byte[4]; copy(offset, b, 0, 4); - return ( - ((b[0] & 0x000000FF)) | - ((b[1] << 8) & 0x0000FF00) | - ((b[2] << 16) & 0x00FF0000) | - ((b[3] << 24) & 0xFF000000) - ); + return LittleEndian.getInt2(b, 0); } // position entry class diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java index 52fe054..974da13 100644 --- a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Searcher.java @@ -8,21 +8,26 @@ package org.lionsoul.ip2region.xdb; // @Author Lion // @Date 2022/06/23 - import java.io.IOException; import java.io.RandomAccessFile; 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 public static final int HeaderInfoLength = 256; public static final int VectorIndexRows = 256; public static final int VectorIndexCols = 256; public static final int VectorIndexSize = 8; - public static final int SegmentIndexSize = 14; // Linux max write / read bytes public static final int MAX_WRITE_BYTES = 0x7ffff000; + // ip version + private final Version version; + // random access file handle for file-based search private final RandomAccessFile handle; @@ -40,21 +45,22 @@ public class Searcher { // --- static method to create searchers - public static Searcher newWithFileOnly(String dbPath) throws IOException { - return new Searcher(dbPath, null, null); + public static Searcher newWithFileOnly(Version version, String dbPath) throws IOException { + return new Searcher(version, dbPath, null, null); } - public static Searcher newWithVectorIndex(String dbPath, byte[] vectorIndex) throws IOException { - return new Searcher(dbPath, vectorIndex, null); + public static Searcher newWithVectorIndex(Version version, String dbPath, byte[] vectorIndex) throws IOException { + return new Searcher(version, dbPath, vectorIndex, null); } - public static Searcher newWithBuffer(LongByteArray cBuff) throws IOException { - return new Searcher(null, null, cBuff); + public static Searcher newWithBuffer(Version version, LongByteArray cBuff) throws IOException { + return new Searcher(version, null, null, cBuff); } // --- 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) { this.handle = null; this.vectorIndex = null; @@ -72,62 +78,68 @@ public class Searcher { } } + public Version getIPVersion() { + return version; + } + public int getIOCount() { return ioCount; } public String search(String ipStr) throws Exception { - long ip = checkIP(ipStr); - return search(ip); + return search(Util.parseIP(ipStr)); } - public String search(long ip) throws IOException { + 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)"); + } + // reset the global counter this.ioCount = 0; // locate the segment index block based on the vector index long sPtr = 0, ePtr = 0; - int il0 = (int) ((ip >> 24) & 0xFF); - int il1 = (int) ((ip >> 16) & 0xFF); + int il0 = (int) (ip[0] & 0xFF); + int il1 = (int) (ip[1] & 0xFF); int idx = il0 * VectorIndexCols * VectorIndexSize + il1 * VectorIndexSize; // System.out.printf("il0: %d, il1: %d, idx: %d\n", il0, il1, idx); if (vectorIndex != null) { - sPtr = getIntLong(vectorIndex, idx); - ePtr = getIntLong(vectorIndex, idx + 4); + sPtr = LittleEndian.getUint32(vectorIndex, idx); + ePtr = LittleEndian.getUint32(vectorIndex, idx + 4); } else if (contentBuff != null) { - sPtr = contentBuff.getIntLong(HeaderInfoLength + idx); - ePtr = contentBuff.getIntLong(HeaderInfoLength + idx + 4); + sPtr = contentBuff.getUint32(HeaderInfoLength + idx); + ePtr = contentBuff.getUint32(HeaderInfoLength + idx + 4); } else { final byte[] buff = new byte[VectorIndexSize]; read(HeaderInfoLength + idx, buff); - sPtr = getIntLong(buff, 0); - ePtr = getIntLong(buff, 4); + sPtr = LittleEndian.getUint32(buff, 0); + ePtr = LittleEndian.getUint32(buff, 4); } // System.out.printf("sPtr: %d, ePtr: %d\n", sPtr, ePtr); // 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; - long dataPtr = -1, l = 0, h = (ePtr - sPtr) / SegmentIndexSize; + long dataPtr = -1, l = 0, h = (ePtr - sPtr) / segIndexSize; while (l <= h) { long m = (l + h) >> 1; - long p = sPtr + m * SegmentIndexSize; + long p = sPtr + m * segIndexSize; // read the segment index read(p, buff); - long sip = getIntLong(buff, 0); - if (ip < sip) { + if (version.ipSubCompare(ip, buff, 0) < 0) { h = m - 1; + } else if (version.ipSubCompare(ip, buff, bytes) > 0) { + l = m + 1; } else { - long eip = getIntLong(buff, 4); - if (ip > eip) { - l = m + 1; - } else { - dataLen = getInt2(buff, 8); - dataPtr = getIntLong(buff, 10); - break; - } + dataLen = LittleEndian.getInt2(buff, dBytes); + dataPtr = LittleEndian.getUint32(buff, dBytes + 2); + break; } } @@ -161,7 +173,7 @@ public class Searcher { } } - // --- static cache util function + // --- static util function public static Header loadHeader(RandomAccessFile handle) throws IOException { handle.seek(0); @@ -222,63 +234,39 @@ public class Searcher { 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 */ - public static long getIntLong(byte[] b, int offset) { - return ( - ((b[offset++] & 0x000000FFL)) | - ((b[offset++] << 8) & 0x0000FF00L) | - ((b[offset++] << 16) & 0x00FF0000L) | - ((b[offset ] << 24) & 0xFF000000L) - ); - } - - public static int getInt(byte[] b, int offset) { - return ( - ((b[offset++] & 0x000000FF)) | - ((b[offset++] << 8) & 0x0000FF00) | - ((b[offset++] << 16) & 0x00FF0000) | - ((b[offset ] << 24) & 0xFF000000) - ); - } - - public static int getInt2(byte[] b, int offset) { - return ( - ((b[offset++] & 0x000000FF)) | - ((b[offset ] << 8) & 0x0000FF00) - ); - } - - /* 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 + "`"); + // get the runtime ptr bytes + int runtimePtrBytes = 0; + if (header.version == STRUCTURE_20) { + runtimePtrBytes = 4; + } else if (header.version == STRUCTURE_30) { + runtimePtrBytes = header.runtimePtrBytes; + } else { + throw new XdbException("invalid structure version `" + header.version + "`"); } - 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]); + // 1, confirm the xdb file size + // to ensure that the maximum file pointer does not overflow + final long maxFilePtr = (1L << (runtimePtrBytes * 8)) - 1; + if (handle.length() > maxFilePtr) { + throw new XdbException("xdb file exceeds the maximum supported bytes: "+maxFilePtr+""); } + } - return ipDst & 0xFFFFFFFFL; + public static void verifyFromFile(String dbFile) throws IOException, XdbException { + final RandomAccessFile handle = new RandomAccessFile(dbFile, "r"); + verify(handle); + handle.close(); } } \ No newline at end of file diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java new file mode 100644 index 0000000..a394327 --- /dev/null +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Util.java @@ -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 +// @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; + } + +} diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Version.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Version.java new file mode 100644 index 0000000..e062a0d --- /dev/null +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/Version.java @@ -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 +// @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); + } +} \ No newline at end of file diff --git a/binding/java/src/main/java/org/lionsoul/ip2region/xdb/XdbException.java b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/XdbException.java new file mode 100644 index 0000000..afdded2 --- /dev/null +++ b/binding/java/src/main/java/org/lionsoul/ip2region/xdb/XdbException.java @@ -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); + } + +} diff --git a/binding/java/src/main/java/xdb/Version.java b/binding/java/src/main/java/xdb/Version.java new file mode 100644 index 0000000..986f689 --- /dev/null +++ b/binding/java/src/main/java/xdb/Version.java @@ -0,0 +1,5 @@ +package xdb; + +public class Version { + +} diff --git a/binding/java/src/test/java/org/lionsoul/ip2region/xdb/LittleEndianTest.java b/binding/java/src/test/java/org/lionsoul/ip2region/xdb/LittleEndianTest.java new file mode 100644 index 0000000..9b216a3 --- /dev/null +++ b/binding/java/src/test/java/org/lionsoul/ip2region/xdb/LittleEndianTest.java @@ -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)); + } + +} diff --git a/binding/java/src/test/java/org/lionsoul/ip2region/xdb/UtilTest.java b/binding/java/src/test/java/org/lionsoul/ip2region/xdb/UtilTest.java new file mode 100644 index 0000000..1b31b97 --- /dev/null +++ b/binding/java/src/test/java/org/lionsoul/ip2region/xdb/UtilTest.java @@ -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)); + } + } + +} diff --git a/binding/java/src/test/java/org/lionsoul/ip2region/xdb/VersionTest.java b/binding/java/src/test/java/org/lionsoul/ip2region/xdb/VersionTest.java new file mode 100644 index 0000000..8c38773 --- /dev/null +++ b/binding/java/src/test/java/org/lionsoul/ip2region/xdb/VersionTest.java @@ -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); + } +}