IPv6 supporting for Java searcher
This commit is contained in:
parent
c6c0ee954f
commit
6db9654c7d
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<groupId>org.lionsoul</groupId>
|
||||
<artifactId>ip2region</artifactId>
|
||||
<version>2.8.1</version>
|
||||
<version>3.1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>ip2region</name>
|
||||
|
|
@ -41,8 +41,8 @@
|
|||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<maven.compiler.source>1.6</maven.compiler.source>
|
||||
<maven.compiler.target>1.6</maven.compiler.target>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.lionsoul.ip2region.SearchTest</mainClass>
|
||||
<mainClass>org.lionsoul.ip2region.SearchApp</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,21 +8,26 @@ package org.lionsoul.ip2region.xdb;
|
|||
// @Author Lion <chenxin619315@gmail.com>
|
||||
// @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,64 +78,70 @@ 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(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
|
||||
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 {
|
||||
long eip = getIntLong(buff, 4);
|
||||
if (ip > eip) {
|
||||
} else if (version.ipSubCompare(ip, buff, bytes) > 0) {
|
||||
l = m + 1;
|
||||
} else {
|
||||
dataLen = getInt2(buff, 8);
|
||||
dataPtr = getIntLong(buff, 10);
|
||||
dataLen = LittleEndian.getInt2(buff, dBytes);
|
||||
dataPtr = LittleEndian.getUint32(buff, dBytes + 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// empty match interception
|
||||
// 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 {
|
||||
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)
|
||||
);
|
||||
// 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 + "`");
|
||||
}
|
||||
|
||||
public static int getInt(byte[] b, int offset) {
|
||||
return (
|
||||
((b[offset++] & 0x000000FF)) |
|
||||
((b[offset++] << 8) & 0x0000FF00) |
|
||||
((b[offset++] << 16) & 0x00FF0000) |
|
||||
((b[offset ] << 24) & 0xFF000000)
|
||||
);
|
||||
// 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+"");
|
||||
}
|
||||
}
|
||||
|
||||
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 + "`");
|
||||
}
|
||||
|
||||
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;
|
||||
public static void verifyFromFile(String dbFile) throws IOException, XdbException {
|
||||
final RandomAccessFile handle = new RandomAccessFile(dbFile, "r");
|
||||
verify(handle);
|
||||
handle.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package xdb;
|
||||
|
||||
public class Version {
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue