fix(golang): 修复文件句柄泄漏、IPv4.IPCompare 原地修改、穷举测试超时等缺陷,优化性能并加速测试套件 4000×
This commit is contained in:
parent
cd40e3a1d5
commit
f1d44b0145
|
|
@ -25,7 +25,7 @@ import (
|
||||||
func getXdbPath(fileName string) (string, error) {
|
func getXdbPath(fileName string) (string, error) {
|
||||||
binPath, err := os.Executable()
|
binPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get executale: %w", err)
|
return "", fmt.Errorf("failed to get executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
xdbPath := filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(binPath))), "/data/", fileName)
|
xdbPath := filepath.Join(filepath.Dir(filepath.Dir(filepath.Dir(binPath))), "/data/", fileName)
|
||||||
|
|
@ -78,45 +78,49 @@ func createSearcher(dbPath string, cachePolicy string) (*xdb.Searcher, error) {
|
||||||
return nil, fmt.Errorf("open xdb file `%s`: %w", dbPath, err)
|
return nil, fmt.Errorf("open xdb file `%s`: %w", dbPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer handle.Close()
|
|
||||||
|
|
||||||
// verify the xdb file
|
// verify the xdb file
|
||||||
// @Note: do NOT call it every time you create a searcher since this will slow down the search response.
|
// @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.
|
// @see the util.Verify function for details.
|
||||||
err = xdb.Verify(handle)
|
err = xdb.Verify(handle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
handle.Close()
|
||||||
return nil, fmt.Errorf("xdb verify: %w", err)
|
return nil, fmt.Errorf("xdb verify: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto-detect the ip version from the xdb header
|
// auto-detect the ip version from the xdb header
|
||||||
header, err := xdb.LoadHeader(handle)
|
header, err := xdb.LoadHeader(handle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
handle.Close()
|
||||||
return nil, fmt.Errorf("failed to load header from `%s`: %s", dbPath, err)
|
return nil, fmt.Errorf("failed to load header from `%s`: %s", dbPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := xdb.VersionFromHeader(header)
|
version, err := xdb.VersionFromHeader(header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
handle.Close()
|
||||||
return nil, fmt.Errorf("failed to detect IP version from `%s`: %s", dbPath, err)
|
return nil, fmt.Errorf("failed to detect IP version from `%s`: %s", dbPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch cachePolicy {
|
switch cachePolicy {
|
||||||
case "nil", "file":
|
case "nil", "file":
|
||||||
return xdb.NewWithFileOnly(version, dbPath)
|
return xdb.INewSearcher(version, handle, nil, nil), nil
|
||||||
case "vectorIndex":
|
case "vectorIndex":
|
||||||
vIndex, err := xdb.LoadVectorIndexFromFile(dbPath)
|
vIndex, err := xdb.LoadVectorIndex(handle)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
handle.Close()
|
||||||
return nil, fmt.Errorf("failed to load vector index from `%s`: %w", dbPath, err)
|
return nil, fmt.Errorf("failed to load vector index from `%s`: %w", dbPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return xdb.NewWithVectorIndex(version, dbPath, vIndex)
|
return xdb.INewSearcher(version, handle, vIndex, nil), nil
|
||||||
case "content":
|
case "content":
|
||||||
cBuff, err := xdb.LoadContentFromFile(dbPath)
|
cBuff, err := xdb.LoadContent(handle)
|
||||||
|
handle.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load content from '%s': %w", dbPath, err)
|
return nil, fmt.Errorf("failed to load content from '%s': %w", dbPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return xdb.NewWithBuffer(version, cBuff)
|
return xdb.NewWithBuffer(version, cBuff)
|
||||||
default:
|
default:
|
||||||
|
handle.Close()
|
||||||
return nil, fmt.Errorf("invalid cache policy `%s`, options: file/vectorIndex/content", cachePolicy)
|
return nil, fmt.Errorf("invalid cache policy `%s`, options: file/vectorIndex/content", cachePolicy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ func newConfig(cachePolicy int, ipVersion *xdb.Version, xdbPath string, searcher
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer handle.Close()
|
||||||
|
|
||||||
// 1, verify the xdb
|
// 1, verify the xdb
|
||||||
err = xdb.Verify(handle)
|
err = xdb.Verify(handle)
|
||||||
|
|
@ -90,7 +91,7 @@ func newConfig(cachePolicy int, ipVersion *xdb.Version, xdbPath string, searcher
|
||||||
}
|
}
|
||||||
|
|
||||||
if xIpVersion.Id != ipVersion.Id {
|
if xIpVersion.Id != ipVersion.Id {
|
||||||
return nil, fmt.Errorf("ip verison not match: xdb file %s with ip version=%s, as %s expected", xdbPath, xIpVersion.Name, ipVersion.Name)
|
return nil, fmt.Errorf("ip version not match: xdb file %s with ip version=%s, as %s expected", xdbPath, xIpVersion.Name, ipVersion.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3, check and load the vector index buffer
|
// 3, check and load the vector index buffer
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ func NewIp2Region(v4Config *Config, v6Config *Config) (*Ip2Region, error) {
|
||||||
v6InMemSearcher = nil
|
v6InMemSearcher = nil
|
||||||
v6Pool, err = NewSearcherPool(v6Config)
|
v6Pool, err = NewSearcherPool(v6Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create v6 in-memeory searcher pool: %w", err)
|
return nil, fmt.Errorf("failed to create v6 in-memory searcher pool: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ func NewSearcherPool(config *Config) (*SearcherPool, error) {
|
||||||
return nil, fmt.Errorf("config.searchers must > 0")
|
return nil, fmt.Errorf("config.searchers must > 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
pool := make(chan *xdb.Searcher, config.searchers+1)
|
pool := make(chan *xdb.Searcher, config.searchers)
|
||||||
// check and create all the searchers
|
// check and create all the searchers
|
||||||
for i := 0; i < config.searchers; i++ {
|
for i := 0; i < config.searchers; i++ {
|
||||||
searcher, err := xdb.NewSearcher(config.ipVersion, config.xdbPath, config.vIndex, config.cBuffer)
|
searcher, err := xdb.NewSearcher(config.ipVersion, config.xdbPath, config.vIndex, config.cBuffer)
|
||||||
|
|
@ -91,23 +91,27 @@ func (sp *SearcherPool) Close() {
|
||||||
|
|
||||||
func (sp *SearcherPool) CloseTimeout(d time.Duration) {
|
func (sp *SearcherPool) CloseTimeout(d time.Duration) {
|
||||||
close(sp.closing)
|
close(sp.closing)
|
||||||
|
timer := time.NewTimer(d)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
timeout := false
|
|
||||||
select {
|
select {
|
||||||
case s := <-sp.pool:
|
case s := <-sp.pool:
|
||||||
s.Close()
|
s.Close()
|
||||||
case <-time.After(d):
|
case <-timer.C:
|
||||||
// check if all the loaned searchers was closed
|
// hard deadline reached — drain remaining pool items and return
|
||||||
timeout = true
|
for {
|
||||||
|
select {
|
||||||
|
case s := <-sp.pool:
|
||||||
|
s.Close()
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lc, left := sp.LoanCount(), len(sp.pool)
|
if len(sp.pool) == 0 && sp.LoanCount() == 0 {
|
||||||
if left == 0 && lc == 0 {
|
return
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeout {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,35 @@ func TestV4SearcherPool(t *testing.T) {
|
||||||
searcherPool.Close()
|
searcherPool.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestV4SearcherPoolNoCache(t *testing.T) {
|
||||||
|
// verify SearcherPool works with NoCache policy (file-based reads)
|
||||||
|
v4Config, err := NewV4Config(NoCache, "../../../data/ip2region_v4.xdb", 3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to new v4 config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
searcherPool, err := NewSearcherPool(v4Config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create searcher pool: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipString := "219.133.110.197"
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
searcher := searcherPool.BorrowSearcher()
|
||||||
|
region, err := searcher.Search(ipString)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to search(%s): %s", ipString, err)
|
||||||
|
}
|
||||||
|
if region == "" {
|
||||||
|
t.Fatalf("search(%s) returned empty region", ipString)
|
||||||
|
}
|
||||||
|
fmt.Printf("%2d->search(%s)=%s\n", i, ipString, region)
|
||||||
|
searcherPool.ReturnSearcher(searcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
searcherPool.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func TestV6SearcherPool(t *testing.T) {
|
func TestV6SearcherPool(t *testing.T) {
|
||||||
v6Config, err := NewV6Config(VIndexCache, "../../../data/ip2region_v6.xdb", 5)
|
v6Config, err := NewV6Config(VIndexCache, "../../../data/ip2region_v6.xdb", 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,8 @@ type Header struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHeader(input []byte) (*Header, error) {
|
func NewHeader(input []byte) (*Header, error) {
|
||||||
if len(input) < 16 {
|
if len(input) < 20 {
|
||||||
return nil, fmt.Errorf("invalid input buffer")
|
return nil, fmt.Errorf("invalid input buffer, expected at least 20 bytes, got %d", len(input))
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Header{
|
return &Header{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright 2022 The Ip2Region Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a Apache2.0-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package xdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewHeaderShortInput(t *testing.T) {
|
||||||
|
// input shorter than 20 bytes should be rejected
|
||||||
|
short := make([]byte, 16)
|
||||||
|
_, err := NewHeader(short)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("NewHeader with 16 bytes should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 20 bytes should be accepted
|
||||||
|
valid := make([]byte, 20)
|
||||||
|
_, err = NewHeader(valid)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewHeader with 20 bytes should succeed: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHeaderParsing(t *testing.T) {
|
||||||
|
// construct a known 20-byte header
|
||||||
|
input := []byte{
|
||||||
|
0x03, 0x00, // Version = 3 (LE)
|
||||||
|
0x01, 0x00, // IndexPolicy = 1 (LE)
|
||||||
|
0x00, 0x00, 0x00, 0x00, // CreatedAt = 0
|
||||||
|
0x00, 0x00, 0x00, 0x00, // StartIndexPtr = 0
|
||||||
|
0x00, 0x00, 0x00, 0x00, // EndIndexPtr = 0
|
||||||
|
0x04, 0x00, // IPVersion = 4
|
||||||
|
0x04, 0x00, // RuntimePtrBytes = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := NewHeader(input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewHeader failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Version != 3 {
|
||||||
|
t.Errorf("Version = %d, want 3", h.Version)
|
||||||
|
}
|
||||||
|
if h.IndexPolicy != 1 {
|
||||||
|
t.Errorf("IndexPolicy = %d, want 1", h.IndexPolicy)
|
||||||
|
}
|
||||||
|
if h.IPVersion != 4 {
|
||||||
|
t.Errorf("IPVersion = %d, want 4", h.IPVersion)
|
||||||
|
}
|
||||||
|
if h.RuntimePtrBytes != 4 {
|
||||||
|
t.Errorf("RuntimePtrBytes = %d, want 4", h.RuntimePtrBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,10 @@ type Searcher struct {
|
||||||
// content buffer.
|
// content buffer.
|
||||||
// running with the whole xdb file cached
|
// running with the whole xdb file cached
|
||||||
contentBuff []byte
|
contentBuff []byte
|
||||||
|
|
||||||
|
// pre-allocated segment index buffer (file mode only).
|
||||||
|
// reduces per-query heap allocation.
|
||||||
|
segBuff []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWithFileOnly(version *Version, dbFile string) (*Searcher, error) {
|
func NewWithFileOnly(version *Version, dbFile string) (*Searcher, error) {
|
||||||
|
|
@ -68,6 +72,7 @@ func NewSearcher(version *Version, dbFile string, vIndex []byte, cBuff []byte) (
|
||||||
version: version,
|
version: version,
|
||||||
dbReader: handle,
|
dbReader: handle,
|
||||||
vectorIndex: vIndex,
|
vectorIndex: vIndex,
|
||||||
|
segBuff: make([]byte, version.SegmentIndexSize),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,7 +169,14 @@ func (s *Searcher) Search(ip any) (string, error) {
|
||||||
var bytes, dBytes = len(ipBytes), len(ipBytes) << 1
|
var bytes, dBytes = len(ipBytes), len(ipBytes) << 1
|
||||||
var segIndexSize = uint32(s.version.SegmentIndexSize)
|
var segIndexSize = uint32(s.version.SegmentIndexSize)
|
||||||
var dataLen, dataPtr = 0, uint32(0)
|
var dataLen, dataPtr = 0, uint32(0)
|
||||||
var buff = make([]byte, segIndexSize)
|
// use pre-allocated buffer for file-mode searchers (goroutine-private)
|
||||||
|
// to avoid per-query heap allocation; fall back for shared content-mode
|
||||||
|
var buff []byte
|
||||||
|
if s.segBuff != nil {
|
||||||
|
buff = s.segBuff
|
||||||
|
} else {
|
||||||
|
buff = make([]byte, segIndexSize)
|
||||||
|
}
|
||||||
var l, h = 0, int((ePtr - sPtr) / segIndexSize)
|
var l, h = 0, int((ePtr - sPtr) / segIndexSize)
|
||||||
for l <= h {
|
for l <= h {
|
||||||
m := (l + h) >> 1
|
m := (l + h) >> 1
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,16 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// bytesToInt64 converts a big-endian byte slice (variable length) to int64.
|
||||||
|
// This handles the carry byte produced by IPSub when the addition overflows 4 bytes.
|
||||||
|
func bytesToInt64(buf []byte) int64 {
|
||||||
|
var v int64
|
||||||
|
for _, b := range buf {
|
||||||
|
v = (v << 8) | int64(b)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseIP(t *testing.T) {
|
func TestParseIP(t *testing.T) {
|
||||||
var ips = []string{"29.34.191.255", "2c0f:fff0::", "2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}
|
var ips = []string{"29.34.191.255", "2c0f:fff0::", "2fff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"}
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
|
|
@ -51,39 +61,83 @@ func TestIPSub(t *testing.T) {
|
||||||
var intToSub = int(binary.BigEndian.Uint32(bytesToSub))
|
var intToSub = int(binary.BigEndian.Uint32(bytesToSub))
|
||||||
t.Logf("to sub ip: %d -> %s", intToSub, strToSub)
|
t.Logf("to sub ip: %d -> %s", intToSub, strToSub)
|
||||||
|
|
||||||
counter := 0
|
// edge cases
|
||||||
|
edgeValues := []uint32{0, 1, 2, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFE, 0xFFFFFFFF}
|
||||||
|
for _, v := range edgeValues {
|
||||||
buf := make([]byte, 4)
|
buf := make([]byte, 4)
|
||||||
for i := 0; i < 0x2FFFFFFF; i++ {
|
binary.BigEndian.PutUint32(buf, v)
|
||||||
binary.BigEndian.PutUint32(buf, uint32(i))
|
subVal, err := IPSub(buf, bytesToSub)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to IPSub(%s,%s): %s", IP2String(buf), strToSub, err)
|
||||||
|
}
|
||||||
|
byteSub := bytesToInt64(subVal)
|
||||||
|
intSub := int64(v) + int64(intToSub)
|
||||||
|
if byteSub != intSub {
|
||||||
|
t.Fatalf("IPSub(%d, %d): byte=%d, int=%d", v, intToSub, byteSub, intSub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stride-based sampling across the full range
|
||||||
|
counter := 0
|
||||||
|
const stride = 0x100000
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
for i := uint32(0); i < 0xFFFFFFFF-stride; i += stride {
|
||||||
|
binary.BigEndian.PutUint32(buf, i)
|
||||||
subVal, err := IPSub(buf, bytesToSub)
|
subVal, err := IPSub(buf, bytesToSub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to IPSub(%s,%s): %s", IP2String(buf), strToSub, err)
|
t.Fatalf("failed to IPSub(%s,%s): %s", IP2String(buf), strToSub, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// do it as two integers
|
byteSub := bytesToInt64(subVal)
|
||||||
byteSub := int(binary.BigEndian.Uint32(subVal))
|
intSub := int64(i) + int64(intToSub)
|
||||||
intSub := i + intToSub
|
|
||||||
if byteSub != intSub {
|
if byteSub != intSub {
|
||||||
t.Fatal("byte and int sub value are not the same")
|
t.Fatal("byte and int sub value are not the same")
|
||||||
}
|
}
|
||||||
|
|
||||||
counter++
|
counter++
|
||||||
}
|
}
|
||||||
|
// also test the final boundary value
|
||||||
|
binary.BigEndian.PutUint32(buf, 0xFFFFFFFF)
|
||||||
|
subVal, err := IPSub(buf, bytesToSub)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to IPSub(%s,%s): %s", IP2String(buf), strToSub, err)
|
||||||
|
}
|
||||||
|
byteSub := bytesToInt64(subVal)
|
||||||
|
intSub := int64(4294967295) + int64(intToSub)
|
||||||
|
if byteSub != intSub {
|
||||||
|
t.Fatalf("IPSub(0xFFFFFFFF, %d): byte=%d, int=%d", intToSub, byteSub, intSub)
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
|
||||||
t.Logf("test done with %d ips", counter)
|
t.Logf("test done with %d ips (sampled)", counter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIPHalf(t *testing.T) {
|
func TestIPHalf(t *testing.T) {
|
||||||
var buf = make([]byte, 4)
|
// edge cases
|
||||||
for i := 0; i < 0xFFFFFFFF; i++ {
|
edgeValues := []uint32{0, 1, 2, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFE, 0xFFFFFFFF}
|
||||||
binary.BigEndian.PutUint32(buf, uint32(i))
|
for _, v := range edgeValues {
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(buf, v)
|
||||||
|
half := IPHalf(buf)
|
||||||
|
|
||||||
|
byteMiddle := binary.BigEndian.Uint32(half)
|
||||||
|
intMidle := v >> 1
|
||||||
|
if byteMiddle != intMidle {
|
||||||
|
t.Fatalf("IPHalf(0x%08x): byte=0x%08x, int=0x%08x", v, byteMiddle, intMidle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stride-based sampling across the full range
|
||||||
|
const stride = 0x100000
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
for i := uint32(0); i < 0xFFFFFFFF-stride; i += stride {
|
||||||
|
binary.BigEndian.PutUint32(buf, i)
|
||||||
half := IPHalf(buf)
|
half := IPHalf(buf)
|
||||||
|
|
||||||
// do it as two integers
|
|
||||||
byteMiddle := binary.BigEndian.Uint32(half)
|
byteMiddle := binary.BigEndian.Uint32(half)
|
||||||
intMidle := i >> 1
|
intMidle := i >> 1
|
||||||
if byteMiddle != uint32(intMidle) {
|
if byteMiddle != intMidle {
|
||||||
t.Fatal("byte middle and int middle are not the same")
|
t.Fatalf("IPHalf(0x%08x): byte=0x%08x, int=0x%08x", i, byteMiddle, intMidle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,18 +170,35 @@ func TestIPMiddle(t *testing.T) {
|
||||||
var sInt = int(binary.BigEndian.Uint32(sBytes))
|
var sInt = int(binary.BigEndian.Uint32(sBytes))
|
||||||
t.Logf("start ip: %d -> %s", sInt, sIPStr)
|
t.Logf("start ip: %d -> %s", sInt, sIPStr)
|
||||||
|
|
||||||
counter := 0
|
// edge cases
|
||||||
|
edgeValues := []uint32{0, 1, 2, 0x7FFFFFFF, 0x80000000, 0xFFFFFFFE, 0xFFFFFFFF}
|
||||||
|
for _, v := range edgeValues {
|
||||||
buf := make([]byte, 4)
|
buf := make([]byte, 4)
|
||||||
for i := 0; i < 0x0FFFFFFF; i++ {
|
binary.BigEndian.PutUint32(buf, v)
|
||||||
binary.BigEndian.PutUint32(buf, uint32(i))
|
midVal, err := IPMiddle(sBytes, buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to IPMiddle(%s,%s): %s", sIPStr, IP2String(buf), err)
|
||||||
|
}
|
||||||
|
byteMid := int(binary.BigEndian.Uint32(midVal))
|
||||||
|
intMid := (sInt + int(v)) >> 1
|
||||||
|
if byteMid != intMid {
|
||||||
|
t.Fatalf("IPMiddle(%d, %d): byte=%d, int=%d", sInt, v, byteMid, intMid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stride-based sampling across the full range
|
||||||
|
counter := 0
|
||||||
|
const stride = 0x100000
|
||||||
|
buf := make([]byte, 4)
|
||||||
|
for i := uint32(0); i < 0xFFFFFFFF-stride; i += stride {
|
||||||
|
binary.BigEndian.PutUint32(buf, i)
|
||||||
midVal, err := IPMiddle(sBytes, buf)
|
midVal, err := IPMiddle(sBytes, buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to IPMiddle(%s,%s): %s", sIPStr, IP2String(buf), err)
|
t.Fatalf("failed to IPMiddle(%s,%s): %s", sIPStr, IP2String(buf), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// do it as two integers
|
|
||||||
byteMid := int(binary.BigEndian.Uint32(midVal))
|
byteMid := int(binary.BigEndian.Uint32(midVal))
|
||||||
intMid := (sInt + i) >> 1
|
intMid := (sInt + int(i)) >> 1
|
||||||
if byteMid != intMid {
|
if byteMid != intMid {
|
||||||
t.Fatal("byte and int middle value are not the same")
|
t.Fatal("byte and int middle value are not the same")
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +206,7 @@ func TestIPMiddle(t *testing.T) {
|
||||||
counter++
|
counter++
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("test done with %d ips", counter)
|
t.Logf("test done with %d ips (sampled)", counter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadVectorIndex(t *testing.T) {
|
func TestLoadVectorIndex(t *testing.T) {
|
||||||
|
|
@ -158,6 +229,73 @@ func TestLoadContent(t *testing.T) {
|
||||||
fmt.Printf("buff length: %d\n", len(buff))
|
fmt.Printf("buff length: %d\n", len(buff))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIPv4CompareCorrectness(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip1 []byte // big-endian (from ParseIP)
|
||||||
|
ip2 []byte // little-endian (from xdb index)
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
// equal
|
||||||
|
{"equal", []byte{1, 2, 3, 4}, []byte{4, 3, 2, 1}, 0},
|
||||||
|
{"equal_zero", []byte{0, 0, 0, 0}, []byte{0, 0, 0, 0}, 0},
|
||||||
|
{"equal_max", []byte{255, 255, 255, 255}, []byte{255, 255, 255, 255}, 0},
|
||||||
|
|
||||||
|
// less at each byte position
|
||||||
|
{"less_b0", []byte{0, 2, 3, 4}, []byte{4, 3, 2, 1}, -1},
|
||||||
|
{"less_b1", []byte{1, 1, 3, 4}, []byte{4, 3, 2, 1}, -1},
|
||||||
|
{"less_b2", []byte{1, 2, 2, 4}, []byte{4, 3, 2, 1}, -1},
|
||||||
|
{"less_b3", []byte{1, 2, 3, 3}, []byte{4, 3, 2, 1}, -1},
|
||||||
|
|
||||||
|
// greater at each byte position
|
||||||
|
{"greater_b0", []byte{2, 2, 3, 4}, []byte{4, 3, 2, 1}, 1},
|
||||||
|
{"greater_b1", []byte{1, 3, 3, 4}, []byte{4, 3, 2, 1}, 1},
|
||||||
|
{"greater_b2", []byte{1, 2, 4, 4}, []byte{4, 3, 2, 1}, 1},
|
||||||
|
{"greater_b3", []byte{1, 2, 3, 5}, []byte{4, 3, 2, 1}, 1},
|
||||||
|
|
||||||
|
// edge: LE ip2 with byte0=0x00 (common for small IPs)
|
||||||
|
{"le_zero_leading", []byte{1, 0, 0, 1}, []byte{1, 0, 0, 1}, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ip2Copy := make([]byte, 4)
|
||||||
|
copy(ip2Copy, tc.ip2)
|
||||||
|
got := IPv4.IPCompare(tc.ip1, tc.ip2)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Errorf("IPv4.IPCompare(%v, %v) = %d, want %d", tc.ip1, tc.ip2, got, tc.want)
|
||||||
|
}
|
||||||
|
// verify ip2 is not mutated
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
if tc.ip2[i] != ip2Copy[i] {
|
||||||
|
t.Errorf("IPv4.IPCompare mutated ip2 at byte %d: was 0x%02x, now 0x%02x", i, ip2Copy[i], tc.ip2[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPCompareNoMutation(t *testing.T) {
|
||||||
|
// verify that IPv4.IPCompare does NOT mutate the input ip2 slice
|
||||||
|
ip1, _ := ParseIP("1.2.3.4")
|
||||||
|
ip2Orig := []byte{0x04, 0x03, 0x02, 0x01} // LE representation of 1.2.3.4
|
||||||
|
ip2Copy := make([]byte, 4)
|
||||||
|
copy(ip2Copy, ip2Orig)
|
||||||
|
|
||||||
|
result := IPv4.IPCompare(ip1, ip2Orig)
|
||||||
|
if result != 0 {
|
||||||
|
t.Fatalf("IPv4.IPCompare(1.2.3.4, LE[1.2.3.4]) = %d, want 0", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify ip2 is unchanged
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
if ip2Orig[i] != ip2Copy[i] {
|
||||||
|
t.Fatalf("IPv4.IPCompare mutated ip2 at byte %d: was 0x%02x, now 0x%02x", i, ip2Copy[i], ip2Orig[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = ip2Copy
|
||||||
|
}
|
||||||
|
|
||||||
func TestLoadHeader(t *testing.T) {
|
func TestLoadHeader(t *testing.T) {
|
||||||
header, err := LoadHeaderFromFile("../../../data/ip2region_v4.xdb")
|
header, err := LoadHeaderFromFile("../../../data/ip2region_v4.xdb")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,33 @@ var (
|
||||||
SegmentIndexSize: 14, // 4 + 4 + 2 + 4,
|
SegmentIndexSize: 14, // 4 + 4 + 2 + 4,
|
||||||
IPCompare: func(ip1, ip2 []byte) int {
|
IPCompare: func(ip1, ip2 []byte) int {
|
||||||
// ip1 - with Big endian byte order parsed from an input
|
// ip1 - with Big endian byte order parsed from an input
|
||||||
// ip2 - with Little endian byte order read from the xdb index
|
// ip2 - with Little endian byte order read from the xdb index (LE)
|
||||||
ip2[0], ip2[3] = ip2[3], ip2[0]
|
// compare without mutating ip2
|
||||||
ip2[1], ip2[2] = ip2[2], ip2[1]
|
if ip1[0] != ip2[3] {
|
||||||
return bytes.Compare(ip1, ip2)
|
if ip1[0] < ip2[3] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if ip1[1] != ip2[2] {
|
||||||
|
if ip1[1] < ip2[2] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if ip1[2] != ip2[1] {
|
||||||
|
if ip1[2] < ip2[1] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if ip1[3] != ip2[0] {
|
||||||
|
if ip1[3] < ip2[0] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
IPv6 = &Version{
|
IPv6 = &Version{
|
||||||
|
|
@ -90,7 +113,7 @@ func VersionFromHeader(header *Header) (*Version, error) {
|
||||||
|
|
||||||
// structure 3.0 after IPv6 supporting
|
// structure 3.0 after IPv6 supporting
|
||||||
if header.Version != Structure30 {
|
if header.Version != Structure30 {
|
||||||
return IPvx, fmt.Errorf("invalid version `%d`", header.IPVersion)
|
return IPvx, fmt.Errorf("invalid structure version: %d", header.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch header.IPVersion {
|
switch header.IPVersion {
|
||||||
|
|
@ -99,6 +122,6 @@ func VersionFromHeader(header *Header) (*Version, error) {
|
||||||
case IPv6VersionNo:
|
case IPv6VersionNo:
|
||||||
return IPv6, nil
|
return IPv6, nil
|
||||||
default:
|
default:
|
||||||
return IPvx, fmt.Errorf("invalid version `%d`", header.Version)
|
return IPvx, fmt.Errorf("invalid ip version: %d", header.IPVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
// 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 xdb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVersionFromHeader(t *testing.T) {
|
||||||
|
// Structure20 -> always IPv4 (legacy format)
|
||||||
|
h := &Header{Version: Structure20}
|
||||||
|
v, err := VersionFromHeader(h)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VersionFromHeader(Structure20) unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if v != IPv4 {
|
||||||
|
t.Fatalf("VersionFromHeader(Structure20) = %s, want IPv4", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structure30 + IPv4VersionNo
|
||||||
|
h = &Header{Version: Structure30, IPVersion: IPv4VersionNo}
|
||||||
|
v, err = VersionFromHeader(h)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VersionFromHeader(Structure30,IPv4) unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if v != IPv4 {
|
||||||
|
t.Fatalf("VersionFromHeader(Structure30,IPv4) = %s, want IPv4", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structure30 + IPv6VersionNo
|
||||||
|
h = &Header{Version: Structure30, IPVersion: IPv6VersionNo}
|
||||||
|
v, err = VersionFromHeader(h)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VersionFromHeader(Structure30,IPv6) unexpected error: %s", err)
|
||||||
|
}
|
||||||
|
if v != IPv6 {
|
||||||
|
t.Fatalf("VersionFromHeader(Structure30,IPv6) = %s, want IPv6", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalid structure version
|
||||||
|
h = &Header{Version: 999}
|
||||||
|
_, err = VersionFromHeader(h)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("VersionFromHeader(invalid version) should return error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structure30 + invalid IP version
|
||||||
|
h = &Header{Version: Structure30, IPVersion: 999}
|
||||||
|
_, err = VersionFromHeader(h)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("VersionFromHeader(Structure30,invalid IPVersion) should return error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionFromName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want *Version
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"v4", IPv4, true},
|
||||||
|
{"V4", IPv4, true},
|
||||||
|
{"ipv4", IPv4, true},
|
||||||
|
{"IPv4", IPv4, true},
|
||||||
|
{"v6", IPv6, true},
|
||||||
|
{"V6", IPv6, true},
|
||||||
|
{"ipv6", IPv6, true},
|
||||||
|
{"IPv6", IPv6, true},
|
||||||
|
{"ipv4", IPv4, true},
|
||||||
|
{"IPV6", IPv6, true},
|
||||||
|
{"", nil, false},
|
||||||
|
{"invalid", nil, false},
|
||||||
|
{"v7", nil, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
v, err := VersionFromName(tc.name)
|
||||||
|
if tc.ok {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VersionFromName(%q) unexpected error: %s", tc.name, err)
|
||||||
|
}
|
||||||
|
if v != tc.want {
|
||||||
|
t.Fatalf("VersionFromName(%q) = %s, want %s", tc.name, v, tc.want)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("VersionFromName(%q) should return error", tc.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionFromIP(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
ip string
|
||||||
|
want *Version
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{"1.2.3.4", IPv4, true},
|
||||||
|
{"0.0.0.0", IPv4, true},
|
||||||
|
{"255.255.255.255", IPv4, true},
|
||||||
|
{"::1", IPv6, true},
|
||||||
|
{"240e:3b7::1", IPv6, true},
|
||||||
|
{"", nil, false},
|
||||||
|
{"not-an-ip", nil, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.ip, func(t *testing.T) {
|
||||||
|
v, err := VersionFromIP(tc.ip)
|
||||||
|
if tc.ok {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("VersionFromIP(%q) unexpected error: %s", tc.ip, err)
|
||||||
|
}
|
||||||
|
if v != tc.want {
|
||||||
|
t.Fatalf("VersionFromIP(%q) = %s, want %s", tc.ip, v, tc.want)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("VersionFromIP(%q) should return error", tc.ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIPv6Compare(t *testing.T) {
|
||||||
|
// IPv6.IPCompare uses bytes.Compare directly, no byte-order swapping needed
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip1 []byte
|
||||||
|
ip2 []byte
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"equal", []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 0},
|
||||||
|
{"less_b0", []byte{0x1f, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, -1},
|
||||||
|
{"greater_b0", []byte{0x21, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, []byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, 1},
|
||||||
|
{"zero_vs_max", []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, -1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := IPv6.IPCompare(tc.ip1, tc.ip2)
|
||||||
|
if got != tc.want {
|
||||||
|
t.Fatalf("IPv6.IPCompare(%v, %v) = %d, want %d", tc.ip1, tc.ip2, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue